Upgrade to RESTHeart v8
RESTHeart v8 introduces many new features, improvements, and changes.
This page summarizes the new features and provides guidance on upgrading from previous versions.
Tip
|
Read What’s new in RESTHeart v8 on Medium |
Virtual Threads
Java 21 introduces Virtual Threads, a feature detailed in JEP 444. These lightweight threads significantly boost performance and simplify concurrent programming.
RESTHeart 8 harnesses Executors.newVirtualThreadPerTaskExecutor()
to manage requests handled by services marked as blocking (with RegisterPlugin(blocking = true)
by default), efficiently processing blocking services.
Furthermore, RESTHeart 8 configures Undertow to use its ThreadAwareByteBufferPool
for optimized resource allocation:
-
Platform Threads: Uses the
undertow.DefaultByteBufferPool
to efficiently pool resources. -
Virtual Threads: Utilizes
NotPoolingByteBufferPool
, which avoids pooling to enhance performance given the distinct characteristics of virtual threads.
Rationale
Virtual threads are lightweight, high-throughput threads that simplify the development, maintenance, and monitoring of concurrent applications.
Configuration
The following options manage virtual threads:
core:
# Specifies the initial number of platform carrier threads used for virtual threads in blocking operations.
# Recommended value: 1.5 * number of cores.
# If <= 0, it defaults to 1.5 times the system core count.
workers-scheduler-parallelism: 0
# Sets the maximum number of platform carrier threads for virtual threads in blocking operations.
workers-scheduler-max-pool-size: 256
Cookie Authentication
Allows storing the auth token in a secure cookie and enables authentication from it.
Rationale
-
Using cookie authentication and JWT tokens effectively enables Single Sign-On.
-
Typically, a client first authenticates using Basic Authentication and then uses the auth token returned in the first response for further requests. This auth token is usually stored in the local storage by web clients. The local storage is readable by JavaScript, thus exposing this approach to Cross-site Scripting (XSS) security attacks. Storing the auth token in a secure cookie avoids XSS.
Overview
Cookie Authentication allows the client browser to store an authentication token in a secure cookie. This mechanism enables authentication based on the stored cookie, allowing the client to remain authenticated across multiple requests without having to send credentials each time. The token is securely saved in the cookie, ensuring that sensitive data is protected and accessible only to the intended server.
Configuration Options
Cookie Authentication is disabled by default. To enable it, the following three plugins must be enabled and configured: authCookieSetter
, authCookieHandler
, and authCookieRemover
.
The cookie authentication mechanism can function using three different options:
Option 1: JWT Verified by jwtAuthenticationMechanism
This option is recommended if you also want to allow clients to authenticate via JWTs sent in the Authorization
header (not stored in a cookie).
/tokenBasicAuthMechanism/enabled->true|false
/jwtAuthenticationMechanism/enabled->true
/jwtTokenManager/enabled->true
/rndTokenManager/enabled->false
Option 2: JWT Verified by tokenBasicAuthMechanism
Choose this option if you have multiple instances of RESTHeart verifying cookies, and JWT header-based authentication isn’t required.
/tokenBasicAuthMechanism/enabled->true
/jwtAuthenticationMechanism/enabled->false
/jwtTokenManager/enabled->true
/rndTokenManager/enabled->false
Option 3: RGT Cookies (Randomly Generated Tokens) For authentication via RGT cookies, enable these components:
-
tokenBasicAuthMechanism
: Manages the basic authentication process. -
rndTokenManager
: Manages and validates randomly generated tokens for Basic Authentication cookies.
/tokenBasicAuthMechanism/enabled->true
/jwtAuthenticationMechanism/enabled->false
/jwtTokenManager/enabled->false
/rndTokenManager/enabled->true
Note: RGT cookies can only be verified by the RESTHeart instance that issued them. For multi-instance deployments, it is advisable to use JWT cookies instead.
authCookieSetter
This component is responsible for initiating a user’s authenticated session by setting the authentication cookie.
Activates when a URL includes the query parameter ?set-auth-cookie
and a user is authenticated, setting a cookie populated with a token generated by the enabled Token Manager.
Configuration
authCookieSetter:
enabled: false # Not enabled by default
name: rh_auth # The name of the cookie to be set
domain: localhost # The domain within which the cookie is valid
path: / # The cookie path, applicable to the entire domain
http-only: true # If true, enhances security by making the cookie inaccessible to JavaScript
same-site: true # Restricts the cookie to first-party contexts, preventing CSRF attacks
same-site-mode: strict # Strictly prevents the cookie from being sent along with cross-site requests
expires-ttl: 86400 # Defines the duration (in seconds, default 1 day) for which the cookie is valid
When using JWT tokens, the cookie can be updated by specifying both ?set-auth-cookie&renew-auth-token
. The query parameter renew-auth-token
forces the jwtTokenManager
to update the JWT.
authCookieHandler
Responsible for utilizing the authentication cookie to maintain authenticated sessions across requests.
Reads the rh_auth
cookie (actual cookie name is defined in authCookieSetter configuration), if available, and constructs an Authorization header. This allows for seamless continuation of sessions and supports both Basic and JWT authentication mechanisms.
Configuration
authCookieHandler:
enabled: false # Not enabled by default
authCookieRemover
Handles the secure and explicit termination of authenticated sessions.
Clears the authentication cookie in response to a POST /logout
request. This effectively logs out the user by wiping the authentication cookie from the user’s browser, ensuring the session is securely terminated.
Configuration
authCookieRemover:
enabled: false # Not enabled by default
secure: false # If the request to clean the cookie should be authenticated
defaultUri: /logout # The endpoint that triggers this service
Example usage
This is an example of how a user might log in, make some requests, and then log out within a system using cookie authentication with the configuration described previously. This example assumes that the system is web-based and communicates over HTTP.
Logging In
The user submits their credentials (username and password) via Basic Authentication (Authorization
header) from a form on a client application, which sends a GET request to the`/roles/{username}` endpoint, including the ?set-auth-cookie
query parameters
GET /roles/{username}?set-auth-cookie HTTP/1.1
Host: localhost
Content-Type: application/json
Authorization: Basic YWRtaW46c2VjcmV0
If the credentials are valid, the server responds by setting an rh_auth
cookie containing the authentication token and returns a success response.
HTTP/1.1 200 OK
Set-Cookie: rh_auth="Basic YWRtaW46MmliNWFsaDFxajZ4eHY5aWlyOTZsejh1bnJjMHQzNWFucnEyYzh1cG12cHNpOGc3dDQ="; Version=1; Path=/; Domain=localhost; Secure; HttpOnly; Expires=Sat, 20 Apr 2024 11:53:00 GMT; SameSite=Strict
Content-Type: application/json
{
"authenticated": true,
"roles": [ "user-role" ]
}
Note that the value of the cookie doesn’t include the actual user credentials but uses the auth token generated by the enabled Token Manager.
Making Authenticated Requests
Once the cookie is set, the user can make subsequent requests to the server. The browser automatically includes the rh_auth
cookie with each request to the domain.
For example, if the user wants to access a protected resource, they might send a GET request to the server:
GET /protected-resource HTTP/1.1
Host: localhost
Cookie: rh_auth="Basic YWRtaW46MmliNWFsaDFxajZ4eHY5aWlyOTZsejh1bnJjMHQzNWFucnEyYzh1cG12cHNpOGc3dDQ="
The server checks the cookie, validates the session, and if valid, responds with the requested data.
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": "Here is your protected resource data."
}
Logging Out
To log out, the user sends a POST request to the logout endpoint. This request doesn’t need to include user credentials but should be made from the same domain to ensure the browser includes the authentication cookie.
POST /logout HTTP/1.1
Host: localhost
Cookie: rh_auth=
The server processes the logout request and clears the authentication cookie by setting its value to null.
HTTP/1.1 200 OK
Set-Cookie: rh_auth=; path=/; domain=localhost; secure; HttpOnly; SameSite=Strict
Content-Type: application/json
After this, the user is logged out, and their session is terminated. The cookie is invalidated, and any subsequent requests to the server that require authentication will fail until the user logs in again.
Programmatic Configuration of ACLs
We want to extend defining security policy rules programmatically by allowing both inclusive and exclusive security policies through veto and permission predicates.
Currently, RESTHeart allows defining a set of predicates via PluginRegistry.getGlobalSecurityPredicates()
that must all resolve to true
to allow the request. Under the hood, the global security predicates are enforced by the vetoer authorizer GlobalPredicatesVetoer
.
For clarity, recall that an Authorizer can be either a VETOER or an ALLOWER. A request is allowed when no VETOER denies it and any ALLOWER allows it.
We want to extend and refactor this feature as follows:
-
Move the current logic from
PluginRegistry
to anACLRegistry
that can be injected with@Inject("acl-registry")
-
Rename global security predicates to "veto predicates" and rename the vetoer as
ACLRegistryVetoer
-
Symmetrically add allow predicates and the corresponding allower authorizer
ACLRegistryAllower
Rationale
By extending the definition of security policy rules programmatically, it will be possible to ship a secure service with its own security policy, avoiding the need to configure the ACL.
As an example, the RoleService
mapped to /roles/{userid}
can be secured and allowed to be requested only if the path parameter userid
matches the authenticated user id. Currently, this is not secured to avoid the need to configure the ACL and the authorization is checked in the service code.
Detailed documentation
The ACLRegistry
can be injected with @Inject("acl-registry")
and allows defining Access Control Lists (ACLs) programmatically:
public interface ACLRegistry {
/**
* Registers a veto predicate that determines if a request should be denied.
* When the predicate evaluates to true, the request is immediately forbidden (vetoed).
* Additionally, a request will also be denied if it is not explicitly authorized by any
* allow predicates or any other active allowing authorizers.
*
* @param veto The veto predicate to register. This predicate should return true to veto (deny) the request,
* and false to let the decision be further evaluated by allow predicates or other authorizers.
*/
public void registerVeto(Predicate<Request<?>> veto);
/**
* Registers an allow predicate that determines if a request should be authorized.
* The request is authorized if this predicate evaluates to true, provided that no veto predicates
* or other active vetoer authorizers subsequently deny the request. This method helps in setting up
* conditions under which requests can proceed unless explicitly vetoed.
*
* @param allow The allow predicate to register. This predicate should return true to authorize the request,
* unless it is vetoed by any veto predicates or other vetoing conditions.
*/
public void registerAllow(Predicate<Request<?>> allow);
/**
* Registers a predicate that determines whether requests handled by the ACLRegistryAllower
* require authentication. This method is used to specify conditions under which authentication
* is mandatory. Typically, authentication is required unless there are allow predicates
* explicitly authorizing requests that are not authenticated.
*
* @param authenticationRequired The predicate to determine if authentication is necessary.
* It should return true if the request must be authenticated,
* otherwise false if unauthenticated requests might be allowed.
*/
public void registerAuthenticationRequirement(Predicate<Request<?>> authenticationRequired);
}
This registry is utilized by the ACLRegistryVetoer
and ACLRegistryAllower
authorizers to manage request permissions. The ACLRegistryVetoer
denies requests based on veto predicates, while the ACLRegistryAllower
grants permission to proceed with requests based on allow predicates.
A request is permitted to proceed if it is not denied by any ACLRegistryVetoer
and at least one ACLRegistryAllower
approves it.
Example usage:
@Inject("acl-registry")
ACLRegistry registry;
@OnInit
public void init() {
registry.registerVeto(r -> r.getPath().equals("/deny"));
registry.registerAllow(r -> r.getPath().equals("/allow"));
}
Lazy-load Request Content
Up to RESTHeart v7, requests processed by the MongoService
are designed to lazy-load their content. This means that the request body is only read when MongoRequest.getContent()
is invoked for the first time.
This lazy-loading approach significantly improves performance across various scenarios. For instance, it speeds up the request validation process and ensures that interceptors that don’t need to access the content can execute faster.
RESTHeart 8 extends this behavior to all request types.
Rationale
Optimizing request handling can enhance performance in cases where the request content is unnecessary. A common example includes situations where a request is denied due to insufficient permissions.
Detailed Documentation
The ServiceRequest
class now features a new abstract method to read and parse the request content:
/**
* Parses the content from the exchange and converts it into an instance of the specified type {@code T}.
*
* This method retrieves data from the exchange, interprets it according to the expected format, and converts
* this data into an object of type {@code T}.
*
* @return an instance of {@code T} representing the parsed content
* @throws IOException if an I/O error occurs
* @throws BadRequestException if the content doesn't conform to the expected format of type {@code T}
*/
public abstract T parseContent() throws IOException, BadRequestException;
ServiceRequest.parseContent()
is called by ServiceRequest.getContent()
on its first invocation. The parsed content is then cached and linked to the request, ensuring that any subsequent calls will reuse the already parsed content object.
This approach makes handling request content more efficient by reducing unnecessary parsing and processing overhead.