@auth0/auth0-spa-js 2.4.0 → 2.5.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.
@@ -32,3 +32,35 @@ export declare const getAuthorizeParams: (clientOptions: Auth0ClientOptions & {
32
32
  * Function used to provide support for the deprecated onRedirect through openUrl.
33
33
  */
34
34
  export declare const patchOpenUrlWithOnRedirect: <T extends Pick<LogoutOptions, "openUrl" | "onRedirect">>(options: T) => T;
35
+ /**
36
+ * @ignore
37
+ *
38
+ * Checks if all scopes are included inside other array of scopes
39
+ */
40
+ export declare const allScopesAreIncluded: (scopeToInclude?: string, scopes?: string) => boolean;
41
+ /**
42
+ * @ignore
43
+ *
44
+ * For backward compatibility we are going to check if we are going to downscope while doing a refresh request
45
+ * while MRRT is allowed. If the audience is the same for the refresh_token we are going to use and it has
46
+ * lower scopes than the ones originally in the token, we are going to return the scopes that were stored
47
+ * with the refresh_token in the tokenset.
48
+ * @param useMrrt Setting that the user can activate to use MRRT in their requests
49
+ * @param authorizationParams Contains the audience and scope that the user requested to obtain a token
50
+ * @param cachedAudience Audience stored with the refresh_token wich we are going to use in the request
51
+ * @param cachedScope Scope stored with the refresh_token wich we are going to use in the request
52
+ */
53
+ export declare const getScopeToRequest: (useMrrt: boolean | undefined, authorizationParams: {
54
+ audience?: string;
55
+ scope: string;
56
+ }, cachedAudience?: string, cachedScope?: string) => string;
57
+ /**
58
+ * @ignore
59
+ *
60
+ * Checks if the refresh request has been done using MRRT
61
+ * @param cachedAudience Audience from the refresh token used to refresh
62
+ * @param cachedScope Scopes from the refresh token used to refresh
63
+ * @param requestAudience Audience sent to the server
64
+ * @param requestScope Scopes sent to the server
65
+ */
66
+ export declare const isRefreshWithMrrt: (cachedAudience: string | undefined, cachedScope: string | undefined, requestAudience: string | undefined, requestScope: string) => boolean;
@@ -1,2 +1,2 @@
1
1
  import { TokenEndpointOptions, TokenEndpointResponse } from './global';
2
- export declare function oauthToken({ baseUrl, timeout, audience, scope, auth0Client, useFormData, dpop, ...options }: TokenEndpointOptions, worker?: Worker): Promise<TokenEndpointResponse>;
2
+ export declare function oauthToken({ baseUrl, timeout, audience, scope, auth0Client, useFormData, useMrrt, dpop, ...options }: TokenEndpointOptions, worker?: Worker): Promise<TokenEndpointResponse>;
@@ -7,7 +7,8 @@ export declare class CacheManager {
7
7
  constructor(cache: ICache, keyManifest?: CacheKeyManifest | undefined, nowProvider?: () => number | Promise<number>);
8
8
  setIdToken(clientId: string, idToken: string, decodedToken: DecodedToken): Promise<void>;
9
9
  getIdToken(cacheKey: CacheKey): Promise<IdTokenEntry | undefined>;
10
- get(cacheKey: CacheKey, expiryAdjustmentSeconds?: number): Promise<Partial<CacheEntry> | undefined>;
10
+ get(cacheKey: CacheKey, expiryAdjustmentSeconds?: number, useMrrt?: boolean, cacheMode?: string): Promise<Partial<CacheEntry> | undefined>;
11
+ private modifiedCachedEntry;
11
12
  set(entry: CacheEntry): Promise<void>;
12
13
  clear(clientId?: string): Promise<void>;
13
14
  private wrapCacheEntry;
@@ -31,4 +32,20 @@ export declare class CacheManager {
31
32
  * @param allKeys A list of existing cache keys
32
33
  */
33
34
  private matchExistingCacheKey;
35
+ /**
36
+ * Returns the first entry that contains a refresh_token that satisfies the following conditions
37
+ * The keys inside the cache are in the format {prefix}::{clientId}::{audience}::{scope}.
38
+ * - `prefix` is strict equal to Auth0's internally configured `keyPrefix`
39
+ * - `clientId` is strict equal to the `cacheKey.clientId`
40
+ * @param keyToMatch The provided cache key
41
+ * @param allKeys A list of existing cache keys
42
+ */
43
+ private getEntryWithRefreshToken;
44
+ /**
45
+ * Updates in the cache all entries that has a match with previous refresh_token with the
46
+ * new refresh_token obtained from the server
47
+ * @param oldRefreshToken Old refresh_token used on refresh
48
+ * @param newRefreshToken New refresh_token obtained from the server after refresh
49
+ */
50
+ updateEntry(oldRefreshToken: string, newRefreshToken: string): Promise<void>;
34
51
  }
@@ -6,7 +6,11 @@ export type CustomFetchMinimalOutput = {
6
6
  headers: ResponseHeaders;
7
7
  };
8
8
  export type CustomFetchImpl<TOutput extends CustomFetchMinimalOutput> = (req: Request) => Promise<TOutput>;
9
- type AccessTokenFactory = () => Promise<string>;
9
+ export type AuthParams = {
10
+ scope?: string[];
11
+ audience?: string;
12
+ };
13
+ type AccessTokenFactory = (authParams?: AuthParams) => Promise<string>;
10
14
  export type FetcherConfig<TOutput extends CustomFetchMinimalOutput> = {
11
15
  getAccessToken?: AccessTokenFactory;
12
16
  baseUrl?: string;
@@ -15,7 +19,7 @@ export type FetcherConfig<TOutput extends CustomFetchMinimalOutput> = {
15
19
  };
16
20
  export type FetcherHooks = {
17
21
  isDpopEnabled: () => boolean;
18
- getAccessToken: () => Promise<string>;
22
+ getAccessToken: AccessTokenFactory;
19
23
  getDpopNonce: () => Promise<string | undefined>;
20
24
  setDpopNonce: (nonce: string) => Promise<void>;
21
25
  generateDpopProof: (params: {
@@ -34,15 +38,15 @@ export declare class Fetcher<TOutput extends CustomFetchMinimalOutput> {
34
38
  constructor(config: FetcherConfig<TOutput>, hooks: FetcherHooks);
35
39
  protected isAbsoluteUrl(url: string): boolean;
36
40
  protected buildUrl(baseUrl: string | undefined, url: string | undefined): string;
37
- protected getAccessToken(): Promise<string>;
41
+ protected getAccessToken(authParams?: AuthParams): Promise<string>;
38
42
  protected buildBaseRequest(info: RequestInfo | URL, init: RequestInit | undefined): Request;
39
43
  protected setAuthorizationHeader(request: Request, accessToken: string): Promise<void>;
40
44
  protected setDpopProofHeader(request: Request, accessToken: string): Promise<void>;
41
- protected prepareRequest(request: Request): Promise<void>;
45
+ protected prepareRequest(request: Request, authParams?: AuthParams): Promise<void>;
42
46
  protected getHeader(headers: ResponseHeaders, name: string): string;
43
47
  protected hasUseDpopNonceError(response: TOutput): boolean;
44
48
  protected handleResponse(response: TOutput, callbacks: FetchWithAuthCallbacks<TOutput>): Promise<TOutput>;
45
- protected internalFetchWithAuth(info: RequestInfo | URL, init: RequestInit | undefined, callbacks: FetchWithAuthCallbacks<TOutput>): Promise<TOutput>;
46
- fetchWithAuth(info: RequestInfo | URL, init?: RequestInit): Promise<TOutput>;
49
+ protected internalFetchWithAuth(info: RequestInfo | URL, init: RequestInit | undefined, callbacks: FetchWithAuthCallbacks<TOutput>, authParams?: AuthParams): Promise<TOutput>;
50
+ fetchWithAuth(info: RequestInfo | URL, init?: RequestInit, authParams?: AuthParams): Promise<TOutput>;
47
51
  }
48
52
  export {};
@@ -243,6 +243,10 @@ export interface Auth0ClientOptions extends BaseLoginOptions {
243
243
  * **Note**: The worker is only used when `useRefreshTokens: true`, `cacheLocation: 'memory'`, and the `cache` is not custom.
244
244
  */
245
245
  workerUrl?: string;
246
+ /**
247
+ * If `true`, the SDK will allow the refreshing of tokens using MRRT
248
+ */
249
+ useMrrt?: boolean;
246
250
  /**
247
251
  * If `true`, DPoP (OAuth 2.0 Demonstrating Proof of Possession, RFC9449)
248
252
  * will be used to cryptographically bind tokens to this specific browser
@@ -1,5 +1,5 @@
1
1
  import { FetchOptions } from './global';
2
2
  import { Dpop } from './dpop/dpop';
3
3
  export declare const createAbortController: () => AbortController;
4
- export declare const switchFetch: (fetchUrl: string, audience: string, scope: string, fetchOptions: FetchOptions, worker?: Worker, useFormData?: boolean, timeout?: number) => Promise<any>;
5
- export declare function getJSON<T>(url: string, timeout: number | undefined, audience: string, scope: string, options: FetchOptions, worker?: Worker, useFormData?: boolean, dpop?: Pick<Dpop, 'generateProof' | 'getNonce' | 'setNonce'>, isDpopRetry?: boolean): Promise<T>;
4
+ export declare const switchFetch: (fetchUrl: string, audience: string, scope: string, fetchOptions: FetchOptions, worker?: Worker, useFormData?: boolean, timeout?: number, useMrrt?: boolean) => Promise<any>;
5
+ export declare function getJSON<T>(url: string, timeout: number | undefined, audience: string, scope: string, options: FetchOptions, worker?: Worker, useFormData?: boolean, useMrrt?: boolean, dpop?: Pick<Dpop, 'generateProof' | 'getNonce' | 'setNonce'>, isDpopRetry?: boolean): Promise<T>;
@@ -1,2 +1,2 @@
1
- declare const _default: "2.4.0";
1
+ declare const _default: "2.5.0";
2
2
  export default _default;
@@ -7,6 +7,7 @@ export type WorkerRefreshTokenMessage = {
7
7
  fetchUrl: string;
8
8
  fetchOptions: FetchOptions;
9
9
  useFormData?: boolean;
10
+ useMrrt?: boolean;
10
11
  auth: {
11
12
  audience: string;
12
13
  scope: string;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "@auth0/auth0-spa-js",
4
4
  "description": "Auth0 SDK for Single Page Applications using Authorization Code Grant Flow with PKCE",
5
5
  "license": "MIT",
6
- "version": "2.4.0",
6
+ "version": "2.5.0",
7
7
  "main": "dist/lib/auth0-spa-js.cjs.js",
8
8
  "types": "dist/typings/index.d.ts",
9
9
  "module": "dist/auth0-spa-js.production.esm.js",
@@ -22,6 +22,11 @@
22
22
  ]
23
23
  }
24
24
  },
25
+ "dependencies": {
26
+ "browser-tabs-lock": "^1.2.15",
27
+ "dpop": "^2.1.1",
28
+ "es-cookie": "~1.3.2"
29
+ },
25
30
  "scripts": {
26
31
  "dev": "rimraf dist && rollup -c --watch",
27
32
  "start": "npm run dev",
@@ -54,14 +59,11 @@
54
59
  "@types/jest": "^28.1.7",
55
60
  "@typescript-eslint/eslint-plugin-tslint": "^5.33.1",
56
61
  "@typescript-eslint/parser": "^5.33.1",
57
- "browser-tabs-lock": "^1.2.15",
58
62
  "browserstack-cypress-cli": "1.32.8",
59
63
  "cli-table": "^0.3.6",
60
64
  "concurrently": "^7.3.0",
61
- "dpop": "^2.1.1",
62
65
  "cypress": "13.17.0",
63
66
  "es-check": "^7.0.1",
64
- "es-cookie": "~1.3.2",
65
67
  "eslint": "^8.22.0",
66
68
  "eslint-plugin-security": "^1.5.0",
67
69
  "fake-indexeddb": "^6.0.1",
@@ -90,7 +90,10 @@ import {
90
90
  getAuthorizeParams,
91
91
  GET_TOKEN_SILENTLY_LOCK_KEY,
92
92
  OLD_IS_AUTHENTICATED_COOKIE_NAME,
93
- patchOpenUrlWithOnRedirect
93
+ patchOpenUrlWithOnRedirect,
94
+ getScopeToRequest,
95
+ allScopesAreIncluded,
96
+ isRefreshWithMrrt
94
97
  } from './Auth0Client.utils';
95
98
  import { CustomTokenExchangeOptions } from './TokenExchange';
96
99
  import { Dpop } from './dpop/dpop';
@@ -321,8 +324,8 @@ export class Auth0Client {
321
324
  nonce,
322
325
  code_challenge,
323
326
  authorizationParams.redirect_uri ||
324
- this.options.authorizationParams.redirect_uri ||
325
- fallbackRedirectUri,
327
+ this.options.authorizationParams.redirect_uri ||
328
+ fallbackRedirectUri,
326
329
  authorizeOptions?.response_mode,
327
330
  thumbprint
328
331
  );
@@ -596,7 +599,7 @@ export class Auth0Client {
596
599
 
597
600
  try {
598
601
  await this.getTokenSilently(options);
599
- } catch (_) {}
602
+ } catch (_) { }
600
603
  }
601
604
 
602
605
  /**
@@ -689,7 +692,8 @@ export class Auth0Client {
689
692
  const entry = await this._getEntryFromCache({
690
693
  scope: getTokenOptions.authorizationParams.scope,
691
694
  audience: getTokenOptions.authorizationParams.audience || 'default',
692
- clientId: this.options.clientId
695
+ clientId: this.options.clientId,
696
+ cacheMode,
693
697
  });
694
698
 
695
699
  if (entry) {
@@ -789,7 +793,9 @@ export class Auth0Client {
789
793
  scope: localOptions.authorizationParams.scope,
790
794
  audience: localOptions.authorizationParams.audience || 'default',
791
795
  clientId: this.options.clientId
792
- })
796
+ }),
797
+ undefined,
798
+ this.options.useMrrt
793
799
  );
794
800
 
795
801
  return cache!.access_token;
@@ -976,7 +982,9 @@ export class Auth0Client {
976
982
  scope: options.authorizationParams.scope,
977
983
  audience: options.authorizationParams.audience || 'default',
978
984
  clientId: this.options.clientId
979
- })
985
+ }),
986
+ undefined,
987
+ this.options.useMrrt
980
988
  );
981
989
 
982
990
  // If you don't have a refresh token in memory
@@ -1004,6 +1012,13 @@ export class Auth0Client {
1004
1012
  ? options.timeoutInSeconds * 1000
1005
1013
  : null;
1006
1014
 
1015
+ const scopesToRequest = getScopeToRequest(
1016
+ this.options.useMrrt,
1017
+ options.authorizationParams,
1018
+ cache?.audience,
1019
+ cache?.scope,
1020
+ );
1021
+
1007
1022
  try {
1008
1023
  const tokenResult = await this._requestToken({
1009
1024
  ...options.authorizationParams,
@@ -1011,7 +1026,51 @@ export class Auth0Client {
1011
1026
  refresh_token: cache && cache.refresh_token,
1012
1027
  redirect_uri,
1013
1028
  ...(timeout && { timeout })
1014
- });
1029
+ },
1030
+ {
1031
+ scopesToRequest,
1032
+ }
1033
+ );
1034
+
1035
+ // If is refreshed with MRRT, we update all entries that have the old
1036
+ // refresh_token with the new one if the server responded with one
1037
+ if (tokenResult.refresh_token && this.options.useMrrt && cache?.refresh_token) {
1038
+ await this.cacheManager.updateEntry(
1039
+ cache.refresh_token,
1040
+ tokenResult.refresh_token
1041
+ );
1042
+ }
1043
+
1044
+ // Some scopes requested to the server might not be inside the refresh policies
1045
+ // In order to return a token with all requested scopes when using MRRT we should
1046
+ // check if all scopes are returned. If not, we will try to use an iframe to request
1047
+ // a token.
1048
+ if (this.options.useMrrt) {
1049
+ const isRefreshMrrt = isRefreshWithMrrt(
1050
+ cache?.audience,
1051
+ cache?.scope,
1052
+ options.authorizationParams.audience,
1053
+ options.authorizationParams.scope,
1054
+ );
1055
+
1056
+ if (isRefreshMrrt) {
1057
+ const tokenHasAllScopes = allScopesAreIncluded(
1058
+ scopesToRequest,
1059
+ tokenResult.scope,
1060
+ );
1061
+
1062
+ if (!tokenHasAllScopes) {
1063
+ if (this.options.useRefreshTokensFallback) {
1064
+ return await this._getTokenFromIFrame(options);
1065
+ }
1066
+
1067
+ throw new MissingRefreshTokenError(
1068
+ options.authorizationParams.audience || 'default',
1069
+ options.authorizationParams.scope,
1070
+ );
1071
+ }
1072
+ }
1073
+ }
1015
1074
 
1016
1075
  return {
1017
1076
  ...tokenResult,
@@ -1084,11 +1143,13 @@ export class Auth0Client {
1084
1143
  private async _getEntryFromCache({
1085
1144
  scope,
1086
1145
  audience,
1087
- clientId
1146
+ clientId,
1147
+ cacheMode,
1088
1148
  }: {
1089
1149
  scope: string;
1090
1150
  audience: string;
1091
1151
  clientId: string;
1152
+ cacheMode?: string;
1092
1153
  }): Promise<undefined | GetTokenSilentlyVerboseResponse> {
1093
1154
  const entry = await this.cacheManager.get(
1094
1155
  new CacheKey({
@@ -1096,7 +1157,9 @@ export class Auth0Client {
1096
1157
  audience,
1097
1158
  clientId
1098
1159
  }),
1099
- 60 // get a new token if within 60 seconds of expiring
1160
+ 60, // get a new token if within 60 seconds of expiring
1161
+ this.options.useMrrt,
1162
+ cacheMode,
1100
1163
  );
1101
1164
 
1102
1165
  if (entry && entry.access_token) {
@@ -1134,7 +1197,7 @@ export class Auth0Client {
1134
1197
  | TokenExchangeRequestOptions,
1135
1198
  additionalParameters?: RequestTokenAdditionalParameters
1136
1199
  ) {
1137
- const { nonceIn, organization } = additionalParameters || {};
1200
+ const { nonceIn, organization, scopesToRequest } = additionalParameters || {};
1138
1201
  const authResult = await oauthToken(
1139
1202
  {
1140
1203
  baseUrl: this.domainUrl,
@@ -1142,8 +1205,10 @@ export class Auth0Client {
1142
1205
  auth0Client: this.options.auth0Client,
1143
1206
  useFormData: this.options.useFormData,
1144
1207
  timeout: this.httpTimeoutMs,
1208
+ useMrrt: this.options.useMrrt,
1145
1209
  dpop: this.dpop,
1146
- ...options
1210
+ ...options,
1211
+ scope: scopesToRequest || options.scope,
1147
1212
  },
1148
1213
  this.worker
1149
1214
  );
@@ -1298,7 +1363,7 @@ export class Auth0Client {
1298
1363
  * This is a drop-in replacement for the Fetch API's `fetch()` method, but will
1299
1364
  * handle certain authentication logic for you, like building the proper auth
1300
1365
  * headers or managing DPoP nonces and retries automatically.
1301
- *
1366
+ *
1302
1367
  * Check the `EXAMPLES.md` file for a deeper look into this method.
1303
1368
  */
1304
1369
  public createFetcher<TOutput extends CustomFetchMinimalOutput = Response>(
@@ -1312,7 +1377,13 @@ export class Auth0Client {
1312
1377
 
1313
1378
  return new Fetcher(config, {
1314
1379
  isDpopEnabled: () => !!this.options.useDpop,
1315
- getAccessToken: () => this.getTokenSilently(),
1380
+ getAccessToken: authParams =>
1381
+ this.getTokenSilently({
1382
+ authorizationParams: {
1383
+ scope: authParams?.scope?.join(' '),
1384
+ audience: authParams?.audience
1385
+ }
1386
+ }),
1316
1387
  getDpopNonce: () => this.getDpopNonce(config.dpopNonceId),
1317
1388
  setDpopNonce: nonce => this.setDpopNonce(nonce),
1318
1389
  generateDpopProof: params => this.generateDpopProof(params)
@@ -1349,4 +1420,5 @@ interface TokenExchangeRequestOptions extends BaseRequestTokenOptions {
1349
1420
  interface RequestTokenAdditionalParameters {
1350
1421
  nonceIn?: string;
1351
1422
  organization?: string;
1423
+ scopesToRequest?: string;
1352
1424
  }
@@ -96,3 +96,69 @@ export const patchOpenUrlWithOnRedirect = <
96
96
 
97
97
  return result as T;
98
98
  };
99
+
100
+ /**
101
+ * @ignore
102
+ *
103
+ * Checks if all scopes are included inside other array of scopes
104
+ */
105
+ export const allScopesAreIncluded = (scopeToInclude?: string, scopes?: string): boolean => {
106
+ const scopeGroup = scopes?.split(" ") || [];
107
+ const scopesToInclude = scopeToInclude?.split(" ") || [];
108
+ return scopesToInclude.every((key) => scopeGroup.includes(key));
109
+ }
110
+
111
+ /**
112
+ * @ignore
113
+ *
114
+ * For backward compatibility we are going to check if we are going to downscope while doing a refresh request
115
+ * while MRRT is allowed. If the audience is the same for the refresh_token we are going to use and it has
116
+ * lower scopes than the ones originally in the token, we are going to return the scopes that were stored
117
+ * with the refresh_token in the tokenset.
118
+ * @param useMrrt Setting that the user can activate to use MRRT in their requests
119
+ * @param authorizationParams Contains the audience and scope that the user requested to obtain a token
120
+ * @param cachedAudience Audience stored with the refresh_token wich we are going to use in the request
121
+ * @param cachedScope Scope stored with the refresh_token wich we are going to use in the request
122
+ */
123
+ export const getScopeToRequest = (
124
+ useMrrt: boolean | undefined,
125
+ authorizationParams: { audience?: string, scope: string },
126
+ cachedAudience?: string,
127
+ cachedScope?: string
128
+ ): string => {
129
+ if (useMrrt && cachedAudience && cachedScope) {
130
+ if (authorizationParams.audience !== cachedAudience) {
131
+ return authorizationParams.scope;
132
+ }
133
+
134
+ const cachedScopes = cachedScope.split(" ");
135
+ const newScopes = authorizationParams.scope?.split(" ") || [];
136
+ const newScopesAreIncluded = newScopes.every((scope) => cachedScopes.includes(scope));
137
+
138
+ return cachedScopes.length >= newScopes.length && newScopesAreIncluded ? cachedScope : authorizationParams.scope;
139
+ }
140
+
141
+ return authorizationParams.scope;
142
+ }
143
+
144
+ /**
145
+ * @ignore
146
+ *
147
+ * Checks if the refresh request has been done using MRRT
148
+ * @param cachedAudience Audience from the refresh token used to refresh
149
+ * @param cachedScope Scopes from the refresh token used to refresh
150
+ * @param requestAudience Audience sent to the server
151
+ * @param requestScope Scopes sent to the server
152
+ */
153
+ export const isRefreshWithMrrt = (
154
+ cachedAudience: string | undefined,
155
+ cachedScope: string | undefined,
156
+ requestAudience: string | undefined,
157
+ requestScope: string,
158
+ ): boolean => {
159
+ if (cachedAudience !== requestAudience) {
160
+ return true;
161
+ }
162
+
163
+ return !allScopesAreIncluded(requestScope, cachedScope);
164
+ }
package/src/api.ts CHANGED
@@ -12,6 +12,7 @@ export async function oauthToken(
12
12
  scope,
13
13
  auth0Client,
14
14
  useFormData,
15
+ useMrrt,
15
16
  dpop,
16
17
  ...options
17
18
  }: TokenEndpointOptions,
@@ -20,10 +21,14 @@ export async function oauthToken(
20
21
  const isTokenExchange =
21
22
  options.grant_type === 'urn:ietf:params:oauth:grant-type:token-exchange';
22
23
 
24
+ const refreshWithMrrt =
25
+ options.grant_type === 'refresh_token' && useMrrt;
26
+
23
27
  const allParams = {
24
28
  ...options,
25
29
  ...(isTokenExchange && audience && { audience }),
26
- ...(isTokenExchange && scope && { scope })
30
+ ...(isTokenExchange && scope && { scope }),
31
+ ...(refreshWithMrrt && { audience, scope })
27
32
  };
28
33
 
29
34
  const body = useFormData
@@ -51,6 +56,7 @@ export async function oauthToken(
51
56
  },
52
57
  worker,
53
58
  useFormData,
59
+ useMrrt,
54
60
  isDpopSupported ? dpop : undefined
55
61
  );
56
62
  }
@@ -69,7 +69,9 @@ export class CacheManager {
69
69
 
70
70
  async get(
71
71
  cacheKey: CacheKey,
72
- expiryAdjustmentSeconds = DEFAULT_EXPIRY_ADJUSTMENT_SECONDS
72
+ expiryAdjustmentSeconds = DEFAULT_EXPIRY_ADJUSTMENT_SECONDS,
73
+ useMrrt = false,
74
+ cacheMode?: string
73
75
  ): Promise<Partial<CacheEntry> | undefined> {
74
76
  let wrappedEntry = await this.cache.get<WrappedCacheEntry>(
75
77
  cacheKey.toKey()
@@ -85,6 +87,13 @@ export class CacheManager {
85
87
  if (matchedKey) {
86
88
  wrappedEntry = await this.cache.get<WrappedCacheEntry>(matchedKey);
87
89
  }
90
+
91
+ // To refresh using MRRT we need to send a request to the server
92
+ // If cacheMode is 'cache-only', this will make us unable to call the server
93
+ // so it won't be needed to find a valid refresh token
94
+ if (!matchedKey && useMrrt && cacheMode !== 'cache-only') {
95
+ return this.getEntryWithRefreshToken(cacheKey, keys);
96
+ }
88
97
  }
89
98
 
90
99
  // If we still don't have an entry, exit.
@@ -97,12 +106,7 @@ export class CacheManager {
97
106
 
98
107
  if (wrappedEntry.expiresAt - expiryAdjustmentSeconds < nowSeconds) {
99
108
  if (wrappedEntry.body.refresh_token) {
100
- wrappedEntry.body = {
101
- refresh_token: wrappedEntry.body.refresh_token
102
- };
103
-
104
- await this.cache.set(cacheKey.toKey(), wrappedEntry);
105
- return wrappedEntry.body;
109
+ return this.modifiedCachedEntry(wrappedEntry, cacheKey);
106
110
  }
107
111
 
108
112
  await this.cache.remove(cacheKey.toKey());
@@ -114,6 +118,24 @@ export class CacheManager {
114
118
  return wrappedEntry.body;
115
119
  }
116
120
 
121
+ private async modifiedCachedEntry(wrappedEntry: WrappedCacheEntry, cacheKey: CacheKey): Promise<Partial<CacheEntry>> {
122
+ // We need to keep audience and scope in order to check them later when doing refresh
123
+ // using MRRT. See getScopeToRequest method.
124
+ wrappedEntry.body = {
125
+ refresh_token: wrappedEntry.body.refresh_token,
126
+ audience: wrappedEntry.body.audience,
127
+ scope: wrappedEntry.body.scope,
128
+ };
129
+
130
+ await this.cache.set(cacheKey.toKey(), wrappedEntry);
131
+
132
+ return {
133
+ refresh_token: wrappedEntry.body.refresh_token,
134
+ audience: wrappedEntry.body.audience,
135
+ scope: wrappedEntry.body.scope,
136
+ };
137
+ }
138
+
117
139
  async set(entry: CacheEntry): Promise<void> {
118
140
  const cacheKey = new CacheKey({
119
141
  clientId: entry.client_id,
@@ -207,4 +229,57 @@ export class CacheManager {
207
229
  );
208
230
  })[0];
209
231
  }
232
+
233
+ /**
234
+ * Returns the first entry that contains a refresh_token that satisfies the following conditions
235
+ * The keys inside the cache are in the format {prefix}::{clientId}::{audience}::{scope}.
236
+ * - `prefix` is strict equal to Auth0's internally configured `keyPrefix`
237
+ * - `clientId` is strict equal to the `cacheKey.clientId`
238
+ * @param keyToMatch The provided cache key
239
+ * @param allKeys A list of existing cache keys
240
+ */
241
+ private async getEntryWithRefreshToken(keyToMatch: CacheKey, allKeys: Array<string>): Promise<Partial<CacheEntry> | undefined> {
242
+ for (const key of allKeys) {
243
+ const cacheKey = CacheKey.fromKey(key);
244
+
245
+ if (cacheKey.prefix === CACHE_KEY_PREFIX &&
246
+ cacheKey.clientId === keyToMatch.clientId) {
247
+ const cachedEntry = await this.cache.get<WrappedCacheEntry>(key);
248
+
249
+ if (cachedEntry?.body?.refresh_token) {
250
+ return this.modifiedCachedEntry(cachedEntry, keyToMatch);
251
+ }
252
+ }
253
+ }
254
+
255
+ return undefined;
256
+ }
257
+
258
+ /**
259
+ * Updates in the cache all entries that has a match with previous refresh_token with the
260
+ * new refresh_token obtained from the server
261
+ * @param oldRefreshToken Old refresh_token used on refresh
262
+ * @param newRefreshToken New refresh_token obtained from the server after refresh
263
+ */
264
+ async updateEntry(
265
+ oldRefreshToken: string,
266
+ newRefreshToken: string,
267
+ ): Promise<void> {
268
+ const allKeys = await this.getCacheKeys();
269
+
270
+ if (!allKeys) return;
271
+
272
+ for (const key of allKeys) {
273
+ const entry = await this.cache.get<WrappedCacheEntry>(key);
274
+
275
+ if (entry?.body?.refresh_token === oldRefreshToken) {
276
+ const cacheEntry = {
277
+ ...entry.body,
278
+ refresh_token: newRefreshToken,
279
+ } as CacheEntry;
280
+
281
+ await this.set(cacheEntry);
282
+ }
283
+ }
284
+ }
210
285
  }