OAuth 2.0 / 2.1
RESTHeart Cloudπ§ Configuration
Overview
RESTHeart provides a standards-compliant OAuth 2.0/2.1 token endpoint and authorization server. It supports three grant types and the full Authorization Code + PKCE flow for delegated user authentication.
|
Note
|
In all examples below, replace |
Available endpoints:
| Endpoint | Enabled by default | Description |
|---|---|---|
|
yes |
Issues a JWT access token. Supports |
|
yes |
Issues a JWT and sets it as an HttpOnly cookie (browser apps). |
|
yes |
Returns the current token for the authenticated user. |
|
yes |
Since RESTHeart 9.5. Returns the token in the body and redirects to a configured URL with the token appended as a URL fragment ( |
|
yes |
Invalidates the current token. |
|
no |
Starts the Authorization Code + PKCE flow β redirects to the configured login page. |
|
no |
Completes the Authorization Code flow β issues the authorization code after successful login. |
|
no |
Authorization Server Metadata (RFC 8414) β for automatic client discovery. |
|
no |
Protected Resource Metadata (RFC 9728) β advertises the authorization server to MCP clients. |
Token Endpoint
authTokenService is enabled by default and bound to POST /token.
Password Grant
Authenticate with username and password using HTTP Basic Auth:
cURL
curl -i -X POST [RESTHEART-URL]/token \
-u [BASIC-AUTH]
HTTPie
http POST [RESTHEART-URL]/token \
Authorization:"Basic [BASIC-AUTH]"
Or using OAuth 2.0 form data (grant_type=password, RFC 6749 Β§4.3):
HTTPie
http -f POST [RESTHEART-URL]/token \
grant_type=password \
username=admin \
password=secret
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"username": "admin",
"roles": ["admin"]
}
Client Credentials Grant
|
Note
|
Available from RESTHeart v9.2.0. |
For machine-to-machine authentication, use grant_type=client_credentials (RFC 6749 Β§4.4).
The client_id and client_secret map to user credentials in the configured authenticator.
HTTPie
http -f POST [RESTHEART-URL]/token \
grant_type=client_credentials \
client_id=myapp \
client_secret=s3cret
Using the Token
Once you have the token, use it as a Bearer token:
cURL
curl -i -X GET [RESTHEART-URL]/mycollection \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
HTTPie
http GET [RESTHEART-URL]/mycollection \
"Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Token Renewal
To renew a token before it expires, add ?renew to any GET /token or POST /token request:
HTTPie
http GET [RESTHEART-URL]/token?renew \
Authorization:"Bearer <current-token>"
|
Note
|
Token generation is a cryptographic operation with some overhead. The client is responsible for renewing before expiry. |
Cookie-Based Authentication
For browser applications, use /token/cookie to set an HttpOnly cookie (the token is never exposed to JavaScript):
HTTPie
http --session=./session.json POST [RESTHEART-URL]/token/cookie \
Authorization:"Basic [BASIC-AUTH]"
Subsequent requests in the same session automatically include the cookie.
Redirect-Based Authentication
|
Note
|
Available from RESTHeart 9.5. |
Some flows have to end in a real browser navigation instead of an AJAX/fetch call β for example, handing a session token to a single-page app after an OAuth-provider round trip, where there is no JSON response for the frontend’s JavaScript to read. GET /token/redirect covers this case: it returns the token in the response body (same shape as GET /token, for callers that don’t follow the redirect) and issues an HTTP redirect to a configured URL with the token appended as a URL fragment:
|
Note
|
Despite living in this OAuth-focused document, GET /token/redirect is not OAuth-specific β it’s a general endpoint of AuthTokenService (like /token and /token/cookie), usable by any flow that authenticates via a real browser navigation rather than a fetch() call. An OAuth-provider callback is the canonical example, but the same mechanism applies just as well to an email-verification link, a signed "magic link," or any other flow where the client can’t read a JSON response body directly.
|
HTTPie
http --follow=false GET [RESTHEART-URL]/token/redirect \
Authorization:"Basic [BASIC-AUTH]"
Response:
HTTP/1.1 307 Temporary Redirect
Location: https://app.example.com/callback#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&token_type=Bearer&expires_in=899
The token is carried as a URL fragment (#access_token=…), not a query parameter, deliberately: a fragment is never sent to any server β not on the redirect response itself, not on any subsequent request the browser makes to that URL, not in the Referer header. A query parameter, by contrast, travels with every future navigation from that URL and routinely ends up in server access logs, reverse-proxy logs, and analytics β a real leak vector for a bearer token. This mirrors OAuth 2.0’s own long-established Implicit Flow convention (#access_token=…).
The frontend reads location.hash, extracts the token, and should clear the fragment (e.g. via history.replaceState) so it doesn’t linger in browser history.
Configuring the redirect target
The redirect target is never a request parameter β accepting a caller-supplied redirect target here would be an open-redirect vector combined with a token leak (an attacker could craft a link to …/token/redirect?redirect=https://evil.com and, if a victim with a valid session clicked it, their token would end up in a redirect to the attacker’s site). It comes from configuration only:
authTokenService:
uri: /token
redirect-url: https://app.example.com/callback
For multi-tenant deployments where different requests should redirect to different frontends (e.g. one RESTHeart instance serving many independent services, each with its own frontend URL), a plugin can attach a per-request override that takes precedence over the static config:
request.attachParam("override-redirect-url", "https://tenant-a.example.com/callback");
If neither the static config nor the override is set, GET /token/redirect returns 400 Bad Request rather than falling back to something guessable.
Choosing How to Deliver the Token
Which of /token, /token/cookie, or /token/redirect to use is a sign-in process decision, not an OAuth-specific one β see Choosing How to Deliver the Token in "How Clients Authenticate" for the full comparison (pros/cons, when to use each, and the synergy with originVetoer).
Authorization Code + PKCE Flow
|
Note
|
Available from RESTHeart v9.2.1. |
The OAuth 2.1 Authorization Code flow with PKCE (RFC 7636) allows a user to authenticate via a frontend login page. The access token is issued only after successful authentication β no credentials are passed to the client.
This flow is the recommended approach for:
-
Web applications with a frontend login UI
-
MCP clients (e.g., Claude Desktop via
mcp-remote) -
CLI tools that open a browser for login
Flow overview:
Client RESTHeart Frontend login UI
| | |
|-- GET /authorize ------>| |
| (code_challenge, S256)| |
|<-- 302 to login-url ----| |
| |<-- POST /authorize -----|
| | username+password |
| |--(valid)- 302 callback?code=... ->|
| |--(invalid)- 302 login?error=... ->|
|<----------------------------------------------- code ---|
|-- POST /token ---------->|
| (code, code_verifier) |
|<-- access_token ---------|
Step 1: Start the authorization request
Generate a PKCE pair and redirect the user to GET /authorize:
# Generate PKCE pair
CODE_VERIFIER="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | \
openssl base64 | tr '+/' '-_' | tr -d '=')
http --follow=false GET "[RESTHEART-URL]/authorize" \
"response_type==code" \
"client_id==my-app" \
"redirect_uri==https://myapp.example.com/callback" \
"code_challenge==${CODE_CHALLENGE}" \
"code_challenge_method==S256" \
"state==random-state-value"
RESTHeart responds with 302 to the configured login-url, forwarding all OAuth parameters as query string.
Step 2: User authenticates
The frontend presents the login form and POSTs the credentials to POST /authorize.
The OAuth parameters are forwarded as query string (exactly as received from the GET /authorize redirect).
Two credential formats are supported:
Option A β form body (recommended for browser login pages):
RESTHeart reads username and password from the application/x-www-form-urlencoded body.
No Authorization header is needed β the browser’s native form POST works as-is.
http --follow=false POST \
"[RESTHEART-URL]/authorize?response_type=code&client_id=my-app&\
redirect_uri=https://myapp.example.com/callback&\
code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256&state=random-state-value" \
username=admin \
password=secret
On invalid credentials RESTHeart redirects back to login-url?error=invalid_credentials&<original_query_params>
so the login page can show an error without losing the OAuth context.
Option B β HTTP Basic Auth (for API clients / programmatic use):
http --follow=false -a admin:secret POST \
"[RESTHEART-URL]/authorize?response_type=code&client_id=my-app&\
redirect_uri=https://myapp.example.com/callback&\
code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256&state=random-state-value"
|
Note
|
If an Authorization: Basic header is present it always takes precedence over form body credentials.
|
Response (valid credentials): 302 to redirect_uri?code=<authorization_code>&state=<state>.
The authorization code is a short-lived JWT (5-minute TTL) signed with the shared jwtConfigProvider key.
It is stateless β valid across all nodes in a cluster without shared storage.
Step 3: Exchange the code for an access token
http -f POST [RESTHEART-URL]/token \
grant_type=authorization_code \
code="<authorization_code>" \
redirect_uri="https://myapp.example.com/callback" \
client_id=my-app \
code_verifier="${CODE_VERIFIER}"
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 899
}
Configuration
All three OAuth endpoints are disabled by default and must be explicitly enabled. Enable them together for the full Authorization Code + PKCE flow:
oauthAuthorizationServerMetadataService:
enabled: true
# Optional: override scheme+host for metadata URLs (needed behind a TLS-terminating proxy)
# Falls back to the request Host header when null.
base-url: null # e.g. https://api.example.com
authorize-endpoint-uri: /authorize
oauthAuthorizationService:
enabled: true
# URL of the frontend login page (required)
login-url: https://myapp.example.com/login
# Allowed redirect_uri values β supports * wildcard
allowed-redirect-uris:
- https://myapp.example.com/callback
- http://localhost:* # for local development
oauthProtectedResourceMetadataService:
enabled: true
# Optional: same as base-url in oauthAuthorizationServerMetadataService
base-url: null # e.g. https://api.example.com
|
Important
|
The jwtTokenManager must be enabled (it is by default in RESTHeart v9). The authorization code is signed with the same key as regular JWT tokens, ensuring stateless multi-node operation.
|
Authorization Server Metadata (RFC 8414)
|
Note
|
Available from RESTHeart v9.2.0. Disabled by default from v9.2.1. |
oauthAuthorizationServerMetadataService exposes GET /.well-known/oauth-authorization-server per RFC 8414.
OAuth 2.0 clients (API gateways, MCP clients, CLI tools) can use this endpoint to auto-configure without hardcoded URLs.
http GET [RESTHEART-URL]/.well-known/oauth-authorization-server
{
"issuer": "https://api.example.com",
"authorization_endpoint": "https://api.example.com/authorize",
"token_endpoint": "https://api.example.com/token",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "password", "client_credentials"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"]
}
The endpoint is publicly accessible (no authentication required).
When base-url is set, all URLs in the response use it as the base.
When null, the base URL is derived in this order:
-
base-urlconfiguration value -
Request
Hostheader
oauthAuthorizationServerMetadataService:
enabled: true
base-url: https://api.example.com # optional
authorize-endpoint-uri: /authorize # must match oauthAuthorizationService URI
registration-endpoint-uri: /register # optional, enables registration_endpoint in metadata
Protected Resource Metadata (RFC 9728)
|
Note
|
Available from RESTHeart v9.2.0. Disabled by default from v9.2.1. |
oauthProtectedResourceMetadataService exposes GET /.well-known/oauth-protected-resource per RFC 9728.
MCP clients (e.g., Claude Desktop via mcp-remote) use this endpoint to discover the authorization server URL before starting the OAuth flow.
http GET [RESTHEART-URL]/.well-known/oauth-protected-resource
{
"resource": "https://api.example.com",
"authorization_servers": ["https://api.example.com"]
}
A resource-specific path can be appended to scope the metadata:
http GET [RESTHEART-URL]/.well-known/oauth-protected-resource/mcp/ade
{
"resource": "https://api.example.com/mcp/ade",
"authorization_servers": ["https://api.example.com"]
}
The base-url option solves the common reverse-proxy problem where the Host header arrives as http:// while the public URL is https://, causing issuer validation failures per RFC 8414 Β§3.3:
oauthProtectedResourceMetadataService:
enabled: true
base-url: https://api.example.com # optional, resolves TLS proxy mismatch
Dynamic Client Registration (RFC 7591)
|
Note
|
Available from RESTHeart v9.3.0. Disabled by default. |
oauthClientRegistrationService exposes POST /register per RFC 7591, allowing OAuth clients to self-register without manual admin configuration.
This is required by tools such as mcp-inspector and MCP SDK clients that perform dynamic registration before starting the authorization flow.
http POST [RESTHEART-URL]/register \
redirect_uris:='["https://client.example.com/callback"]' \
client_name="My MCP Client" \
token_endpoint_auth_method=none
{
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"client_id_issued_at": 1712345678,
"redirect_uris": ["https://client.example.com/callback"],
"client_name": "My MCP Client",
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code"],
"response_types": ["code"]
}
The endpoint is publicly accessible (no authentication required). The returned client_id is a UUID that clients must use in subsequent authorization requests.
|
Note
|
RESTHeart’s authorization service embeds client_id directly in the authorization code JWT without database validation. No client storage is required on the server side.
|
To enable dynamic client registration and advertise it in the AS metadata:
oauthClientRegistrationService:
enabled: true
oauthAuthorizationServerMetadataService:
enabled: true
registration-endpoint-uri: /register # adds registration_endpoint to discovery metadata
Token Manager Configuration
The JWT Token Manager is enabled by default in RESTHeart v9:
jwtTokenManager:
key: secret # Change this in production!
enabled: true
ttl: 15 # Token time-to-live in minutes
issuer: restheart.org
|
Important
|
Always set a strong, random key value in production environments.
|