Commit 35ec131
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` rename1 parent 8e464f4 commit 35ec131
10 files changed
Lines changed: 508 additions & 46 deletions
File tree
- __tests__
- Auth0Client
- src
- worker
- static
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
157 | | - | |
158 | | - | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
159 | 164 | | |
160 | 165 | | |
161 | 166 | | |
162 | 167 | | |
163 | 168 | | |
164 | 169 | | |
165 | | - | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
166 | 173 | | |
167 | | - | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
168 | 179 | | |
169 | 180 | | |
170 | 181 | | |
| |||
320 | 331 | | |
321 | 332 | | |
322 | 333 | | |
323 | | - | |
324 | | - | |
| 334 | + | |
| 335 | + | |
325 | 336 | | |
326 | 337 | | |
327 | 338 | | |
| |||
0 commit comments