Skip to content

Commit f72913c

Browse files
feat: refactor online access implementation to use refreshTokenMode and enforce type checks
1 parent a3c9c7b commit f72913c

7 files changed

Lines changed: 100 additions & 61 deletions

File tree

__tests__/Auth0Client/onlineAccess.test.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,43 +84,64 @@ describe('Auth0Client', () => {
8484
});
8585

8686
describe('online access — configuration validation', () => {
87-
it('throws InvalidConfigurationError when onlineAccess is true but useDpop is unset', () => {
87+
it('throws InvalidConfigurationError when online but useRefreshTokens is unset', () => {
8888
expect(() =>
89-
setup({ onlineAccess: true } as any)
89+
setup({ refreshTokenMode: 'online', useDpop: true } as any)
9090
).toThrow(InvalidConfigurationError);
9191
});
9292

93-
it('throws InvalidConfigurationError when onlineAccess is true but useDpop is false', () => {
93+
it('error message tells the customer to pass useRefreshTokens: true', () => {
9494
expect(() =>
95-
setup({ onlineAccess: true, useDpop: false } as any)
95+
setup({ refreshTokenMode: 'online', useDpop: true } as any)
96+
).toThrow(/useRefreshTokens: true/);
97+
});
98+
99+
it('throws InvalidConfigurationError when online but useDpop is unset', () => {
100+
expect(() =>
101+
setup({ refreshTokenMode: 'online', useRefreshTokens: true } as any)
102+
).toThrow(InvalidConfigurationError);
103+
});
104+
105+
it('throws InvalidConfigurationError when online but useDpop is false', () => {
106+
expect(() =>
107+
setup({
108+
refreshTokenMode: 'online',
109+
useRefreshTokens: true,
110+
useDpop: false
111+
} as any)
96112
).toThrow(InvalidConfigurationError);
97113
});
98114

99115
it('error message tells the customer to pass useDpop: true', () => {
100-
expect(() => setup({ onlineAccess: true } as any)).toThrow(
101-
/useDpop: true/
102-
);
116+
expect(() =>
117+
setup({ refreshTokenMode: 'online', useRefreshTokens: true } as any)
118+
).toThrow(/useDpop: true/);
103119
});
104120

105-
it('does not throw when onlineAccess is true and useDpop is true', () => {
121+
it('does not throw when online with useRefreshTokens: true and useDpop: true', () => {
106122
expect(() =>
107-
setup({ onlineAccess: true, useDpop: true } as any)
123+
setup({
124+
refreshTokenMode: 'online',
125+
useRefreshTokens: true,
126+
useDpop: true
127+
} as any)
108128
).not.toThrow();
109129
});
110130

111-
it('leaves behavior unchanged when onlineAccess is unset (no DPoP required)', () => {
131+
it('leaves behavior unchanged when refreshTokenMode is unset (no DPoP required)', () => {
112132
expect(() => setup()).not.toThrow();
113133
});
114134

115-
it('leaves behavior unchanged when onlineAccess is false (no DPoP required)', () => {
116-
expect(() => setup({ onlineAccess: false } as any)).not.toThrow();
135+
it('leaves behavior unchanged when refreshTokenMode is offline (no DPoP required)', () => {
136+
expect(() => setup({ refreshTokenMode: 'offline' } as any)).not.toThrow();
117137
});
118138
});
119139

120140
describe('online access — scope injection', () => {
121141
it('injects online_access (and not offline_access) when online', () => {
122142
const auth0 = setup({
123-
onlineAccess: true,
143+
refreshTokenMode: 'online',
144+
useRefreshTokens: true,
124145
useDpop: true,
125146
authorizationParams: {
126147
scope: 'profile email test-scope'
@@ -155,7 +176,8 @@ describe('Auth0Client', () => {
155176
describe('online access — refresh-token routing', () => {
156177
it('routes silent renewal through the refresh_token grant when online', async () => {
157178
const auth0 = setup({
158-
onlineAccess: true,
179+
refreshTokenMode: 'online',
180+
useRefreshTokens: true,
159181
useDpop: true,
160182
cacheLocation: 'localstorage'
161183
} as any);
@@ -179,7 +201,8 @@ describe('Auth0Client', () => {
179201
describe('online access — non-rotation', () => {
180202
it('does NOT rotate the stored refresh token when online', async () => {
181203
const auth0 = setup({
182-
onlineAccess: true,
204+
refreshTokenMode: 'online',
205+
useRefreshTokens: true,
183206
useDpop: true,
184207
cacheLocation: 'localstorage'
185208
} as any);

__tests__/Auth0Client/onlineAccess.types.test.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/**
22
* Compile-time enforcement of the online-access option, via `createAuth0Client`
3-
* overloads (the interface itself stays a plain, non-breaking optional boolean).
3+
* overloads (the interface itself stays plain, non-breaking optional fields).
44
*
55
* These assertions are validated by ts-jest's type checker at test time: each
66
* `@ts-expect-error` MUST sit over a genuine type error, or TypeScript reports an
77
* unused-directive error and the suite fails. The runtime safety net (the
88
* `InvalidConfigurationError` throw) is covered separately in onlineAccess.test.ts.
99
*
10-
* The overloads only narrow on a literal `onlineAccess: true`, so these cover the
11-
* common literal mistakes a TS user would make at the call site.
10+
* The overloads only narrow on a literal `refreshTokenMode: 'online'`, so these cover
11+
* the common literal mistakes a TS user would make at the call site.
1212
*/
1313
import { createAuth0Client } from '../../src';
1414

@@ -18,40 +18,49 @@ const base = {
1818
};
1919

2020
describe('createAuth0Client — online access type enforcement', () => {
21-
it('requires useDpop: true and forbids useRefreshTokens when onlineAccess is true', () => {
21+
it('requires useRefreshTokens: true and useDpop: true when refreshTokenMode is online', () => {
2222
// Compile-only — never invoked. Wrapped in a never-true guard so the calls
2323
// are type-checked but do not execute (no DOM/crypto needed).
2424
const _typecheck = async () => {
2525
// Valid combo.
26-
await createAuth0Client({ ...base, onlineAccess: true, useDpop: true });
26+
await createAuth0Client({
27+
...base,
28+
refreshTokenMode: 'online',
29+
useRefreshTokens: true,
30+
useDpop: true
31+
});
32+
33+
// Missing useRefreshTokens → compile error.
34+
// @ts-expect-error refreshTokenMode: 'online' requires useRefreshTokens: true
35+
await createAuth0Client({ ...base, refreshTokenMode: 'online', useDpop: true });
2736

2837
// Missing useDpop → compile error.
29-
// @ts-expect-error onlineAccess: true requires useDpop: true
30-
await createAuth0Client({ ...base, onlineAccess: true });
38+
// @ts-expect-error refreshTokenMode: 'online' requires useDpop: true
39+
await createAuth0Client({
40+
...base,
41+
refreshTokenMode: 'online',
42+
useRefreshTokens: true
43+
});
3144

3245
// useDpop: false → compile error.
33-
// @ts-expect-error onlineAccess: true requires useDpop: true (not false)
34-
await createAuth0Client({ ...base, onlineAccess: true, useDpop: false });
35-
36-
// useRefreshTokens alongside online → compile error (it injects offline_access).
37-
// @ts-expect-error useRefreshTokens conflicts with onlineAccess
46+
// @ts-expect-error refreshTokenMode: 'online' requires useDpop: true (not false)
3847
await createAuth0Client({
3948
...base,
40-
onlineAccess: true,
41-
useDpop: true,
42-
useRefreshTokens: true
49+
refreshTokenMode: 'online',
50+
useRefreshTokens: true,
51+
useDpop: false
4352
});
4453
};
4554
void _typecheck;
4655

4756
expect(true).toBe(true);
4857
});
4958

50-
it('leaves the legacy (non-online) call unconstrained', () => {
59+
it('leaves the legacy (offline / non-online) call unconstrained', () => {
5160
const _typecheck = async () => {
5261
// Legacy combos remain valid — the overloads must not constrain them.
5362
await createAuth0Client({ ...base, useRefreshTokens: true, useDpop: false });
54-
await createAuth0Client({ ...base, onlineAccess: false, useRefreshTokens: true });
63+
await createAuth0Client({ ...base, refreshTokenMode: 'offline', useRefreshTokens: true });
5564
};
5665
void _typecheck;
5766

__tests__/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ const setup = async (
9090
domain: TEST_DOMAIN,
9191
clientId: TEST_CLIENT_ID,
9292
...clientOptions
93-
} as Auth0ClientOptions & { onlineAccess?: false };
93+
} as Auth0ClientOptions & { refreshTokenMode?: 'offline' };
9494

9595
const auth0 = callConstructor
9696
? await createAuth0Client(opts)

src/Auth0Client.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -191,30 +191,32 @@ export class Auth0Client {
191191
scope: DEFAULT_SCOPE
192192
},
193193
useRefreshTokensFallback: false,
194-
useFormData: true
194+
useFormData: true,
195+
refreshTokenMode: 'offline',
195196
};
196197

197198
/**
198199
* Validates the online-access configuration and returns whether online mode is enabled.
199200
*
200-
* Online access requires DPoP, so `onlineAccess: true` without `useDpop: true` throws.
201+
* Online mode (`refreshTokenMode: 'online'`) is a refresh-token grant, so it requires
202+
* `useRefreshTokens: true`, and it requires DPoP, so `useDpop: true` is mandatory.
201203
*/
202204
private resolveOnlineAccess(options: Auth0ClientOptions): boolean {
203205

204-
if (options.onlineAccess !== true) {
206+
if (options.refreshTokenMode !== 'online') {
205207
return false;
206208
}
207209

208-
if (options.useRefreshTokens == true) {
210+
if (options.useRefreshTokens !== true) {
209211
throw new InvalidConfigurationError(
210-
'`onlineAccess: true` requires useRefreshTokens to be false.',
211-
'Set `useRefreshTokens: false`.'
212+
'`refreshTokenMode: "online"` requires the refresh-token grant.',
213+
'Set `useRefreshTokens: true`.'
212214
);
213215
}
214216

215217
if (options.useDpop !== true) {
216218
throw new InvalidConfigurationError(
217-
'`onlineAccess: true` requires DPoP, which is missing or disabled.',
219+
'`refreshTokenMode: "online"` requires DPoP, which is missing or disabled.',
218220
'Set `useDpop: true` (DPoP is mandatory for online access).'
219221
);
220222
}
@@ -350,11 +352,12 @@ export class Auth0Client {
350352
this
351353
);
352354

353-
// Don't use web workers unless using refresh tokens in memory
355+
// Don't use web workers unless using refresh tokens in memory.
356+
// Online mode requires `useRefreshTokens: true`, so it is covered here too.
354357
if (
355358
typeof window !== 'undefined' &&
356359
window.Worker &&
357-
(this.options.useRefreshTokens || this.onlineAccess) &&
360+
this.options.useRefreshTokens &&
358361
cacheLocation === CACHE_LOCATION_MEMORY
359362
) {
360363
if (this.options.workerUrl) {
@@ -1020,7 +1023,7 @@ export class Auth0Client {
10201023
}
10211024
}
10221025

1023-
const authResult = (this.options.useRefreshTokens || this.onlineAccess)
1026+
const authResult = this.options.useRefreshTokens
10241027
? await this._getTokenUsingRefreshToken(getTokenOptions)
10251028
: await this._getTokenFromIFrame(getTokenOptions);
10261029

@@ -1237,7 +1240,7 @@ export class Auth0Client {
12371240
* await auth0.revokeRefreshToken({ audience: 'https://api2.example.com' });
12381241
*/
12391242
public async revokeRefreshToken(options: RevokeRefreshTokenOptions = {}): Promise<void> {
1240-
if (!this.options.useRefreshTokens && !this.onlineAccess) {
1243+
if (!this.options.useRefreshTokens) {
12411244
return;
12421245
}
12431246

src/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class GenericError extends Error {
3030

3131
/**
3232
* Thrown at construction time when the Auth0Client is configured with an invalid combination
33-
* of options (e.g. `onlineAccess: true` without `useDpop: true`). Carries a `suggestion` with
33+
* of options (e.g. `refreshTokenMode: 'online'` without `useDpop: true`). Carries a `suggestion` with
3434
* the exact configuration change to make.
3535
*/
3636
export class InvalidConfigurationError extends GenericError {

src/global.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -313,18 +313,22 @@ export interface Auth0ClientOptions {
313313
useDpop?: boolean;
314314

315315
/**
316-
* Opt in to Online Refresh Tokens (ORTs): non-rotating refresh tokens bound to
317-
* the Auth0 session lifetime. When `true`, the SDK injects the `online_access`
318-
* scope, routes token renewal through the refresh_token grant, and stores the
319-
* (non-rotating) refresh token in the existing cache.
316+
* Selects the refresh-token variant used when `useRefreshTokens: true`.
320317
*
321-
* Online access requires DPoP — you must also set `useDpop: true`. Do NOT set
322-
* `useRefreshTokens` alongside it: that injects `offline_access`, which conflicts
323-
* with `online_access`.
318+
* - `'offline'` (default): rotating offline refresh tokens, requested via the
319+
* `offline_access` scope. This is the existing behavior.
320+
* - `'online'`: non-rotating Online Refresh Tokens (ORTs), bound to the Auth0
321+
* session lifetime, requested via the `online_access` scope. When the session
322+
* ends or is revoked, the ORT becomes invalid.
324323
*
325-
* Defaults to `false` — when unset or `false` the SDK behaves exactly as before.
324+
* Online mode requires `useRefreshTokens: true` (it is a kind of refresh-token
325+
* grant) and `useDpop: true` (DPoP is mandatory for online access). The
326+
* `online_access` and `offline_access` scopes are mutually exclusive — online
327+
* mode never injects `offline_access`.
328+
*
329+
* Defaults to `'offline'` — when unset the SDK behaves exactly as before.
326330
*/
327-
onlineAccess?: boolean;
331+
refreshTokenMode?: 'offline' | 'online';
328332

329333
/**
330334
* Configures automatic handling of interactive authentication errors.

src/index.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,21 @@ import './global';
66
export * from './global';
77

88
/**
9-
* Online access requires DPoP and is incompatible with `useRefreshTokens` (which
10-
* requests `offline_access`). When `onlineAccess` is the literal `true`, the compiler
11-
* requires `useDpop: true` and forbids `useRefreshTokens`. This only narrows on a literal
12-
* `true`; dynamic values, casts, and plain JS are covered by the runtime check in the
9+
* Online mode (`refreshTokenMode: 'online'`) is a refresh-token grant and requires DPoP.
10+
* When `refreshTokenMode` is the literal `'online'`, the compiler requires both
11+
* `useRefreshTokens: true` and `useDpop: true`. This only narrows on the literal `'online'`;
12+
* dynamic values, casts, and plain JS are covered by the runtime check in the
1313
* `Auth0Client` constructor.
1414
*/
1515
export async function createAuth0Client(
1616
options: Auth0ClientOptions & {
17-
onlineAccess: true;
17+
refreshTokenMode: 'online';
18+
useRefreshTokens: true;
1819
useDpop: true;
19-
useRefreshTokens?: never;
2020
}
2121
): Promise<Auth0Client>;
2222
export async function createAuth0Client(
23-
options: Auth0ClientOptions & { onlineAccess?: false }
23+
options: Auth0ClientOptions & { refreshTokenMode?: 'offline' }
2424
): Promise<Auth0Client>;
2525
/**
2626
* Asynchronously creates the Auth0Client instance and calls `checkSession`.

0 commit comments

Comments
 (0)