@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.
- package/README.md +1 -1
- package/dist/auth0-spa-js.development.js +135 -37
- 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 +34 -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 +138 -37
- package/dist/lib/auth0-spa-js.cjs.js.map +1 -1
- package/dist/typings/Auth0Client.utils.d.ts +32 -0
- package/dist/typings/api.d.ts +1 -1
- package/dist/typings/cache/cache-manager.d.ts +18 -1
- package/dist/typings/fetcher.d.ts +10 -6
- package/dist/typings/global.d.ts +4 -0
- package/dist/typings/http.d.ts +2 -2
- package/dist/typings/version.d.ts +1 -1
- package/dist/typings/worker/worker.types.d.ts +1 -0
- package/package.json +6 -4
- package/src/Auth0Client.ts +86 -14
- package/src/Auth0Client.utils.ts +66 -0
- package/src/api.ts +7 -1
- package/src/cache/cache-manager.ts +82 -7
- package/src/fetcher.ts +28 -16
- package/src/global.ts +7 -1
- package/src/http.ts +12 -5
- package/src/version.ts +1 -1
- package/src/worker/token.worker.ts +60 -9
- package/src/worker/worker.types.ts +1 -0
|
@@ -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;
|
package/dist/typings/api.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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 {};
|
package/dist/typings/global.d.ts
CHANGED
|
@@ -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
|
package/dist/typings/http.d.ts
CHANGED
|
@@ -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.
|
|
1
|
+
declare const _default: "2.5.0";
|
|
2
2
|
export default _default;
|
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.
|
|
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",
|
package/src/Auth0Client.ts
CHANGED
|
@@ -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
|
-
|
|
325
|
-
|
|
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:
|
|
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
|
}
|
package/src/Auth0Client.utils.ts
CHANGED
|
@@ -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
|
|
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
|
}
|