@auth0/auth0-spa-js 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/auth0-spa-js.development.js +615 -17
- package/dist/auth0-spa-js.development.js.map +1 -1
- package/dist/auth0-spa-js.production.esm.js +1 -1
- package/dist/auth0-spa-js.production.esm.js.map +1 -1
- package/dist/auth0-spa-js.production.js +1 -1
- package/dist/auth0-spa-js.production.js.map +1 -1
- package/dist/auth0-spa-js.worker.development.js +10 -2
- package/dist/auth0-spa-js.worker.development.js.map +1 -1
- package/dist/auth0-spa-js.worker.production.js +1 -1
- package/dist/auth0-spa-js.worker.production.js.map +1 -1
- package/dist/lib/auth0-spa-js.cjs.js +656 -17
- package/dist/lib/auth0-spa-js.cjs.js.map +1 -1
- package/dist/typings/Auth0Client.d.ts +54 -4
- package/dist/typings/Auth0Client.utils.d.ts +1 -1
- package/dist/typings/TokenExchange.d.ts +3 -2
- package/dist/typings/api.d.ts +1 -1
- package/dist/typings/cache/shared.d.ts +1 -0
- package/dist/typings/dpop/dpop.d.ts +17 -0
- package/dist/typings/dpop/storage.d.ts +27 -0
- package/dist/typings/dpop/utils.d.ts +15 -0
- package/dist/typings/errors.d.ts +7 -0
- package/dist/typings/fetcher.d.ts +48 -0
- package/dist/typings/global.d.ts +21 -0
- package/dist/typings/http.d.ts +2 -1
- package/dist/typings/index.d.ts +2 -1
- package/dist/typings/utils.d.ts +6 -0
- package/dist/typings/version.d.ts +1 -1
- package/package.json +22 -19
- package/src/Auth0Client.ts +126 -11
- package/src/Auth0Client.utils.ts +4 -2
- package/src/TokenExchange.ts +3 -2
- package/src/api.ts +17 -3
- package/src/cache/shared.ts +1 -0
- package/src/dpop/dpop.ts +56 -0
- package/src/dpop/storage.ts +134 -0
- package/src/dpop/utils.ts +66 -0
- package/src/errors.ts +11 -0
- package/src/fetcher.ts +224 -0
- package/src/global.ts +23 -0
- package/src/http.ts +70 -5
- package/src/index.ts +4 -1
- package/src/utils.ts +15 -0
- package/src/version.ts +1 -1
- package/src/worker/token.worker.ts +11 -5
package/src/Auth0Client.ts
CHANGED
|
@@ -93,6 +93,12 @@ import {
|
|
|
93
93
|
patchOpenUrlWithOnRedirect
|
|
94
94
|
} from './Auth0Client.utils';
|
|
95
95
|
import { CustomTokenExchangeOptions } from './TokenExchange';
|
|
96
|
+
import { Dpop } from './dpop/dpop';
|
|
97
|
+
import {
|
|
98
|
+
Fetcher,
|
|
99
|
+
type FetcherConfig,
|
|
100
|
+
type CustomFetchMinimalOutput
|
|
101
|
+
} from './fetcher';
|
|
96
102
|
|
|
97
103
|
/**
|
|
98
104
|
* @ignore
|
|
@@ -119,6 +125,7 @@ export class Auth0Client {
|
|
|
119
125
|
private readonly tokenIssuer: string;
|
|
120
126
|
private readonly scope: string;
|
|
121
127
|
private readonly cookieStorage: ClientStorage;
|
|
128
|
+
private readonly dpop: Dpop | undefined;
|
|
122
129
|
private readonly sessionCheckExpiryDays: number;
|
|
123
130
|
private readonly orgHintCookieName: string;
|
|
124
131
|
private readonly isAuthenticatedCookieName: string;
|
|
@@ -130,7 +137,6 @@ export class Auth0Client {
|
|
|
130
137
|
private readonly userCache: ICache = new InMemoryCache().enclosedCache;
|
|
131
138
|
|
|
132
139
|
private worker?: Worker;
|
|
133
|
-
|
|
134
140
|
private readonly defaultOptions: Partial<Auth0ClientOptions> = {
|
|
135
141
|
authorizationParams: {
|
|
136
142
|
scope: DEFAULT_SCOPE
|
|
@@ -222,6 +228,10 @@ export class Auth0Client {
|
|
|
222
228
|
this.nowProvider
|
|
223
229
|
);
|
|
224
230
|
|
|
231
|
+
this.dpop = this.options.useDpop
|
|
232
|
+
? new Dpop(this.options.clientId)
|
|
233
|
+
: undefined;
|
|
234
|
+
|
|
225
235
|
this.domainUrl = getDomain(this.options.domain);
|
|
226
236
|
this.tokenIssuer = getTokenIssuer(this.options.issuer, this.domainUrl);
|
|
227
237
|
|
|
@@ -301,6 +311,7 @@ export class Auth0Client {
|
|
|
301
311
|
const code_verifier = createRandomString();
|
|
302
312
|
const code_challengeBuffer = await sha256(code_verifier);
|
|
303
313
|
const code_challenge = bufferToBase64UrlEncoded(code_challengeBuffer);
|
|
314
|
+
const thumbprint = await this.dpop?.calculateThumbprint();
|
|
304
315
|
|
|
305
316
|
const params = getAuthorizeParams(
|
|
306
317
|
this.options,
|
|
@@ -312,7 +323,8 @@ export class Auth0Client {
|
|
|
312
323
|
authorizationParams.redirect_uri ||
|
|
313
324
|
this.options.authorizationParams.redirect_uri ||
|
|
314
325
|
fallbackRedirectUri,
|
|
315
|
-
authorizeOptions?.response_mode
|
|
326
|
+
authorizeOptions?.response_mode,
|
|
327
|
+
thumbprint
|
|
316
328
|
);
|
|
317
329
|
|
|
318
330
|
const url = this._authorizeUrl(params);
|
|
@@ -716,11 +728,17 @@ export class Auth0Client {
|
|
|
716
728
|
? await this._getTokenUsingRefreshToken(getTokenOptions)
|
|
717
729
|
: await this._getTokenFromIFrame(getTokenOptions);
|
|
718
730
|
|
|
719
|
-
const {
|
|
720
|
-
|
|
731
|
+
const {
|
|
732
|
+
id_token,
|
|
733
|
+
token_type,
|
|
734
|
+
access_token,
|
|
735
|
+
oauthTokenScope,
|
|
736
|
+
expires_in
|
|
737
|
+
} = authResult;
|
|
721
738
|
|
|
722
739
|
return {
|
|
723
740
|
id_token,
|
|
741
|
+
token_type,
|
|
724
742
|
access_token,
|
|
725
743
|
...(oauthTokenScope ? { scope: oauthTokenScope } : null),
|
|
726
744
|
expires_in
|
|
@@ -848,6 +866,8 @@ export class Auth0Client {
|
|
|
848
866
|
});
|
|
849
867
|
this.userCache.remove(CACHE_KEY_ID_TOKEN_SUFFIX);
|
|
850
868
|
|
|
869
|
+
await this.dpop?.clear();
|
|
870
|
+
|
|
851
871
|
const url = this._buildLogoutUrl(logoutOptions);
|
|
852
872
|
|
|
853
873
|
if (openUrl) {
|
|
@@ -901,7 +921,15 @@ export class Auth0Client {
|
|
|
901
921
|
const authorizeTimeout =
|
|
902
922
|
options.timeoutInSeconds || this.options.authorizeTimeoutInSeconds;
|
|
903
923
|
|
|
904
|
-
|
|
924
|
+
// Extract origin from domainUrl, fallback to domainUrl if URL parsing fails
|
|
925
|
+
let eventOrigin: string;
|
|
926
|
+
try {
|
|
927
|
+
eventOrigin = new URL(this.domainUrl).origin;
|
|
928
|
+
} catch {
|
|
929
|
+
eventOrigin = this.domainUrl;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const codeResult = await runIframe(url, eventOrigin, authorizeTimeout);
|
|
905
933
|
|
|
906
934
|
if (stateIn !== codeResult.state) {
|
|
907
935
|
throw new GenericError('state_mismatch', 'Invalid state');
|
|
@@ -1072,11 +1100,13 @@ export class Auth0Client {
|
|
|
1072
1100
|
);
|
|
1073
1101
|
|
|
1074
1102
|
if (entry && entry.access_token) {
|
|
1075
|
-
const { access_token, oauthTokenScope, expires_in } =
|
|
1103
|
+
const { token_type, access_token, oauthTokenScope, expires_in } =
|
|
1104
|
+
entry as CacheEntry;
|
|
1076
1105
|
const cache = await this._getIdTokenFromCache();
|
|
1077
1106
|
return (
|
|
1078
1107
|
cache && {
|
|
1079
1108
|
id_token: cache.id_token,
|
|
1109
|
+
token_type: token_type ? token_type : 'Bearer',
|
|
1080
1110
|
access_token,
|
|
1081
1111
|
...(oauthTokenScope ? { scope: oauthTokenScope } : null),
|
|
1082
1112
|
expires_in
|
|
@@ -1112,6 +1142,7 @@ export class Auth0Client {
|
|
|
1112
1142
|
auth0Client: this.options.auth0Client,
|
|
1113
1143
|
useFormData: this.options.useFormData,
|
|
1114
1144
|
timeout: this.httpTimeoutMs,
|
|
1145
|
+
dpop: this.dpop,
|
|
1115
1146
|
...options
|
|
1116
1147
|
},
|
|
1117
1148
|
this.worker
|
|
@@ -1171,7 +1202,7 @@ export class Auth0Client {
|
|
|
1171
1202
|
* - `subject_token_type`: The type of the external token (validated by this function).
|
|
1172
1203
|
* - `scope`: A unique set of scopes, generated by merging the scopes supplied in the options
|
|
1173
1204
|
* with the SDK’s default scopes.
|
|
1174
|
-
* - `audience`: The target audience,
|
|
1205
|
+
* - `audience`: The target audience from the options, with fallback to the SDK's authorization configuration.
|
|
1175
1206
|
*
|
|
1176
1207
|
* **Example Usage:**
|
|
1177
1208
|
*
|
|
@@ -1180,15 +1211,15 @@ export class Auth0Client {
|
|
|
1180
1211
|
* const options: CustomTokenExchangeOptions = {
|
|
1181
1212
|
* subject_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...',
|
|
1182
1213
|
* subject_token_type: 'urn:acme:legacy-system-token',
|
|
1183
|
-
* scope:
|
|
1214
|
+
* scope: "openid profile"
|
|
1184
1215
|
* };
|
|
1185
1216
|
*
|
|
1186
1217
|
* // Exchange the external token for Auth0 tokens
|
|
1187
1218
|
* try {
|
|
1188
1219
|
* const tokenResponse = await instance.exchangeToken(options);
|
|
1189
|
-
*
|
|
1220
|
+
* // Use tokenResponse.access_token, tokenResponse.id_token, etc.
|
|
1190
1221
|
* } catch (error) {
|
|
1191
|
-
*
|
|
1222
|
+
* // Handle token exchange error
|
|
1192
1223
|
* }
|
|
1193
1224
|
* ```
|
|
1194
1225
|
*/
|
|
@@ -1200,7 +1231,91 @@ export class Auth0Client {
|
|
|
1200
1231
|
subject_token: options.subject_token,
|
|
1201
1232
|
subject_token_type: options.subject_token_type,
|
|
1202
1233
|
scope: getUniqueScopes(options.scope, this.scope),
|
|
1203
|
-
audience: this.options.authorizationParams.audience
|
|
1234
|
+
audience: options.audience || this.options.authorizationParams.audience
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
protected _assertDpop(dpop: Dpop | undefined): asserts dpop is Dpop {
|
|
1239
|
+
if (!dpop) {
|
|
1240
|
+
throw new Error('`useDpop` option must be enabled before using DPoP.');
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Returns the current DPoP nonce used for making requests to Auth0.
|
|
1246
|
+
*
|
|
1247
|
+
* It can return `undefined` because when starting fresh it will not
|
|
1248
|
+
* be populated until after the first response from the server.
|
|
1249
|
+
*
|
|
1250
|
+
* It requires enabling the {@link Auth0ClientOptions.useDpop} option.
|
|
1251
|
+
*
|
|
1252
|
+
* @param nonce The nonce value.
|
|
1253
|
+
* @param id The identifier of a nonce: if absent, it will get the nonce
|
|
1254
|
+
* used for requests to Auth0. Otherwise, it will be used to
|
|
1255
|
+
* select a specific non-Auth0 nonce.
|
|
1256
|
+
*/
|
|
1257
|
+
public getDpopNonce(id?: string): Promise<string | undefined> {
|
|
1258
|
+
this._assertDpop(this.dpop);
|
|
1259
|
+
|
|
1260
|
+
return this.dpop.getNonce(id);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Sets the current DPoP nonce used for making requests to Auth0.
|
|
1265
|
+
*
|
|
1266
|
+
* It requires enabling the {@link Auth0ClientOptions.useDpop} option.
|
|
1267
|
+
*
|
|
1268
|
+
* @param nonce The nonce value.
|
|
1269
|
+
* @param id The identifier of a nonce: if absent, it will set the nonce
|
|
1270
|
+
* used for requests to Auth0. Otherwise, it will be used to
|
|
1271
|
+
* select a specific non-Auth0 nonce.
|
|
1272
|
+
*/
|
|
1273
|
+
public setDpopNonce(nonce: string, id?: string): Promise<void> {
|
|
1274
|
+
this._assertDpop(this.dpop);
|
|
1275
|
+
|
|
1276
|
+
return this.dpop.setNonce(nonce, id);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Returns a string to be used to demonstrate possession of the private
|
|
1281
|
+
* key used to cryptographically bind access tokens with DPoP.
|
|
1282
|
+
*
|
|
1283
|
+
* It requires enabling the {@link Auth0ClientOptions.useDpop} option.
|
|
1284
|
+
*/
|
|
1285
|
+
public generateDpopProof(params: {
|
|
1286
|
+
url: string;
|
|
1287
|
+
method: string;
|
|
1288
|
+
nonce?: string;
|
|
1289
|
+
accessToken: string;
|
|
1290
|
+
}): Promise<string> {
|
|
1291
|
+
this._assertDpop(this.dpop);
|
|
1292
|
+
|
|
1293
|
+
return this.dpop.generateProof(params);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Returns a new `Fetcher` class that will contain a `fetchWithAuth()` method.
|
|
1298
|
+
* This is a drop-in replacement for the Fetch API's `fetch()` method, but will
|
|
1299
|
+
* handle certain authentication logic for you, like building the proper auth
|
|
1300
|
+
* headers or managing DPoP nonces and retries automatically.
|
|
1301
|
+
*
|
|
1302
|
+
* Check the `EXAMPLES.md` file for a deeper look into this method.
|
|
1303
|
+
*/
|
|
1304
|
+
public createFetcher<TOutput extends CustomFetchMinimalOutput = Response>(
|
|
1305
|
+
config: FetcherConfig<TOutput> = {}
|
|
1306
|
+
): Fetcher<TOutput> {
|
|
1307
|
+
if (this.options.useDpop && !config.dpopNonceId) {
|
|
1308
|
+
throw new TypeError(
|
|
1309
|
+
'When `useDpop` is enabled, `dpopNonceId` must be set when calling `createFetcher()`.'
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
return new Fetcher(config, {
|
|
1314
|
+
isDpopEnabled: () => !!this.options.useDpop,
|
|
1315
|
+
getAccessToken: () => this.getTokenSilently(),
|
|
1316
|
+
getDpopNonce: () => this.getDpopNonce(config.dpopNonceId),
|
|
1317
|
+
setDpopNonce: nonce => this.setDpopNonce(nonce),
|
|
1318
|
+
generateDpopProof: params => this.generateDpopProof(params)
|
|
1204
1319
|
});
|
|
1205
1320
|
}
|
|
1206
1321
|
}
|
package/src/Auth0Client.utils.ts
CHANGED
|
@@ -57,7 +57,8 @@ export const getAuthorizeParams = (
|
|
|
57
57
|
nonce: string,
|
|
58
58
|
code_challenge: string,
|
|
59
59
|
redirect_uri: string | undefined,
|
|
60
|
-
response_mode: string | undefined
|
|
60
|
+
response_mode: string | undefined,
|
|
61
|
+
thumbprint: string | undefined
|
|
61
62
|
): AuthorizeOptions => {
|
|
62
63
|
return {
|
|
63
64
|
client_id: clientOptions.clientId,
|
|
@@ -71,7 +72,8 @@ export const getAuthorizeParams = (
|
|
|
71
72
|
redirect_uri:
|
|
72
73
|
redirect_uri || clientOptions.authorizationParams.redirect_uri,
|
|
73
74
|
code_challenge,
|
|
74
|
-
code_challenge_method: 'S256'
|
|
75
|
+
code_challenge_method: 'S256',
|
|
76
|
+
dpop_jkt: thumbprint
|
|
75
77
|
};
|
|
76
78
|
};
|
|
77
79
|
|
package/src/TokenExchange.ts
CHANGED
|
@@ -38,12 +38,13 @@ export type CustomTokenExchangeOptions = {
|
|
|
38
38
|
* The target audience for the requested Auth0 token
|
|
39
39
|
*
|
|
40
40
|
* @remarks
|
|
41
|
-
* Must match exactly with an API identifier configured in your Auth0 tenant
|
|
41
|
+
* Must match exactly with an API identifier configured in your Auth0 tenant.
|
|
42
|
+
* If not provided, falls back to the client's default audience.
|
|
42
43
|
*
|
|
43
44
|
* @example
|
|
44
45
|
* "https://api.your-service.com/v1"
|
|
45
46
|
*/
|
|
46
|
-
audience
|
|
47
|
+
audience?: string;
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Space-separated list of OAuth 2.0 scopes being requested
|
package/src/api.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TokenEndpointOptions, TokenEndpointResponse } from './global';
|
|
2
2
|
import { DEFAULT_AUTH0_CLIENT } from './constants';
|
|
3
|
+
import * as dpopUtils from './dpop/utils';
|
|
3
4
|
import { getJSON } from './http';
|
|
4
5
|
import { createQueryParams } from './utils';
|
|
5
6
|
|
|
@@ -11,13 +12,25 @@ export async function oauthToken(
|
|
|
11
12
|
scope,
|
|
12
13
|
auth0Client,
|
|
13
14
|
useFormData,
|
|
15
|
+
dpop,
|
|
14
16
|
...options
|
|
15
17
|
}: TokenEndpointOptions,
|
|
16
18
|
worker?: Worker
|
|
17
19
|
) {
|
|
20
|
+
const isTokenExchange =
|
|
21
|
+
options.grant_type === 'urn:ietf:params:oauth:grant-type:token-exchange';
|
|
22
|
+
|
|
23
|
+
const allParams = {
|
|
24
|
+
...options,
|
|
25
|
+
...(isTokenExchange && audience && { audience }),
|
|
26
|
+
...(isTokenExchange && scope && { scope })
|
|
27
|
+
};
|
|
28
|
+
|
|
18
29
|
const body = useFormData
|
|
19
|
-
? createQueryParams(
|
|
20
|
-
: JSON.stringify(
|
|
30
|
+
? createQueryParams(allParams)
|
|
31
|
+
: JSON.stringify(allParams);
|
|
32
|
+
|
|
33
|
+
const isDpopSupported = dpopUtils.isGrantTypeSupported(options.grant_type);
|
|
21
34
|
|
|
22
35
|
return await getJSON<TokenEndpointResponse>(
|
|
23
36
|
`${baseUrl}/oauth/token`,
|
|
@@ -37,6 +50,7 @@ export async function oauthToken(
|
|
|
37
50
|
}
|
|
38
51
|
},
|
|
39
52
|
worker,
|
|
40
|
-
useFormData
|
|
53
|
+
useFormData,
|
|
54
|
+
isDpopSupported ? dpop : undefined
|
|
41
55
|
);
|
|
42
56
|
}
|
package/src/cache/shared.ts
CHANGED
package/src/dpop/dpop.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DpopStorage } from './storage';
|
|
2
|
+
import * as dpopUtils from './utils';
|
|
3
|
+
|
|
4
|
+
export class Dpop {
|
|
5
|
+
protected readonly storage: DpopStorage;
|
|
6
|
+
|
|
7
|
+
public constructor(clientId: string) {
|
|
8
|
+
this.storage = new DpopStorage(clientId);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public getNonce(id?: string): Promise<string | undefined> {
|
|
12
|
+
return this.storage.findNonce(id);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public setNonce(nonce: string, id?: string): Promise<void> {
|
|
16
|
+
return this.storage.setNonce(nonce, id);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
protected async getOrGenerateKeyPair(): Promise<dpopUtils.KeyPair> {
|
|
20
|
+
let keyPair = await this.storage.findKeyPair();
|
|
21
|
+
|
|
22
|
+
if (!keyPair) {
|
|
23
|
+
keyPair = await dpopUtils.generateKeyPair();
|
|
24
|
+
await this.storage.setKeyPair(keyPair);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return keyPair;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async generateProof(params: {
|
|
31
|
+
url: string;
|
|
32
|
+
method: string;
|
|
33
|
+
nonce?: string;
|
|
34
|
+
accessToken?: string;
|
|
35
|
+
}): Promise<string> {
|
|
36
|
+
const keyPair = await this.getOrGenerateKeyPair();
|
|
37
|
+
|
|
38
|
+
return dpopUtils.generateProof({
|
|
39
|
+
keyPair,
|
|
40
|
+
...params
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async calculateThumbprint(): Promise<string> {
|
|
45
|
+
const keyPair = await this.getOrGenerateKeyPair();
|
|
46
|
+
|
|
47
|
+
return dpopUtils.calculateThumbprint(keyPair);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async clear(): Promise<void> {
|
|
51
|
+
await Promise.all([
|
|
52
|
+
this.storage.clearNonces(),
|
|
53
|
+
this.storage.clearKeyPairs()
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type KeyPair } from './utils';
|
|
2
|
+
|
|
3
|
+
const VERSION = 1;
|
|
4
|
+
const NAME = 'auth0-spa-js';
|
|
5
|
+
const TABLES = {
|
|
6
|
+
NONCE: 'nonce',
|
|
7
|
+
KEYPAIR: 'keypair'
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
const AUTH0_NONCE_ID = 'auth0';
|
|
11
|
+
|
|
12
|
+
type Table = (typeof TABLES)[keyof typeof TABLES];
|
|
13
|
+
|
|
14
|
+
export class DpopStorage {
|
|
15
|
+
protected readonly clientId: string;
|
|
16
|
+
protected dbHandle: IDBDatabase | undefined;
|
|
17
|
+
|
|
18
|
+
constructor(clientId: string) {
|
|
19
|
+
this.clientId = clientId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected getVersion(): number {
|
|
23
|
+
return VERSION;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
protected createDbHandle(): Promise<IDBDatabase> {
|
|
27
|
+
const req = window.indexedDB.open(NAME, this.getVersion());
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
req.onupgradeneeded = () =>
|
|
31
|
+
Object.values(TABLES).forEach(t => req.result.createObjectStore(t));
|
|
32
|
+
|
|
33
|
+
req.onerror = () => reject(req.error);
|
|
34
|
+
req.onsuccess = () => resolve(req.result);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected async getDbHandle(): Promise<IDBDatabase> {
|
|
39
|
+
if (!this.dbHandle) {
|
|
40
|
+
this.dbHandle = await this.createDbHandle();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return this.dbHandle;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected async executeDbRequest<T = unknown>(
|
|
47
|
+
table: string,
|
|
48
|
+
mode: IDBTransactionMode,
|
|
49
|
+
requestFactory: (table: IDBObjectStore) => IDBRequest<T>
|
|
50
|
+
): Promise<T> {
|
|
51
|
+
const db = await this.getDbHandle();
|
|
52
|
+
|
|
53
|
+
const txn = db.transaction(table, mode);
|
|
54
|
+
const store = txn.objectStore(table);
|
|
55
|
+
|
|
56
|
+
const request = requestFactory(store);
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
request.onsuccess = () => resolve(request.result);
|
|
60
|
+
request.onerror = () => reject(request.error);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected buildKey(id?: string): string {
|
|
65
|
+
const finalId = id
|
|
66
|
+
? `_${id}` // prefix to avoid collisions
|
|
67
|
+
: AUTH0_NONCE_ID;
|
|
68
|
+
|
|
69
|
+
return `${this.clientId}::${finalId}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public setNonce(nonce: string, id?: string): Promise<void> {
|
|
73
|
+
return this.save(TABLES.NONCE, this.buildKey(id), nonce);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public setKeyPair(keyPair: KeyPair): Promise<void> {
|
|
77
|
+
return this.save(TABLES.KEYPAIR, this.buildKey(), keyPair);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected async save(
|
|
81
|
+
table: Table,
|
|
82
|
+
key: IDBValidKey,
|
|
83
|
+
obj: unknown
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
return void await this.executeDbRequest(table, 'readwrite', table =>
|
|
86
|
+
table.put(obj, key)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public findNonce(id?: string): Promise<string | undefined> {
|
|
91
|
+
return this.find(TABLES.NONCE, this.buildKey(id));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public findKeyPair(): Promise<KeyPair | undefined> {
|
|
95
|
+
return this.find(TABLES.KEYPAIR, this.buildKey());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected find<T = unknown>(
|
|
99
|
+
table: Table,
|
|
100
|
+
key: IDBValidKey
|
|
101
|
+
): Promise<T | undefined> {
|
|
102
|
+
return this.executeDbRequest(table, 'readonly', table => table.get(key));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
protected async deleteBy(
|
|
106
|
+
table: Table,
|
|
107
|
+
predicate: (key: IDBValidKey) => boolean
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
const allKeys = await this.executeDbRequest(table, 'readonly', table =>
|
|
110
|
+
table.getAllKeys()
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
allKeys
|
|
114
|
+
?.filter(predicate)
|
|
115
|
+
.map(k =>
|
|
116
|
+
this.executeDbRequest(table, 'readwrite', table => table.delete(k))
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
protected deleteByClientId(table: Table, clientId: string): Promise<void> {
|
|
121
|
+
return this.deleteBy(
|
|
122
|
+
table,
|
|
123
|
+
k => typeof k === 'string' && k.startsWith(`${clientId}::`)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public clearNonces(): Promise<void> {
|
|
128
|
+
return this.deleteByClientId(TABLES.NONCE, this.clientId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public clearKeyPairs(): Promise<void> {
|
|
132
|
+
return this.deleteByClientId(TABLES.KEYPAIR, this.clientId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as dpopLib from 'dpop';
|
|
2
|
+
|
|
3
|
+
export const DPOP_NONCE_HEADER = 'dpop-nonce';
|
|
4
|
+
|
|
5
|
+
const KEY_PAIR_ALGORITHM: dpopLib.JWSAlgorithm = 'ES256';
|
|
6
|
+
|
|
7
|
+
const SUPPORTED_GRANT_TYPES = [
|
|
8
|
+
'authorization_code',
|
|
9
|
+
'refresh_token',
|
|
10
|
+
'urn:ietf:params:oauth:grant-type:token-exchange'
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export type KeyPair = Readonly<dpopLib.KeyPair>;
|
|
14
|
+
|
|
15
|
+
type GenerateProofParams = {
|
|
16
|
+
keyPair: KeyPair;
|
|
17
|
+
url: string;
|
|
18
|
+
method: string;
|
|
19
|
+
nonce?: string;
|
|
20
|
+
accessToken?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function generateKeyPair(): Promise<KeyPair> {
|
|
24
|
+
return dpopLib.generateKeyPair(KEY_PAIR_ALGORITHM, { extractable: false });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function calculateThumbprint(
|
|
28
|
+
keyPair: Pick<KeyPair, 'publicKey'>
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
return dpopLib.calculateThumbprint(keyPair.publicKey);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeUrl(url: string): string {
|
|
34
|
+
const parsedUrl = new URL(url);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* "The HTTP target URI (...) without query and fragment parts"
|
|
38
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9449.html#section-4.2-4.6}
|
|
39
|
+
*/
|
|
40
|
+
parsedUrl.search = '';
|
|
41
|
+
parsedUrl.hash = '';
|
|
42
|
+
|
|
43
|
+
return parsedUrl.href;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function generateProof({
|
|
47
|
+
keyPair,
|
|
48
|
+
url,
|
|
49
|
+
method,
|
|
50
|
+
nonce,
|
|
51
|
+
accessToken
|
|
52
|
+
}: GenerateProofParams): Promise<string> {
|
|
53
|
+
const normalizedUrl = normalizeUrl(url);
|
|
54
|
+
|
|
55
|
+
return dpopLib.generateProof(
|
|
56
|
+
keyPair,
|
|
57
|
+
normalizedUrl,
|
|
58
|
+
method,
|
|
59
|
+
nonce,
|
|
60
|
+
accessToken
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isGrantTypeSupported(grantType: string): boolean {
|
|
65
|
+
return SUPPORTED_GRANT_TYPES.includes(grantType);
|
|
66
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -96,6 +96,17 @@ export class MissingRefreshTokenError extends GenericError {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Error thrown when the wrong DPoP nonce is used and a potential subsequent retry wasn't able to fix it.
|
|
101
|
+
*/
|
|
102
|
+
export class UseDpopNonceError extends GenericError {
|
|
103
|
+
constructor(public newDpopNonce: string | undefined) {
|
|
104
|
+
super('use_dpop_nonce', 'Server rejected DPoP proof: wrong nonce');
|
|
105
|
+
|
|
106
|
+
Object.setPrototypeOf(this, UseDpopNonceError.prototype);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
99
110
|
/**
|
|
100
111
|
* Returns an empty string when value is falsy, or when it's value is included in the exclude argument.
|
|
101
112
|
* @param value The value to check
|