diff --git a/CHANGELOG.md b/CHANGELOG.md index e92911c..4265ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Change Log +## [2.0.0-beta.1](https://github.com/auth0/auth0-java-mvc-common/tree/2.0.0-beta.1) (2026-07-15) +[Full Changelog](https://github.com/auth0/auth0-java-mvc-common/compare/2.0.0-beta.0...2.0.0-beta.1) + +**Added** +- Refresh Token Grant (MRRT): `AuthenticationController.renewAuth(...)` exchanges a refresh token for a new set of tokens, supporting Multi-Resource Refresh Token flows via `withAudience()`/`withScope()` [\#242](https://github.com/auth0/auth0-java-mvc-common/pull/242) ([tanya732](https://github.com/tanya732)) +- Custom Token Exchange (CTE): `AuthenticationController.customTokenExchange(...)` and `loginWithCustomTokenExchange(...)` exchange an external `subject_token` for Auth0 tokens (RFC 8693) [\#253](https://github.com/auth0/auth0-java-mvc-common/pull/253) ([tanya732](https://github.com/tanya732)) +- Client-Initiated Backchannel Authentication (CIBA): `AuthenticationController.backChannelAuthorize(...)` and `backChannelPoll(...)` support the decoupled, app-owned polling flow, with `BackChannelAuthorizationException` typed helpers (`isAuthorizationPending()`, `isSlowDown()`, `isExpiredToken()`, `isAccessDenied()`) [\#254](https://github.com/auth0/auth0-java-mvc-common/pull/254) ([tanya732](https://github.com/tanya732)) + +> All three flows are stateless and support Multiple Custom Domains (MCD) via domain-qualified overloads. Merged via [\#255](https://github.com/auth0/auth0-java-mvc-common/pull/255). + +--- + ## [2.0.0-beta.0](https://github.com/auth0/auth0-java-mvc-common/tree/2.0.0-beta.0) (2026-05-29) This is the first beta release of the v2 major version. See the [Migration Guide](MIGRATION_GUIDE.md) for full upgrade instructions. diff --git a/EXAMPLES.md b/EXAMPLES.md index 6b56503..c2645a8 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -3,6 +3,9 @@ - [Including additional authorization parameters](#including-additional-authorization-parameters) - [Organizations](#organizations) - [Multiple Custom Domains (MCD) Support](#multiple-custom-domains-support) +- [Refresh Token Grant (MRRT)](#refresh-token-grant-mrrt) +- [Custom Token Exchange (CTE)](#custom-token-exchange-cte) +- [Client-Initiated Backchannel Authentication (CIBA)](#client-initiated-backchannel-authentication-ciba) - [Allowing clock skew for token validation](#allow-a-clock-skew-for-token-validation) - [Changing the OAuth response_type](#changing-the-oauth-response_type) - [HTTP logging](#http-logging) @@ -200,6 +203,156 @@ When using MCD, your application must be deployed behind a secure edge or revers Without a trusted proxy layer to validate these headers, an attacker can manipulate the domain resolution process. This can result in malicious redirects, where users are sent to unauthorized or fraudulent endpoints during the login and logout flows. +## Refresh Token Grant (MRRT) + +Exchange a refresh token for a fresh set of tokens using Auth0's [refresh token grant](https://auth0.com/docs/secure/tokens/refresh-tokens). This supports [Multi-Resource Refresh Token (MRRT)](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token) flows, where a single refresh token can obtain access tokens for multiple APIs by varying the requested `audience` and `scope`. + +The library remains stateless: your application owns storage of the refresh token, caching of the resulting access tokens, and any concurrency control around refresh-token rotation. + +```java +Tokens tokens = controller.renewAuth("YOUR-REFRESH-TOKEN") + .withAudience("https://my-api.example.com") + .withScope("openid profile read:messages") + .execute(); + +String accessToken = tokens.getAccessToken(); +// When refresh-token rotation is enabled, a new refresh token is returned and supersedes the +// one used here — persist it. +String rotatedRefreshToken = tokens.getRefreshToken(); +``` + +> **Note:** If the requested `audience` is not permitted by the application's MRRT policy, Auth0 does not error — it returns a token for the default audience instead. Always verify the `aud` claim of the returned access token. + +The refresh-token grant does not return an ID token, so `tokens.getIdToken()` is typically `null`. + +### Using MRRT with Multiple Custom Domains + +When using a `DomainResolver`, pass the domain explicitly so the grant targets the correct tenant. This is required because a refresh can occur outside of an HTTP request: + +```java +Tokens tokens = controller.renewAuth("YOUR-REFRESH-TOKEN", "acme.auth0.com") + .withAudience("https://my-api.example.com") + .execute(); +``` + +Alternatively, pass the `HttpServletRequest` to let the resolver derive the domain: + +```java +Tokens tokens = controller.renewAuth("YOUR-REFRESH-TOKEN", request).execute(); +``` + +## Custom Token Exchange (CTE) + +[Custom Token Exchange](https://auth0.com/docs/authenticate/custom-token-exchange) (RFC 8693) exchanges an external `subject_token` for a set of Auth0 tokens. There are two variants: + +- **`loginWithCustomTokenExchange(...)`** — applies login semantics and **always verifies** the returned ID token. Use this to establish an authenticated session. +- **`customTokenExchange(...)`** — a utility exchange that returns the tokens without verification, suitable for obtaining tokens for a downstream API. + +```java +// Login semantics: the returned ID token is verified. +Tokens tokens = controller.loginWithCustomTokenExchange("EXTERNAL-SUBJECT-TOKEN", "urn:acme:legacy-token") + .withAudience("https://my-api.example.com") + .withScope("openid profile") + .execute(); + +// Utility exchange: tokens are returned without ID token verification. +Tokens apiTokens = controller.customTokenExchange("EXTERNAL-SUBJECT-TOKEN", "urn:acme:legacy-token") + .withAudience("https://my-api.example.com") + .execute(); +``` + +The `subjectTokenType` is a customer-defined URI describing the external token. Configure a matching [Custom Token Exchange profile](https://auth0.com/docs/authenticate/custom-token-exchange) in the Auth0 Dashboard. + +### Organizations with Custom Token Exchange + +When an organization is configured via `withOrganization(...)`, the library validates the `org_id`/`org_name` claim. Because that claim lives in the ID token, the ID token is fully verified on either variant whenever an organization is in play: + +```java +Tokens tokens = controller.loginWithCustomTokenExchange("EXTERNAL-SUBJECT-TOKEN", "urn:acme:legacy-token") + .withOrganization("org_123") + .execute(); +``` + +### Using CTE with Multiple Custom Domains + +When using a `DomainResolver`, pass the domain explicitly. A token exchange can occur outside of an HTTP request, where the domain cannot otherwise be resolved: + +```java +Tokens tokens = controller.loginWithCustomTokenExchange("EXTERNAL-SUBJECT-TOKEN", "urn:acme:legacy-token", "acme.auth0.com") + .execute(); +``` + +## Client-Initiated Backchannel Authentication (CIBA) + +[CIBA](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow) is a decoupled flow: the application initiates authentication and the user approves it out-of-band on a separate device (e.g., a push notification to their phone). It is a **two-step** flow: + +1. **Initiate** (`backChannelAuthorize`) — ask Auth0 to authenticate the user; receive an `auth_req_id`, a polling `interval`, and an `expires_in`. +2. **Poll** (`backChannelPoll`) — poll the token endpoint until the user approves (yielding verified `Tokens`) or a terminal error occurs. + +The library is deliberately stateless and **the application owns the polling loop** — this keeps it safe for horizontally-scaled deployments. Honor the `interval` returned by the initiate step, and stop on terminal errors. + +### Step 1: Initiate + +The `login_hint` identifies the user. Auth0 expects the `iss_sub` shape: + +```java +Map loginHint = new HashMap<>(); +loginHint.put("format", "iss_sub"); +loginHint.put("iss", "https://YOUR-AUTH0-DOMAIN/"); +loginHint.put("sub", "auth0|abc123"); + +BackChannelAuthorizeResponse authorize = controller + .backChannelAuthorize("openid profile", "Approve login request 1234", loginHint) + .withAudience("https://my-api.example.com") // optional + .withRequestedExpiry(300) // optional, seconds + .execute(); + +String authReqId = authorize.getAuthReqId(); +Integer interval = authorize.getInterval(); // seconds between polls +Long expiresIn = authorize.getExpiresIn(); // seconds until auth_req_id expires +``` + +### Step 2: Poll + +Poll no more frequently than `interval`. Use the typed helpers on `BackChannelAuthorizationException` to drive the loop — `authorization_pending` and `slow_down` are non-terminal (keep polling), while `expired_token` and `access_denied` are terminal (stop): + +```java +try { + Tokens tokens = controller.backChannelPoll(authReqId).execute(); + // Success — the ID token has been verified. + String idToken = tokens.getIdToken(); +} catch (BackChannelAuthorizationException e) { + if (e.isAuthorizationPending()) { + // User has not approved yet — wait `interval` seconds and poll again. + } else if (e.isSlowDown()) { + // Polling too fast — increase the interval (commonly by 5 seconds) and poll again. + } else if (e.isExpiredToken()) { + // Terminal — the auth_req_id expired. Start over with backChannelAuthorize. + } else if (e.isAccessDenied()) { + // Terminal — the user rejected the request. + } +} catch (IdentityVerificationException e) { + // The returned ID token failed verification. +} +``` + +> **Note:** CIBA returns no refresh token, and the token type is reported as `Bearer`. `tokens.getRefreshToken()` is `null`. + +### Using CIBA with Multiple Custom Domains + +The `auth_req_id` is bound to the domain it was issued for, so the poll **must** target that same domain. When using a `DomainResolver`, pass the domain explicitly on both steps — polling commonly happens outside the initiating HTTP request, so store the domain alongside the `auth_req_id`: + +```java +BackChannelAuthorizeResponse authorize = controller + .backChannelAuthorize("openid profile", "Approve login", loginHint, "acme.auth0.com") + .execute(); + +// ...later, on the poll timer: +Tokens tokens = controller.backChannelPoll(authReqId, "acme.auth0.com").execute(); +``` + +The no-domain overloads (`backChannelAuthorize(...)` / `backChannelPoll(...)`) are only valid when the controller is configured with a fixed domain; calling them on a `DomainResolver`-backed controller throws `IllegalStateException`. + ## Allow a clock skew for token validation During the authentication flow, the ID token is verified and validated to ensure it is secure. Time-based claims such as the time the token was issued at and the token's expiration are verified to ensure the token is valid. diff --git a/README.md b/README.md index d746375..c720703 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,14 @@ AuthenticationController controller = AuthenticationController The library handles storing and retrieving the resolved domain throughout the authentication flow. The returned `Tokens` object includes `getDomain()` and `getIssuer()` for tenant-specific session management. See [EXAMPLES.md](./EXAMPLES.md#multiple-custom-domains-support) for more details. +### Additional authentication flows + +Beyond the interactive login flow above, the `AuthenticationController` supports: + +- **Refresh Token Grant (MRRT)** — exchange a refresh token for fresh tokens, including Multi-Resource Refresh Token flows that target multiple APIs. See [EXAMPLES.md](./EXAMPLES.md#refresh-token-grant-mrrt). +- **Custom Token Exchange (CTE)** — exchange an external `subject_token` for Auth0 tokens (RFC 8693). See [EXAMPLES.md](./EXAMPLES.md#custom-token-exchange-cte). +- **Client-Initiated Backchannel Authentication (CIBA)** — a decoupled flow where the user approves authentication out-of-band on a separate device. See [EXAMPLES.md](./EXAMPLES.md#client-initiated-backchannel-authentication-ciba). + ## API Reference - [JavaDocs](https://javadoc.io/doc/com.auth0/mvc-auth-commons)