From 2e74d7be3de097a35dde12d833098cd17e3e2822 Mon Sep 17 00:00:00 2001 From: Nandan Prabhu Date: Mon, 6 Jul 2026 10:41:30 +0530 Subject: [PATCH 1/4] feat: add support for Online Access (Online Refresh Tokens) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds revokeRefreshToken() to AuthService, following the same Observable pattern as logout() — wraps the underlying Auth0Client call and triggers AuthState.refresh() so isAuthenticated$/user$/idTokenClaims$ reflect the result. This is the one gap in Online Access support — refreshTokenMode, useDpop, and useMrrt already pass through AuthConfig's existing Auth0ClientOptions config surface. Also exports RefreshTokenMode, InvalidConfigurationError, MissingScopesError, and RevokeRefreshTokenOptions from the package root, and documents the feature in EXAMPLES.md. --- EXAMPLES.md | 144 ++++++++++++++++++ .../src/lib/auth.service.spec.ts | 44 ++++++ .../auth0-angular/src/lib/auth.service.ts | 32 ++++ projects/auth0-angular/src/public-api.ts | 4 + 4 files changed, 224 insertions(+) diff --git a/EXAMPLES.md b/EXAMPLES.md index 23ab3357..40edd062 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -820,6 +820,150 @@ bootstrapApplication(AppComponent, { }); ``` +## Online Access (Online Refresh Tokens) + +**Online Refresh Tokens (ORTs)** are a refresh token type bound to the lifetime of the user's Auth0 session, unlike rotating offline refresh tokens. An ORT is: + +- **Session-bound** — valid only while the underlying Auth0 session is active. When the session ends (logout, idle/absolute session expiry, or an admin revoking the session), the ORT stops working. +- **Non-rotating** — refreshing an access token with an ORT does **not** issue a new refresh token; the same ORT is reused for the life of the session. + +Read more about [Online Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens/online-refresh-tokens/online-refresh-tokens) to decide whether this fits your application. + +> [!IMPORTANT] +> Online access requires DPoP. Sender-constraining the token via [DPoP](#device-bound-tokens-with-dpop) is mandatory because the ORT is non-rotating — binding it to the browser's key pair is what mitigates token replay if it is exfiltrated. You must set `useDpop: true` explicitly; the SDK does not enable it for you. +> +> This also requires the `online_refresh_tokens` flag to be enabled for your Auth0 tenant, and `allow_online_access` to be enabled on the resource server you log in with (on by default). + +### Enabling Online Access + +Set `refreshTokenMode` to `RefreshTokenMode.Online` together with `useRefreshTokens: true` and `useDpop: true`: + +```ts +import { NgModule } from '@angular/core'; +import { AuthModule, RefreshTokenMode } from '@auth0/auth0-angular'; + +@NgModule({ + imports: [ + AuthModule.forRoot({ + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', + useRefreshTokens: true, // required — online access is a refresh-token grant + refreshTokenMode: RefreshTokenMode.Online, // 👈 + useDpop: true, // required — DPoP is mandatory for online access + authorizationParams: { + redirect_uri: window.location.origin, + }, + }), + ], +}) +export class AppModule {} +``` + +Or with standalone components via `provideAuth0`: + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideAuth0, RefreshTokenMode } from '@auth0/auth0-angular'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, { + providers: [ + provideAuth0({ + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', + useRefreshTokens: true, + refreshTokenMode: RefreshTokenMode.Online, + useDpop: true, + authorizationParams: { + redirect_uri: window.location.origin, + }, + }), + ], +}); +``` + +`refreshTokenMode` defaults to `RefreshTokenMode.Offline` (rotating refresh tokens). Enabling online mode causes the underlying SDK to: + +- Send the `online_access` scope to the authorization server (instead of `offline_access`) — you do **not** need to add it to `authorizationParams.scope` yourself. +- Route token renewal through the `refresh_token` grant against `/oauth/token` rather than a hidden iframe. +- Store the non-rotating ORT in the existing cache and reuse it on every refresh, never replacing it. + +### Configuration validation + +If `refreshTokenMode: RefreshTokenMode.Online` is set without `useRefreshTokens: true` and `useDpop: true`, the underlying `Auth0Client` constructor throws an `InvalidConfigurationError`: + +```ts +import { InvalidConfigurationError } from '@auth0/auth0-angular'; + +try { + // AuthModule.forRoot / provideAuth0 constructs the client during app bootstrap. + // Wrap it, or validate your configuration up front, to catch misconfiguration. +} catch (e) { + if (e instanceof InvalidConfigurationError) { + console.error(e.error_description); // includes the suggested fix + } +} +``` + +### Revoking the Online Refresh Token + +Call `revokeRefreshToken()` on `AuthService` to explicitly revoke the refresh token via the `/oauth/revoke` endpoint: + +```ts +import { Component } from '@angular/core'; +import { AuthService } from '@auth0/auth0-angular'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', +}) +export class AppComponent { + constructor(public auth: AuthService) {} + + revoke() { + this.auth.revokeRefreshToken().subscribe(); + } + + revokeForAudience() { + this.auth + .revokeRefreshToken({ audience: 'https://api.example.com' }) + .subscribe(); + } +} +``` + +> [!WARNING] +> In online mode, `revokeRefreshToken()` behaves differently from offline mode: +> - The ORT **is** revoked at the authorization server, and because it is session-bound, the Auth0 **session is terminated server-side** as part of revocation. +> - The entire local cache is cleared immediately — `isAuthenticated$` emits `false`, and `user$`/`idTokenClaims$` emit `null` right away, without waiting for the access token to expire. +> +> In **offline mode**, only the refresh token is invalidated — the cached access token and user profile remain valid until the access token expires. +> +> After calling `revokeRefreshToken()` in online mode, redirect the user to login. For a redirect-based sign-out in either mode, use `logout()` instead. + +### Using Online Access with MRRT + +Online access is compatible with Multi-Resource Refresh Tokens (MRRT): a single ORT can be exchanged for access tokens across the audiences allowed by your refresh-token policies. The ORT remains non-rotating throughout. + +```ts +AuthModule.forRoot({ + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', + useRefreshTokens: true, + refreshTokenMode: RefreshTokenMode.Online, + useDpop: true, + useMrrt: true, // 👈 + authorizationParams: { + redirect_uri: window.location.origin, + audience: 'https://api.example.com', + }, +}); +``` + +> [!IMPORTANT] +> In order for MRRT to work, it needs a previous configuration setting the refresh token policies. +> Visit [configure and implement MRRT](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token/configure-and-implement-multi-resource-refresh-token). + ## Standalone components and a more functional approach As of Angular 15, the Angular team is putting standalone components, as well as a more functional approach, in favor of the traditional use of NgModules and class-based approach. diff --git a/projects/auth0-angular/src/lib/auth.service.spec.ts b/projects/auth0-angular/src/lib/auth.service.spec.ts index 8c3335dd..b11f2541 100644 --- a/projects/auth0-angular/src/lib/auth.service.spec.ts +++ b/projects/auth0-angular/src/lib/auth.service.spec.ts @@ -75,6 +75,7 @@ describe('AuthService', () => { jest.spyOn(auth0Client, 'getUser').mockResolvedValue(undefined); jest.spyOn(auth0Client, 'getIdTokenClaims').mockResolvedValue(undefined); jest.spyOn(auth0Client, 'logout'); + jest.spyOn(auth0Client, 'revokeRefreshToken').mockResolvedValue(); jest .spyOn(auth0Client, 'getTokenSilently') .mockResolvedValue('__access_token__'); @@ -865,6 +866,49 @@ describe('AuthService', () => { }); }); + it('should call `revokeRefreshToken`', () => { + const service = createService(); + service.revokeRefreshToken().subscribe(); + expect(auth0Client.revokeRefreshToken).toHaveBeenCalledWith(undefined); + }); + + it('should call `revokeRefreshToken` with options', () => { + const options = { audience: 'https://api.example.com' }; + const service = createService(); + service.revokeRefreshToken(options).subscribe(); + expect(auth0Client.revokeRefreshToken).toHaveBeenCalledWith(options); + }); + + it('should refresh the authentication state after revokeRefreshToken', (done) => { + // In online mode, revoking terminates the session server-side; in offline + // mode the cached session is untouched. Either way the SDK must re-derive + // isAuthenticated$/user$/idTokenClaims$ from auth0Client after the call — + // asserted here via the same AuthState.refresh() trigger `logout` uses. + jest.spyOn(authState, 'refresh'); + + const service = createService(); + + service.revokeRefreshToken().subscribe(() => { + expect(authState.refresh).toHaveBeenCalled(); + done(); + }); + }); + + it('should propagate errors from `revokeRefreshToken`', (done) => { + ( + auth0Client.revokeRefreshToken as unknown as jest.SpyInstance + ).mockRejectedValue(new Error('The token has been revoked')); + + const service = createService(); + + service.revokeRefreshToken().subscribe({ + error: (e) => { + expect(e.message).toBe('The token has been revoked'); + done(); + }, + }); + }); + describe('getAccessTokenSilently', () => { it('should call the underlying SDK', (done) => { const service = createService(); diff --git a/projects/auth0-angular/src/lib/auth.service.ts b/projects/auth0-angular/src/lib/auth.service.ts index f930962d..9ed9993c 100644 --- a/projects/auth0-angular/src/lib/auth.service.ts +++ b/projects/auth0-angular/src/lib/auth.service.ts @@ -18,6 +18,7 @@ import { ResponseType, } from '@auth0/auth0-spa-js'; import type { + RevokeRefreshTokenOptions, EnrollParams, ChallengeAuthenticatorParams, VerifyParams, @@ -245,6 +246,37 @@ export class AuthService ); } + /** + * ```js + * revokeRefreshToken().subscribe(); + * ``` + * + * Revokes the refresh token via the `/oauth/revoke` endpoint. This invalidates the + * refresh token so it can no longer be used to obtain new access tokens. + * + * If `useRefreshTokens` is disabled, this method does nothing. + * + * **Online mode** (`refreshTokenMode: RefreshTokenMode.Online`): revoking the Online + * Refresh Token also terminates the Auth0 session server-side and clears the entire + * local cache. `isAuthenticated$`, `user$`, and `idTokenClaims$` update immediately + * to reflect the terminated session — no redirect required. Use `logout()` instead + * if you want a redirect-based sign-out. + * + * **Offline mode**: only the refresh token is invalidated; the cached access token + * and user profile remain valid until the access token expires. `isAuthenticated$` + * and `user$` are unaffected until then. + * + * @param options The options to identify which refresh token to revoke. + * Defaults to the audience configured in `authorizationParams`. + */ + revokeRefreshToken(options?: RevokeRefreshTokenOptions): Observable { + return from( + this.auth0Client.revokeRefreshToken(options).then(() => { + this.authState.refresh(); + }) + ); + } + /** * Fetches a new access token and returns the response from the /oauth/token endpoint, omitting the refresh token. * diff --git a/projects/auth0-angular/src/public-api.ts b/projects/auth0-angular/src/public-api.ts index c90fec6c..69c24c13 100644 --- a/projects/auth0-angular/src/public-api.ts +++ b/projects/auth0-angular/src/public-api.ts @@ -36,6 +36,8 @@ export { AuthenticationError, PopupCancelledError, MissingRefreshTokenError, + MissingScopesError, + InvalidConfigurationError, ConnectError, Fetcher, FetcherConfig, @@ -44,6 +46,7 @@ export { CustomTokenExchangeOptions, TokenEndpointResponse, ResponseType, + RefreshTokenMode, MfaError, MfaListAuthenticatorsError, MfaEnrollmentError, @@ -61,6 +64,7 @@ export { export type { InteractiveErrorHandler, + RevokeRefreshTokenOptions, Authenticator, AuthenticatorType, OobChannel, From 9c68957e553ec651a8a09212d13d48dbc97c56f2 Mon Sep 17 00:00:00 2001 From: Nandan Prabhu Date: Mon, 6 Jul 2026 11:18:33 +0530 Subject: [PATCH 2/4] docs: mark Online Access as Early Access Online Access (Online Refresh Tokens) is gated behind Auth0 account team enablement. Adds the same Early Access notice used for the MFA section. --- EXAMPLES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/EXAMPLES.md b/EXAMPLES.md index 40edd062..36e8c4a5 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -822,6 +822,9 @@ bootstrapApplication(AppComponent, { ## Online Access (Online Refresh Tokens) +> [!NOTE] +> Online Access (Online Refresh Tokens) support via SDKs is currently in Early Access. To request access to this feature, contact your Auth0 representative. + **Online Refresh Tokens (ORTs)** are a refresh token type bound to the lifetime of the user's Auth0 session, unlike rotating offline refresh tokens. An ORT is: - **Session-bound** — valid only while the underlying Auth0 session is active. When the session ends (logout, idle/absolute session expiry, or an admin revoking the session), the ORT stops working. From 4eab048d900b3df845161b6499e97641944fd837 Mon Sep 17 00:00:00 2001 From: Nandan Prabhu Date: Mon, 6 Jul 2026 12:29:25 +0530 Subject: [PATCH 3/4] build: bump @auth0/auth0-spa-js to ^2.23.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RefreshTokenMode is not exported until 2.23.0 — the previous ^2.21.0 range let installs resolve to an older minor where RefreshTokenMode is undefined, breaking refreshTokenMode: RefreshTokenMode.Online at runtime. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7271eda..357a6d2d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@angular/platform-browser": "^19.2.21", "@angular/platform-browser-dynamic": "^19.2.21", "@angular/router": "^19.2.21", - "@auth0/auth0-spa-js": "^2.21.0", + "@auth0/auth0-spa-js": "^2.23.0", "rxjs": "^6.6.7", "tslib": "^2.8.1", "zone.js": "~0.15.1" From f48600a39646013b86e0fac95fe189d6515b6ce2 Mon Sep 17 00:00:00 2001 From: Nandan Prabhu Date: Thu, 16 Jul 2026 12:17:35 +0530 Subject: [PATCH 4/4] fix: remove revokeRefreshToken from Online Access support Drops the revokeRefreshToken() method, its RevokeRefreshTokenOptions export, tests, and EXAMPLES.md section added in 2b81160. Online Access (refreshTokenMode, useDpop, useMrrt) and the other new exports (RefreshTokenMode, InvalidConfigurationError, MissingScopesError) are unaffected. --- EXAMPLES.md | 36 --------------- .../src/lib/auth.service.spec.ts | 44 ------------------- .../auth0-angular/src/lib/auth.service.ts | 32 -------------- projects/auth0-angular/src/public-api.ts | 1 - 4 files changed, 113 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 36e8c4a5..02c1c763 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -908,42 +908,6 @@ try { } ``` -### Revoking the Online Refresh Token - -Call `revokeRefreshToken()` on `AuthService` to explicitly revoke the refresh token via the `/oauth/revoke` endpoint: - -```ts -import { Component } from '@angular/core'; -import { AuthService } from '@auth0/auth0-angular'; - -@Component({ - selector: 'app-root', - templateUrl: './app.component.html', -}) -export class AppComponent { - constructor(public auth: AuthService) {} - - revoke() { - this.auth.revokeRefreshToken().subscribe(); - } - - revokeForAudience() { - this.auth - .revokeRefreshToken({ audience: 'https://api.example.com' }) - .subscribe(); - } -} -``` - -> [!WARNING] -> In online mode, `revokeRefreshToken()` behaves differently from offline mode: -> - The ORT **is** revoked at the authorization server, and because it is session-bound, the Auth0 **session is terminated server-side** as part of revocation. -> - The entire local cache is cleared immediately — `isAuthenticated$` emits `false`, and `user$`/`idTokenClaims$` emit `null` right away, without waiting for the access token to expire. -> -> In **offline mode**, only the refresh token is invalidated — the cached access token and user profile remain valid until the access token expires. -> -> After calling `revokeRefreshToken()` in online mode, redirect the user to login. For a redirect-based sign-out in either mode, use `logout()` instead. - ### Using Online Access with MRRT Online access is compatible with Multi-Resource Refresh Tokens (MRRT): a single ORT can be exchanged for access tokens across the audiences allowed by your refresh-token policies. The ORT remains non-rotating throughout. diff --git a/projects/auth0-angular/src/lib/auth.service.spec.ts b/projects/auth0-angular/src/lib/auth.service.spec.ts index b11f2541..8c3335dd 100644 --- a/projects/auth0-angular/src/lib/auth.service.spec.ts +++ b/projects/auth0-angular/src/lib/auth.service.spec.ts @@ -75,7 +75,6 @@ describe('AuthService', () => { jest.spyOn(auth0Client, 'getUser').mockResolvedValue(undefined); jest.spyOn(auth0Client, 'getIdTokenClaims').mockResolvedValue(undefined); jest.spyOn(auth0Client, 'logout'); - jest.spyOn(auth0Client, 'revokeRefreshToken').mockResolvedValue(); jest .spyOn(auth0Client, 'getTokenSilently') .mockResolvedValue('__access_token__'); @@ -866,49 +865,6 @@ describe('AuthService', () => { }); }); - it('should call `revokeRefreshToken`', () => { - const service = createService(); - service.revokeRefreshToken().subscribe(); - expect(auth0Client.revokeRefreshToken).toHaveBeenCalledWith(undefined); - }); - - it('should call `revokeRefreshToken` with options', () => { - const options = { audience: 'https://api.example.com' }; - const service = createService(); - service.revokeRefreshToken(options).subscribe(); - expect(auth0Client.revokeRefreshToken).toHaveBeenCalledWith(options); - }); - - it('should refresh the authentication state after revokeRefreshToken', (done) => { - // In online mode, revoking terminates the session server-side; in offline - // mode the cached session is untouched. Either way the SDK must re-derive - // isAuthenticated$/user$/idTokenClaims$ from auth0Client after the call — - // asserted here via the same AuthState.refresh() trigger `logout` uses. - jest.spyOn(authState, 'refresh'); - - const service = createService(); - - service.revokeRefreshToken().subscribe(() => { - expect(authState.refresh).toHaveBeenCalled(); - done(); - }); - }); - - it('should propagate errors from `revokeRefreshToken`', (done) => { - ( - auth0Client.revokeRefreshToken as unknown as jest.SpyInstance - ).mockRejectedValue(new Error('The token has been revoked')); - - const service = createService(); - - service.revokeRefreshToken().subscribe({ - error: (e) => { - expect(e.message).toBe('The token has been revoked'); - done(); - }, - }); - }); - describe('getAccessTokenSilently', () => { it('should call the underlying SDK', (done) => { const service = createService(); diff --git a/projects/auth0-angular/src/lib/auth.service.ts b/projects/auth0-angular/src/lib/auth.service.ts index 9ed9993c..f930962d 100644 --- a/projects/auth0-angular/src/lib/auth.service.ts +++ b/projects/auth0-angular/src/lib/auth.service.ts @@ -18,7 +18,6 @@ import { ResponseType, } from '@auth0/auth0-spa-js'; import type { - RevokeRefreshTokenOptions, EnrollParams, ChallengeAuthenticatorParams, VerifyParams, @@ -246,37 +245,6 @@ export class AuthService ); } - /** - * ```js - * revokeRefreshToken().subscribe(); - * ``` - * - * Revokes the refresh token via the `/oauth/revoke` endpoint. This invalidates the - * refresh token so it can no longer be used to obtain new access tokens. - * - * If `useRefreshTokens` is disabled, this method does nothing. - * - * **Online mode** (`refreshTokenMode: RefreshTokenMode.Online`): revoking the Online - * Refresh Token also terminates the Auth0 session server-side and clears the entire - * local cache. `isAuthenticated$`, `user$`, and `idTokenClaims$` update immediately - * to reflect the terminated session — no redirect required. Use `logout()` instead - * if you want a redirect-based sign-out. - * - * **Offline mode**: only the refresh token is invalidated; the cached access token - * and user profile remain valid until the access token expires. `isAuthenticated$` - * and `user$` are unaffected until then. - * - * @param options The options to identify which refresh token to revoke. - * Defaults to the audience configured in `authorizationParams`. - */ - revokeRefreshToken(options?: RevokeRefreshTokenOptions): Observable { - return from( - this.auth0Client.revokeRefreshToken(options).then(() => { - this.authState.refresh(); - }) - ); - } - /** * Fetches a new access token and returns the response from the /oauth/token endpoint, omitting the refresh token. * diff --git a/projects/auth0-angular/src/public-api.ts b/projects/auth0-angular/src/public-api.ts index 69c24c13..051954f8 100644 --- a/projects/auth0-angular/src/public-api.ts +++ b/projects/auth0-angular/src/public-api.ts @@ -64,7 +64,6 @@ export { export type { InteractiveErrorHandler, - RevokeRefreshTokenOptions, Authenticator, AuthenticatorType, OobChannel,