@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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/auth0-spa-js.development.js +615 -17
  3. package/dist/auth0-spa-js.development.js.map +1 -1
  4. package/dist/auth0-spa-js.production.esm.js +1 -1
  5. package/dist/auth0-spa-js.production.esm.js.map +1 -1
  6. package/dist/auth0-spa-js.production.js +1 -1
  7. package/dist/auth0-spa-js.production.js.map +1 -1
  8. package/dist/auth0-spa-js.worker.development.js +10 -2
  9. package/dist/auth0-spa-js.worker.development.js.map +1 -1
  10. package/dist/auth0-spa-js.worker.production.js +1 -1
  11. package/dist/auth0-spa-js.worker.production.js.map +1 -1
  12. package/dist/lib/auth0-spa-js.cjs.js +656 -17
  13. package/dist/lib/auth0-spa-js.cjs.js.map +1 -1
  14. package/dist/typings/Auth0Client.d.ts +54 -4
  15. package/dist/typings/Auth0Client.utils.d.ts +1 -1
  16. package/dist/typings/TokenExchange.d.ts +3 -2
  17. package/dist/typings/api.d.ts +1 -1
  18. package/dist/typings/cache/shared.d.ts +1 -0
  19. package/dist/typings/dpop/dpop.d.ts +17 -0
  20. package/dist/typings/dpop/storage.d.ts +27 -0
  21. package/dist/typings/dpop/utils.d.ts +15 -0
  22. package/dist/typings/errors.d.ts +7 -0
  23. package/dist/typings/fetcher.d.ts +48 -0
  24. package/dist/typings/global.d.ts +21 -0
  25. package/dist/typings/http.d.ts +2 -1
  26. package/dist/typings/index.d.ts +2 -1
  27. package/dist/typings/utils.d.ts +6 -0
  28. package/dist/typings/version.d.ts +1 -1
  29. package/package.json +22 -19
  30. package/src/Auth0Client.ts +126 -11
  31. package/src/Auth0Client.utils.ts +4 -2
  32. package/src/TokenExchange.ts +3 -2
  33. package/src/api.ts +17 -3
  34. package/src/cache/shared.ts +1 -0
  35. package/src/dpop/dpop.ts +56 -0
  36. package/src/dpop/storage.ts +134 -0
  37. package/src/dpop/utils.ts +66 -0
  38. package/src/errors.ts +11 -0
  39. package/src/fetcher.ts +224 -0
  40. package/src/global.ts +23 -0
  41. package/src/http.ts +70 -5
  42. package/src/index.ts +4 -1
  43. package/src/utils.ts +15 -0
  44. package/src/version.ts +1 -1
  45. package/src/worker/token.worker.ts +11 -5
@@ -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 { id_token, access_token, oauthTokenScope, expires_in } =
720
- authResult;
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
- const codeResult = await runIframe(url, this.domainUrl, authorizeTimeout);
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 } = entry as CacheEntry;
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, as determined by the SDK's authorization configuration.
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: ['openid', 'profile']
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
- * console.log('Token response:', tokenResponse);
1220
+ * // Use tokenResponse.access_token, tokenResponse.id_token, etc.
1190
1221
  * } catch (error) {
1191
- * console.error('Token exchange failed:', error);
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
  }
@@ -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
 
@@ -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: string;
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(options)
20
- : JSON.stringify(options);
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
  }
@@ -73,6 +73,7 @@ export interface IdTokenEntry {
73
73
 
74
74
  export type CacheEntry = {
75
75
  id_token?: string;
76
+ token_type?: string;
76
77
  access_token: string;
77
78
  expires_in: number;
78
79
  decodedToken?: DecodedToken;
@@ -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