Skip to content

Commit 35ec131

Browse files
authored
fix: rename nonRotating to preserveRefreshToken and fix revokeRefreshToken in online mode (#1644)
## Summary Four fixes for online mode (`refreshTokenMode: 'online'`) and related ORT handling: 1. **Local cache not cleared after `revokeRefreshToken()` in online mode** — after a successful revoke, the access token, ID token, and user profile remained in the local cache. `isAuthenticated()` returned `true` and `getUser()` returned the stale profile until the access token expired. Fixed by calling `_clearLocalSession()` after a successful revoke in online mode, so client state is consistent with server state immediately. 2. **ORT not preserved after MFA step-up** — in online mode the server never returns a new refresh token (ORTs are non-rotating). The previous carry-forward logic only covered the `refresh_token` grant type explicitly, so the ORT was silently evicted after an MFA step-up (which uses a different grant type). Refactored to mutate `authResult.refresh_token` directly using `options.refresh_token ?? cacheManager.get(...)?.refresh_token` so the ORT is preserved for any grant type when no new RT is returned by the server. 3. **Incorrect JSDoc / docs** — the previous documentation incorrectly stated that `revokeRefreshToken()` in online mode only clears the local cache and does _not_ revoke the ORT at the server. In reality the `/oauth/revoke` endpoint accepts ORTs, and because an ORT is session-bound, revoking it also terminates the Auth0 session server-side. All documentation updated to reflect actual behavior. 4. **`nonRotating` renamed to `preserveRefreshToken`** — the old name created a confusing double-negative at the use site (`!nonRotating`) in security-critical token-handling code inside the web worker. Renamed across `worker.types.ts`, `token.worker.ts`, `http.ts`, and `api.ts`. No behavior change. Additionally: exported `MissingScopesError` and `MfaRequirements` from `src/index.ts` (were already defined internally but not re-exported). ## Details ### Bug: stale client state after `revokeRefreshToken()` in online mode `revokeRefreshToken()` called `stripRefreshToken()` on success, which removes only the `refresh_token` field from each cache entry — leaving the access token, ID token, and user profile intact. After the call: - Server: ORT revoked, Auth0 session terminated - Client: `isAuthenticated()` → `true`, `getUser()` → stale user object, `getTokenSilently()` → returns the still-cached (but server-invalidated) access token until it expires Fix: call `_clearLocalSession()` in online mode after `revokeToken` resolves. This wipes the full cache (access token, ID token, user profile), cookies, and the worker's in-memory RT store — matching what `logout()` does for the local side. `isAuthenticated()` returns `false` immediately. ### Bug: ORT evicted after MFA step-up in online mode When `mfa_required` is thrown during `getTokenSilently()` and the user completes MFA, the SDK calls `_getTokenUsingMfaCode()` with the `http://auth0.com/oauth/grant-type/mfa-otp` grant type. The previous carry-forward logic: ```typescript const refresh_token = authResult.refresh_token || (this.onlineAccess && options.grant_type === 'refresh_token' ? options.refresh_token : undefined); ``` only preserved the ORT when `grant_type === 'refresh_token'`. For MFA grants, `options.grant_type` is `'http://auth0.com/oauth/grant-type/mfa-otp'`, so the condition was `false` and `refresh_token` was `undefined` — causing the ORT to be evicted from the cache. Refactored to mutate `authResult` directly: ```typescript // Mutate authResult so ...authResult in _saveEntryInCache picks up the ORT directly. // Safe: GetTokenSilentlyVerboseResponse omits refresh_token, so it never reaches app code. if (!authResult.refresh_token && this.onlineAccess) { authResult.refresh_token = (options as RefreshTokenRequestTokenOptions).refresh_token ?? (await this.cacheManager.get( new CacheKey({ scope: scopesToRequest || options.scope, audience: options.audience || DEFAULT_AUDIENCE, clientId: this.options.clientId, }) ))?.refresh_token; } ``` This preserves the ORT for any grant type. If `options.refresh_token` is set (refresh_token grant), it uses that directly without a cache lookup. For MFA grants where `options.refresh_token` is absent, it falls back to the cache to recover the stored ORT. The mutation is safe because `GetTokenSilentlyVerboseResponse` explicitly `Omit`s `refresh_token`, so the ORT never reaches app code. ### `revokeRefreshToken()` vs `logout()` in online mode Both terminate the session. The difference is mechanics: | | `revokeRefreshToken()` | `logout()` | |---|---|---| | Mechanism | POST to `/oauth/revoke` | Redirect to `/v2/logout` | | Session terminated server-side | Yes | Yes | | Local cache cleared | Yes (after this fix) | Yes | | Redirect | No | Yes | | Use when | Background / silent sign-out | Redirect to post-logout page | After calling `revokeRefreshToken()` in online mode, the user must log in again — `getTokenSilently()` will fail because the session is gone. ### `preserveRefreshToken` rename Threaded through four files with no behavior change: - `src/worker/worker.types.ts` — type definition - `src/worker/token.worker.ts` — destructuring + use site - `src/http.ts` — `fetchWithWorker`, `switchFetch`, `getJSON` (×2 for DPoP retry) - `src/api.ts` — `oauthToken` destructuring + pass-through ## Test plan - [x] `revokeRefreshToken()` in online mode (localStorage path): after the call, `isAuthenticated()` returns `false` and `getUser()` returns `undefined` - [x] `revokeRefreshToken()` in online mode (worker/memory path): `isAuthenticated()` returns `false` and `getUser()` returns `undefined` immediately - [x] `revokeRefreshToken()` in online mode: `getTokenSilently()` fails (session gone) — not the cached token - [x] `revokeRefreshToken()` failure: cache NOT cleared, user remains authenticated - [x] `revokeRefreshToken()` in offline mode: access token remains cached; `isAuthenticated()` still returns `true` until the AT expires - [x] ORT is preserved (not deleted) from the worker store after a successful token refresh in online mode - [x] ORT is preserved after MFA step-up (mfa-otp, mfa-oob, mfa-recovery-code grants) in online mode - [x] Server-returned ORT on MFA grant is used in preference to the cached one - [x] When `options.refresh_token` is set (refresh_token grant), cache is not hit for the carry-forward (verified via spy) - [x] ORT NOT carried forward on MFA grants in offline mode (single-use RT must not be reused) - [x] Existing worker tests pass with the `preserveRefreshToken` rename
1 parent 8e464f4 commit 35ec131

10 files changed

Lines changed: 508 additions & 46 deletions

File tree

EXAMPLES.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,17 +154,28 @@ The `revokeRefreshToken()` method explicitly revokes a refresh token via the `/o
154154

155155
This method only has an effect when `useRefreshTokens` is `true`. If refresh tokens are disabled it returns immediately without doing anything.
156156

157-
> [!NOTE]
158-
> In [online access](#online-access-online-refresh-tokens) mode (`refreshTokenMode: RefreshTokenMode.Online`), `revokeRefreshToken()` only clears the cached refresh token locally — it does **not** revoke the Online Refresh Token at the authorization server. Online Refresh Tokens are non-rotating and session-bound, and the server does not support token-only revocation for them. To end an online session, call `logout()` instead, which terminates the session and thereby invalidates the ORT.
157+
> [!WARNING]
158+
> In [online access](#online-access-online-refresh-tokens) mode (`refreshTokenMode: RefreshTokenMode.Online`), `revokeRefreshToken()` behaves differently from offline mode:
159+
> - The ORT **is** revoked at the authorization server via `/oauth/revoke`.
160+
> - Because the ORT is session-bound, the Auth0 **session is terminated server-side** as part of revocation.
161+
> - The entire local cache is cleared immediately — the access token, ID token, and user profile are wiped. `isAuthenticated()` returns `false` and `getUser()` returns `undefined` right away.
162+
>
163+
> After calling `revokeRefreshToken()` in online mode, redirect the user to login — `getTokenSilently()` will fail because the session is gone. For a redirect-based sign-out, `logout()` achieves the same result.
159164
160165
```js
161166
// Revoke the refresh token for the default audience
162167
await auth0.revokeRefreshToken();
163168
```
164169

165-
**How it affects the cache:** The access token is preserved in the cache — only the refresh token entry is cleared. Once the access token expires, `getTokenSilently()` will attempt silent auth (via iframe, if `useRefreshTokensFallback` is enabled and the Auth0 session is still active) before requiring a new interactive login.
170+
**How it affects the cache:**
171+
- **Offline mode:** only the refresh token entry is cleared — the access token remains in cache until it expires. Once it expires, `getTokenSilently()` will attempt silent auth (via iframe, if `useRefreshTokensFallback` is enabled and the Auth0 session is still active) before requiring a new interactive login.
172+
- **Online mode:** the entire local cache is cleared (access token, ID token, user profile). `isAuthenticated()` returns `false` immediately. The user must log in again.
166173

167-
**Difference from `logout()`:** `revokeRefreshToken()` invalidates the refresh token on the Auth0 server and removes it from the local cache, but it does **not** clear the user's Auth0 session or the rest of the local cache. If you want to fully terminate the session, use `logout()` instead.
174+
**Difference from `logout()`:**
175+
- In **offline mode**, `revokeRefreshToken()` invalidates the rotating refresh token at the server and strips it from the cache, but does **not** terminate the Auth0 session or clear the rest of the local cache (access token, ID token, user profile remain until they expire).
176+
- In **online mode**, `revokeRefreshToken()` terminates the Auth0 session server-side and clears the entire local cache immediately — equivalent to a silent `logout()` without a redirect.
177+
178+
In both modes, if you want a **redirect-based** sign-out, use `logout()` instead.
168179

169180
#### Error Handling
170181

@@ -320,8 +331,8 @@ await auth0.logout({ logoutParams: { returnTo: window.location.origin } });
320331

321332
After logout, the ORT is no longer valid; a subsequent `getTokenSilently()` falls through to the [iframe fallback](#refresh-token-fallback) (if `useRefreshTokensFallback` is enabled) and ultimately to an interactive login.
322333

323-
> [!NOTE]
324-
> In online mode, [`revokeRefreshToken()`](#revoking-refresh-tokens) only clears the cached token locally — it does **not** revoke the ORT at the authorization server. The server has no token-only revocation for non-rotating ORTs, so `logout()` is the only way to invalidate an ORT.
334+
> [!WARNING]
335+
> In online mode, [`revokeRefreshToken()`](#revoking-refresh-tokens) revokes the ORT at the authorization server **and terminates the Auth0 session**. The entire local cache (access token, ID token, user profile) is cleared immediately — `isAuthenticated()` returns `false` right away. Redirect the user to login after calling this. Use `logout()` instead if you want a redirect-based sign-out.
325336
326337
### Using with Multi-Resource Refresh Tokens (MRRT)
327338

0 commit comments

Comments
 (0)