@auth0/auth0-spa-js 2.18.3 → 2.19.1
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 +427 -370
- 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 +132 -81
- 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 +449 -393
- package/dist/lib/auth0-spa-js.cjs.js.map +1 -1
- package/dist/typings/Auth0Client.d.ts +476 -439
- package/dist/typings/Auth0Client.utils.d.ts +90 -90
- package/dist/typings/MyAccountApiClient.d.ts +92 -92
- package/dist/typings/TokenExchange.d.ts +77 -77
- package/dist/typings/api.d.ts +33 -2
- package/dist/typings/cache/cache-localstorage.d.ts +7 -7
- package/dist/typings/cache/cache-manager.d.ts +69 -56
- package/dist/typings/cache/cache-memory.d.ts +4 -4
- package/dist/typings/cache/index.d.ts +4 -4
- package/dist/typings/cache/key-manifest.d.ts +12 -12
- package/dist/typings/cache/shared.d.ts +68 -68
- package/dist/typings/constants.d.ts +58 -58
- package/dist/typings/dpop/dpop.d.ts +17 -17
- package/dist/typings/dpop/storage.d.ts +27 -27
- package/dist/typings/dpop/utils.d.ts +15 -15
- package/dist/typings/errors.d.ts +96 -96
- package/dist/typings/fetcher.d.ts +54 -54
- package/dist/typings/global.d.ts +826 -819
- package/dist/typings/http.d.ts +11 -5
- package/dist/typings/index.d.ts +24 -24
- package/dist/typings/jwt.d.ts +21 -21
- package/dist/typings/lock.d.ts +32 -32
- package/dist/typings/mfa/MfaApiClient.d.ts +225 -225
- package/dist/typings/mfa/MfaContextManager.d.ts +79 -79
- package/dist/typings/mfa/constants.d.ts +23 -23
- package/dist/typings/mfa/errors.d.ts +117 -117
- package/dist/typings/mfa/index.d.ts +4 -4
- package/dist/typings/mfa/types.d.ts +181 -181
- package/dist/typings/mfa/utils.d.ts +23 -23
- package/dist/typings/promise-utils.d.ts +2 -2
- package/dist/typings/scope.d.ts +35 -35
- package/dist/typings/storage.d.ts +26 -26
- package/dist/typings/transaction-manager.d.ts +33 -33
- package/dist/typings/utils.d.ts +36 -36
- package/dist/typings/version.d.ts +2 -2
- package/dist/typings/worker/token.worker.d.ts +1 -1
- package/dist/typings/worker/worker.types.d.ts +27 -20
- package/dist/typings/worker/worker.utils.d.ts +13 -7
- package/package.json +2 -2
- package/src/Auth0Client.ts +73 -2
- package/src/api.ts +116 -2
- package/src/cache/cache-manager.ts +85 -9
- package/src/global.ts +8 -0
- package/src/http.ts +28 -21
- package/src/version.ts +1 -1
- package/src/worker/token.worker.ts +120 -5
- package/src/worker/worker.types.ts +17 -6
- package/src/worker/worker.utils.ts +18 -7
package/dist/typings/utils.d.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { AuthenticationResult, PopupConfigOptions } from './global';
|
|
2
|
-
export declare const parseAuthenticationResult: (queryString: string) => AuthenticationResult;
|
|
3
|
-
export declare const runIframe: (authorizeUrl: string, eventOrigin: string, timeoutInSeconds?: number) => Promise<AuthenticationResult>;
|
|
4
|
-
export declare const openPopup: (url: string) => Window | null;
|
|
5
|
-
export declare const runPopup: (config: PopupConfigOptions, eventOrigin: string) => Promise<AuthenticationResult>;
|
|
6
|
-
export declare const getCrypto: () => Crypto;
|
|
7
|
-
export declare const createRandomString: () => string;
|
|
8
|
-
export declare const encode: (value: string) => string;
|
|
9
|
-
export declare const decode: (value: string) => string;
|
|
10
|
-
/**
|
|
11
|
-
* Strips any property that is not present in ALLOWED_AUTH0CLIENT_PROPERTIES
|
|
12
|
-
* @param auth0Client - The full auth0Client object
|
|
13
|
-
* @param excludeEnv - If true, excludes the 'env' property from the result
|
|
14
|
-
* @returns The stripped auth0Client object
|
|
15
|
-
*/
|
|
16
|
-
export declare const stripAuth0Client: (auth0Client: any, excludeEnv?: boolean) => any;
|
|
17
|
-
export declare const createQueryParams: ({ clientId: client_id, ...params }: any) => string;
|
|
18
|
-
export declare const sha256: (s: string) => Promise<any>;
|
|
19
|
-
export declare const urlDecodeB64: (input: string) => string;
|
|
20
|
-
export declare const bufferToBase64UrlEncoded: (input: number[] | Uint8Array) => string;
|
|
21
|
-
export declare const validateCrypto: () => void;
|
|
22
|
-
/**
|
|
23
|
-
* @ignore
|
|
24
|
-
*/
|
|
25
|
-
export declare const getDomain: (domainUrl: string) => string;
|
|
26
|
-
/**
|
|
27
|
-
* @ignore
|
|
28
|
-
*/
|
|
29
|
-
export declare const getTokenIssuer: (issuer: string | undefined, domainUrl: string) => string;
|
|
30
|
-
export declare const parseNumber: (value: any) => number | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* Ponyfill for `Object.fromEntries()`, which is not available until ES2020.
|
|
33
|
-
*
|
|
34
|
-
* When the target of this project reaches ES2020, this can be removed.
|
|
35
|
-
*/
|
|
36
|
-
export declare const fromEntries: <T = any>(iterable: Iterable<[PropertyKey, T]>) => Record<PropertyKey, T>;
|
|
1
|
+
import { AuthenticationResult, PopupConfigOptions } from './global';
|
|
2
|
+
export declare const parseAuthenticationResult: (queryString: string) => AuthenticationResult;
|
|
3
|
+
export declare const runIframe: (authorizeUrl: string, eventOrigin: string, timeoutInSeconds?: number) => Promise<AuthenticationResult>;
|
|
4
|
+
export declare const openPopup: (url: string) => Window | null;
|
|
5
|
+
export declare const runPopup: (config: PopupConfigOptions, eventOrigin: string) => Promise<AuthenticationResult>;
|
|
6
|
+
export declare const getCrypto: () => Crypto;
|
|
7
|
+
export declare const createRandomString: () => string;
|
|
8
|
+
export declare const encode: (value: string) => string;
|
|
9
|
+
export declare const decode: (value: string) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Strips any property that is not present in ALLOWED_AUTH0CLIENT_PROPERTIES
|
|
12
|
+
* @param auth0Client - The full auth0Client object
|
|
13
|
+
* @param excludeEnv - If true, excludes the 'env' property from the result
|
|
14
|
+
* @returns The stripped auth0Client object
|
|
15
|
+
*/
|
|
16
|
+
export declare const stripAuth0Client: (auth0Client: any, excludeEnv?: boolean) => any;
|
|
17
|
+
export declare const createQueryParams: ({ clientId: client_id, ...params }: any) => string;
|
|
18
|
+
export declare const sha256: (s: string) => Promise<any>;
|
|
19
|
+
export declare const urlDecodeB64: (input: string) => string;
|
|
20
|
+
export declare const bufferToBase64UrlEncoded: (input: number[] | Uint8Array) => string;
|
|
21
|
+
export declare const validateCrypto: () => void;
|
|
22
|
+
/**
|
|
23
|
+
* @ignore
|
|
24
|
+
*/
|
|
25
|
+
export declare const getDomain: (domainUrl: string) => string;
|
|
26
|
+
/**
|
|
27
|
+
* @ignore
|
|
28
|
+
*/
|
|
29
|
+
export declare const getTokenIssuer: (issuer: string | undefined, domainUrl: string) => string;
|
|
30
|
+
export declare const parseNumber: (value: any) => number | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Ponyfill for `Object.fromEntries()`, which is not available until ES2020.
|
|
33
|
+
*
|
|
34
|
+
* When the target of this project reaches ES2020, this can be removed.
|
|
35
|
+
*/
|
|
36
|
+
export declare const fromEntries: <T = any>(iterable: Iterable<[PropertyKey, T]>) => Record<PropertyKey, T>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "2.
|
|
2
|
-
export default _default;
|
|
1
|
+
declare const _default: "2.19.1";
|
|
2
|
+
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export {};
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import { FetchOptions } from '../global';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
export type
|
|
1
|
+
import { FetchOptions } from '../global';
|
|
2
|
+
export type WorkerInitMessage = {
|
|
3
|
+
type: 'init';
|
|
4
|
+
allowedBaseUrl: string;
|
|
5
|
+
};
|
|
6
|
+
type WorkerTokenMessage = {
|
|
7
|
+
timeout: number;
|
|
8
|
+
fetchUrl: string;
|
|
9
|
+
fetchOptions: FetchOptions;
|
|
10
|
+
useFormData?: boolean;
|
|
11
|
+
auth: {
|
|
12
|
+
audience: string;
|
|
13
|
+
scope: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type WorkerRefreshTokenMessage = WorkerTokenMessage & {
|
|
17
|
+
type: 'refresh';
|
|
18
|
+
useMrrt?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type WorkerRevokeTokenMessage = Omit<WorkerTokenMessage, 'auth'> & {
|
|
21
|
+
type: 'revoke';
|
|
22
|
+
auth: {
|
|
23
|
+
audience: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export type WorkerMessage = WorkerInitMessage | WorkerRefreshTokenMessage | WorkerRevokeTokenMessage;
|
|
27
|
+
export {};
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { WorkerRefreshTokenMessage } from './worker.types';
|
|
2
|
-
/**
|
|
3
|
-
* Sends
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { WorkerRefreshTokenMessage, WorkerRevokeTokenMessage } from './worker.types';
|
|
2
|
+
/**
|
|
3
|
+
* Sends a message to a Web Worker and returns a Promise that resolves with
|
|
4
|
+
* the worker's response, or rejects if the worker replies with an error.
|
|
5
|
+
*
|
|
6
|
+
* Uses a {@link MessageChannel} so each call gets its own private reply port,
|
|
7
|
+
* making concurrent calls safe without shared state.
|
|
8
|
+
*
|
|
9
|
+
* @param message - The typed message to send (`refresh` or `revoke`).
|
|
10
|
+
* @param to - The target {@link Worker} instance.
|
|
11
|
+
* @returns A Promise that resolves with the worker's response payload.
|
|
12
|
+
*/
|
|
13
|
+
export declare const sendMessage: <T = any>(message: WorkerRefreshTokenMessage | WorkerRevokeTokenMessage, to: Worker) => Promise<T>;
|
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.19.1",
|
|
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",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"rollup-plugin-livereload": "^2.0.5",
|
|
94
94
|
"rollup-plugin-sourcemaps": "^0.6.3",
|
|
95
95
|
"rollup-plugin-terser": "^7.0.2",
|
|
96
|
-
"rollup-plugin-typescript2": "^0.
|
|
96
|
+
"rollup-plugin-typescript2": "^0.37.0",
|
|
97
97
|
"rollup-plugin-visualizer": "^5.7.1",
|
|
98
98
|
"rollup-plugin-web-worker-loader": "~1.6.1",
|
|
99
99
|
"serve": "^14.0.1",
|
package/src/Auth0Client.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
|
|
18
18
|
import { getLockManager, type ILockManager } from './lock';
|
|
19
19
|
|
|
20
|
-
import { oauthToken } from './api';
|
|
20
|
+
import { oauthToken, revokeToken } from './api';
|
|
21
21
|
|
|
22
22
|
import { injectDefaultScopes, scopesToRequest } from './scope';
|
|
23
23
|
|
|
@@ -90,7 +90,8 @@ import {
|
|
|
90
90
|
RedirectConnectAccountOptions,
|
|
91
91
|
ResponseType,
|
|
92
92
|
ClientAuthorizationParams,
|
|
93
|
-
ClientConfiguration
|
|
93
|
+
ClientConfiguration,
|
|
94
|
+
RevokeRefreshTokenOptions
|
|
94
95
|
} from './global';
|
|
95
96
|
|
|
96
97
|
// @ts-ignore
|
|
@@ -1144,6 +1145,76 @@ export class Auth0Client {
|
|
|
1144
1145
|
return url + federatedQuery;
|
|
1145
1146
|
}
|
|
1146
1147
|
|
|
1148
|
+
/**
|
|
1149
|
+
* ```js
|
|
1150
|
+
* await auth0.revokeRefreshToken();
|
|
1151
|
+
* ```
|
|
1152
|
+
*
|
|
1153
|
+
* Revokes the refresh token using the `/oauth/revoke` endpoint.
|
|
1154
|
+
* This invalidates the refresh token so it can no longer be used to obtain new access tokens.
|
|
1155
|
+
*
|
|
1156
|
+
* The method works with both memory and localStorage cache modes:
|
|
1157
|
+
* - For memory storage with worker: The refresh token never leaves the worker thread,
|
|
1158
|
+
* maintaining security isolation
|
|
1159
|
+
* - For localStorage: The token is retrieved from cache and revoked
|
|
1160
|
+
*
|
|
1161
|
+
* If `useRefreshTokens` is disabled, this method does nothing.
|
|
1162
|
+
*
|
|
1163
|
+
* **Important:** This method revokes the refresh token for a single audience. If your
|
|
1164
|
+
* application requests tokens for multiple audiences, each audience may have its own
|
|
1165
|
+
* refresh token. To fully revoke all refresh tokens, call this method once per audience.
|
|
1166
|
+
* If you want to terminate the user's session entirely, use `logout()` instead.
|
|
1167
|
+
*
|
|
1168
|
+
* When using Multi-Resource Refresh Tokens (MRRT), a single refresh token may cover
|
|
1169
|
+
* multiple audiences. In that case, revoking it will affect all cache entries that
|
|
1170
|
+
* share the same token.
|
|
1171
|
+
*
|
|
1172
|
+
* @param options - Optional parameters to identify which refresh token to revoke.
|
|
1173
|
+
* Defaults to the audience configured in `authorizationParams`.
|
|
1174
|
+
*
|
|
1175
|
+
* @example
|
|
1176
|
+
* // Revoke the default refresh token
|
|
1177
|
+
* await auth0.revokeRefreshToken();
|
|
1178
|
+
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* // Revoke refresh tokens for each audience individually
|
|
1181
|
+
* await auth0.revokeRefreshToken({ audience: 'https://api.example.com' });
|
|
1182
|
+
* await auth0.revokeRefreshToken({ audience: 'https://api2.example.com' });
|
|
1183
|
+
*/
|
|
1184
|
+
public async revokeRefreshToken(options: RevokeRefreshTokenOptions = {}): Promise<void> {
|
|
1185
|
+
if (!this.options.useRefreshTokens) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const audience =
|
|
1190
|
+
options.audience || this.options.authorizationParams.audience;
|
|
1191
|
+
|
|
1192
|
+
const resolvedAudience = audience || DEFAULT_AUDIENCE;
|
|
1193
|
+
|
|
1194
|
+
// For the non-worker path the main-thread cache holds the refresh tokens.
|
|
1195
|
+
// For the worker path the worker holds its own RT store — the cache returns
|
|
1196
|
+
// [] and revokeToken sends a single message; the worker loops internally.
|
|
1197
|
+
const refreshTokens = await this.cacheManager.getRefreshTokensByAudience(
|
|
1198
|
+
resolvedAudience,
|
|
1199
|
+
this.options.clientId
|
|
1200
|
+
);
|
|
1201
|
+
|
|
1202
|
+
await revokeToken(
|
|
1203
|
+
{
|
|
1204
|
+
baseUrl: this.domainUrl,
|
|
1205
|
+
timeout: this.httpTimeoutMs,
|
|
1206
|
+
auth0Client: this.options.auth0Client,
|
|
1207
|
+
useFormData: this.options.useFormData,
|
|
1208
|
+
client_id: this.options.clientId,
|
|
1209
|
+
refreshTokens,
|
|
1210
|
+
audience: resolvedAudience,
|
|
1211
|
+
onRefreshTokenRevoked: refreshToken =>
|
|
1212
|
+
this.cacheManager.stripRefreshToken(refreshToken)
|
|
1213
|
+
},
|
|
1214
|
+
this.worker
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1147
1218
|
/**
|
|
1148
1219
|
* ```js
|
|
1149
1220
|
* await auth0.logout(options);
|
package/src/api.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import { TokenEndpointOptions, TokenEndpointResponse } from './global';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_AUTH0_CLIENT,
|
|
4
|
+
DEFAULT_AUDIENCE,
|
|
5
|
+
DEFAULT_FETCH_TIMEOUT_MS
|
|
6
|
+
} from './constants';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @ignore
|
|
10
|
+
* Internal options for the revokeToken API call.
|
|
11
|
+
* Kept in api.ts (not global.ts) so it is not part of the public type surface.
|
|
12
|
+
*/
|
|
13
|
+
interface RevokeTokenOptions {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
/** Maps directly to the OAuth `client_id` parameter. */
|
|
16
|
+
client_id: string;
|
|
17
|
+
/** Tokens to revoke. Empty for the worker path — the worker holds its own store. */
|
|
18
|
+
refreshTokens: string[];
|
|
19
|
+
audience?: string;
|
|
20
|
+
timeout?: number;
|
|
21
|
+
auth0Client?: any;
|
|
22
|
+
useFormData?: boolean;
|
|
23
|
+
onRefreshTokenRevoked?: (refreshToken: string) => Promise<void> | void;
|
|
24
|
+
}
|
|
3
25
|
import * as dpopUtils from './dpop/utils';
|
|
4
|
-
import {
|
|
26
|
+
import { GenericError } from './errors';
|
|
27
|
+
import { getJSON, fetchWithTimeout } from './http';
|
|
28
|
+
import { sendMessage } from './worker/worker.utils';
|
|
5
29
|
import { createQueryParams, stripAuth0Client } from './utils';
|
|
6
30
|
|
|
7
31
|
export async function oauthToken(
|
|
@@ -59,3 +83,93 @@ export async function oauthToken(
|
|
|
59
83
|
isDpopSupported ? dpop : undefined
|
|
60
84
|
);
|
|
61
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Revokes refresh tokens using the /oauth/revoke endpoint.
|
|
89
|
+
*
|
|
90
|
+
* Mirrors the oauthToken pattern: the worker/non-worker dispatch lives here,
|
|
91
|
+
* keeping Auth0Client free of transport concerns.
|
|
92
|
+
*
|
|
93
|
+
* - Worker path: sends a single message; the worker holds its own RT store and
|
|
94
|
+
* loops internally. refreshTokens is empty (worker ignores it).
|
|
95
|
+
* - Non-worker path: loops over refreshTokens and issues one request per token.
|
|
96
|
+
*
|
|
97
|
+
* @throws {GenericError} If any revoke request fails
|
|
98
|
+
*/
|
|
99
|
+
export async function revokeToken(
|
|
100
|
+
{
|
|
101
|
+
baseUrl,
|
|
102
|
+
timeout,
|
|
103
|
+
auth0Client,
|
|
104
|
+
useFormData,
|
|
105
|
+
refreshTokens,
|
|
106
|
+
audience,
|
|
107
|
+
client_id,
|
|
108
|
+
onRefreshTokenRevoked
|
|
109
|
+
}: RevokeTokenOptions,
|
|
110
|
+
worker?: Worker
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
const resolvedTimeout = timeout || DEFAULT_FETCH_TIMEOUT_MS;
|
|
113
|
+
// token_type_hint is a SHOULD per RFC 7009 §2.1; used in both paths below.
|
|
114
|
+
const token_type_hint = 'refresh_token' as const;
|
|
115
|
+
const fetchUrl = `${baseUrl}/oauth/revoke`;
|
|
116
|
+
const headers = {
|
|
117
|
+
'Content-Type': useFormData
|
|
118
|
+
? 'application/x-www-form-urlencoded'
|
|
119
|
+
: 'application/json',
|
|
120
|
+
'Auth0-Client': btoa(
|
|
121
|
+
JSON.stringify(stripAuth0Client(auth0Client || DEFAULT_AUTH0_CLIENT))
|
|
122
|
+
)
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (worker) {
|
|
126
|
+
// Worker holds its own RT store and injects each token into the request.
|
|
127
|
+
// Send the base body (without token) so the worker can loop over its tokens.
|
|
128
|
+
const baseParams = { client_id, token_type_hint };
|
|
129
|
+
const body = useFormData
|
|
130
|
+
? createQueryParams(baseParams)
|
|
131
|
+
: JSON.stringify(baseParams);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
return await sendMessage(
|
|
135
|
+
{
|
|
136
|
+
type: 'revoke',
|
|
137
|
+
timeout: resolvedTimeout,
|
|
138
|
+
fetchUrl,
|
|
139
|
+
fetchOptions: { method: 'POST', body, headers },
|
|
140
|
+
useFormData,
|
|
141
|
+
auth: { audience: audience ?? DEFAULT_AUDIENCE }
|
|
142
|
+
},
|
|
143
|
+
worker
|
|
144
|
+
);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
throw new GenericError('revoke_error', (e as Error).message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const refreshToken of refreshTokens) {
|
|
151
|
+
const params = { client_id, token_type_hint, token: refreshToken };
|
|
152
|
+
const body = useFormData
|
|
153
|
+
? createQueryParams(params)
|
|
154
|
+
: JSON.stringify(params);
|
|
155
|
+
|
|
156
|
+
const response = await fetchWithTimeout(
|
|
157
|
+
fetchUrl,
|
|
158
|
+
{ method: 'POST', body, headers },
|
|
159
|
+
resolvedTimeout
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
let error: string | undefined;
|
|
164
|
+
let errorDescription: string | undefined;
|
|
165
|
+
try {
|
|
166
|
+
({ error, error_description: errorDescription } = JSON.parse(await response.text()));
|
|
167
|
+
} catch {
|
|
168
|
+
// body absent or not valid JSON
|
|
169
|
+
}
|
|
170
|
+
throw new GenericError(error || 'revoke_error', errorDescription || `HTTP error ${response.status}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await onRefreshTokenRevoked?.(refreshToken);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -77,6 +77,11 @@ export class CacheManager {
|
|
|
77
77
|
cacheKey.toKey()
|
|
78
78
|
);
|
|
79
79
|
|
|
80
|
+
// Track the key where the entry was actually found, so that
|
|
81
|
+
// expiry-related writes (strip / remove) target the correct entry
|
|
82
|
+
// instead of creating a ghost entry under the lookup key.
|
|
83
|
+
let resolvedCacheKey = cacheKey;
|
|
84
|
+
|
|
80
85
|
if (!wrappedEntry) {
|
|
81
86
|
const keys = await this.getCacheKeys();
|
|
82
87
|
|
|
@@ -86,6 +91,7 @@ export class CacheManager {
|
|
|
86
91
|
|
|
87
92
|
if (matchedKey) {
|
|
88
93
|
wrappedEntry = await this.cache.get<WrappedCacheEntry>(matchedKey);
|
|
94
|
+
resolvedCacheKey = CacheKey.fromKey(matchedKey);
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
// To refresh using MRRT we need to send a request to the server
|
|
@@ -106,11 +112,11 @@ export class CacheManager {
|
|
|
106
112
|
|
|
107
113
|
if (wrappedEntry.expiresAt - expiryAdjustmentSeconds < nowSeconds) {
|
|
108
114
|
if (wrappedEntry.body.refresh_token) {
|
|
109
|
-
return this.modifiedCachedEntry(wrappedEntry,
|
|
115
|
+
return this.modifiedCachedEntry(wrappedEntry, resolvedCacheKey);
|
|
110
116
|
}
|
|
111
117
|
|
|
112
|
-
await this.cache.remove(
|
|
113
|
-
await this.keyManifest?.remove(
|
|
118
|
+
await this.cache.remove(resolvedCacheKey.toKey());
|
|
119
|
+
await this.keyManifest?.remove(resolvedCacheKey.toKey());
|
|
114
120
|
|
|
115
121
|
return;
|
|
116
122
|
}
|
|
@@ -121,18 +127,27 @@ export class CacheManager {
|
|
|
121
127
|
private async modifiedCachedEntry(wrappedEntry: WrappedCacheEntry, cacheKey: CacheKey): Promise<Partial<CacheEntry>> {
|
|
122
128
|
// We need to keep audience and scope in order to check them later when doing refresh
|
|
123
129
|
// using MRRT. See getScopeToRequest method.
|
|
124
|
-
|
|
130
|
+
//
|
|
131
|
+
// Build a new object instead of mutating wrappedEntry.body in-place,
|
|
132
|
+
// because InMemoryCache returns direct references — mutating would
|
|
133
|
+
// corrupt the original entry stored under a different (superset) key.
|
|
134
|
+
const strippedBody: Partial<CacheEntry> = {
|
|
125
135
|
refresh_token: wrappedEntry.body.refresh_token,
|
|
126
136
|
audience: wrappedEntry.body.audience,
|
|
127
137
|
scope: wrappedEntry.body.scope,
|
|
128
138
|
};
|
|
129
139
|
|
|
130
|
-
|
|
140
|
+
const strippedEntry: WrappedCacheEntry = {
|
|
141
|
+
body: strippedBody,
|
|
142
|
+
expiresAt: wrappedEntry.expiresAt,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
await this.cache.set(cacheKey.toKey(), strippedEntry);
|
|
131
146
|
|
|
132
147
|
return {
|
|
133
|
-
refresh_token:
|
|
134
|
-
audience:
|
|
135
|
-
scope:
|
|
148
|
+
refresh_token: strippedBody.refresh_token,
|
|
149
|
+
audience: strippedBody.audience,
|
|
150
|
+
scope: strippedBody.scope,
|
|
136
151
|
};
|
|
137
152
|
}
|
|
138
153
|
|
|
@@ -163,6 +178,23 @@ export class CacheManager {
|
|
|
163
178
|
await this.cache.remove(cacheKey.toKey());
|
|
164
179
|
}
|
|
165
180
|
|
|
181
|
+
async stripRefreshToken(refreshToken: string): Promise<void> {
|
|
182
|
+
const keys = await this.getCacheKeys();
|
|
183
|
+
|
|
184
|
+
/* c8 ignore next */
|
|
185
|
+
if (!keys) return;
|
|
186
|
+
|
|
187
|
+
// Find all cache entries that have this refresh token and strip only the refresh token,
|
|
188
|
+
// leaving the access token intact (it remains valid until it expires)
|
|
189
|
+
for (const key of keys) {
|
|
190
|
+
const entry = await this.cache.get<WrappedCacheEntry>(key);
|
|
191
|
+
if (entry?.body?.refresh_token === refreshToken) {
|
|
192
|
+
delete entry.body.refresh_token;
|
|
193
|
+
await this.cache.set(key, entry);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
166
198
|
async clear(clientId?: string): Promise<void> {
|
|
167
199
|
const keys = await this.getCacheKeys();
|
|
168
200
|
|
|
@@ -261,7 +293,11 @@ export class CacheManager {
|
|
|
261
293
|
const cachedEntry = await this.cache.get<WrappedCacheEntry>(key);
|
|
262
294
|
|
|
263
295
|
if (cachedEntry?.body?.refresh_token) {
|
|
264
|
-
return
|
|
296
|
+
return {
|
|
297
|
+
refresh_token: cachedEntry.body.refresh_token,
|
|
298
|
+
audience: cachedEntry.body.audience,
|
|
299
|
+
scope: cachedEntry.body.scope,
|
|
300
|
+
};
|
|
265
301
|
}
|
|
266
302
|
}
|
|
267
303
|
}
|
|
@@ -269,6 +305,46 @@ export class CacheManager {
|
|
|
269
305
|
return undefined;
|
|
270
306
|
}
|
|
271
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Returns all distinct refresh tokens stored for a given audience and client.
|
|
310
|
+
*
|
|
311
|
+
* Multiple cache entries may exist for the same audience when different scope
|
|
312
|
+
* combinations were obtained via separate authorization flows, each potentially
|
|
313
|
+
* carrying a different refresh token. A Set is used to deduplicate tokens that
|
|
314
|
+
* are shared across entries (e.g. MRRT).
|
|
315
|
+
*
|
|
316
|
+
* @param audience The audience to look up
|
|
317
|
+
* @param clientId The client id to scope the lookup
|
|
318
|
+
*/
|
|
319
|
+
async getRefreshTokensByAudience(
|
|
320
|
+
audience: string,
|
|
321
|
+
clientId: string
|
|
322
|
+
): Promise<string[]> {
|
|
323
|
+
const keys = await this.getCacheKeys();
|
|
324
|
+
|
|
325
|
+
if (!keys) return [];
|
|
326
|
+
|
|
327
|
+
const tokens = new Set<string>();
|
|
328
|
+
|
|
329
|
+
for (const key of keys) {
|
|
330
|
+
const cacheKey = CacheKey.fromKey(key);
|
|
331
|
+
|
|
332
|
+
if (
|
|
333
|
+
cacheKey.prefix === CACHE_KEY_PREFIX &&
|
|
334
|
+
cacheKey.clientId === clientId &&
|
|
335
|
+
cacheKey.audience === audience
|
|
336
|
+
) {
|
|
337
|
+
const entry = await this.cache.get<WrappedCacheEntry>(key);
|
|
338
|
+
|
|
339
|
+
if (entry?.body?.refresh_token) {
|
|
340
|
+
tokens.add(entry.body.refresh_token);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return Array.from(tokens);
|
|
346
|
+
}
|
|
347
|
+
|
|
272
348
|
/**
|
|
273
349
|
* Updates the refresh token in all cache entries that contain the old refresh token.
|
|
274
350
|
*
|
package/src/global.ts
CHANGED
|
@@ -903,6 +903,14 @@ export type GetTokenSilentlyVerboseResponse = Omit<
|
|
|
903
903
|
'refresh_token'
|
|
904
904
|
>;
|
|
905
905
|
|
|
906
|
+
/**
|
|
907
|
+
* Options for revoking a refresh token
|
|
908
|
+
*/
|
|
909
|
+
export interface RevokeRefreshTokenOptions {
|
|
910
|
+
/** Audience to identify which refresh token to revoke. Omit for default audience. */
|
|
911
|
+
audience?: string;
|
|
912
|
+
}
|
|
913
|
+
|
|
906
914
|
// MFA API types
|
|
907
915
|
export type {
|
|
908
916
|
Authenticator,
|
package/src/http.ts
CHANGED
|
@@ -17,27 +17,16 @@ import { DPOP_NONCE_HEADER } from './dpop/utils';
|
|
|
17
17
|
|
|
18
18
|
export const createAbortController = () => new AbortController();
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* This is not needed, but do it anyway so the object shape is the
|
|
29
|
-
* same as when using a Web Worker (which *does* need this, see
|
|
30
|
-
* src/worker/token.worker.ts).
|
|
31
|
-
*/
|
|
32
|
-
headers: fromEntries(response.headers)
|
|
33
|
-
};
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const fetchWithoutWorker = async (
|
|
20
|
+
/**
|
|
21
|
+
* Wraps a single `fetch` call with an AbortController-based timeout and
|
|
22
|
+
* returns the raw `Response`. Shared by the JSON token path and the revoke
|
|
23
|
+
* path to avoid duplicating abort/timeout orchestration.
|
|
24
|
+
*/
|
|
25
|
+
export const fetchWithTimeout = (
|
|
37
26
|
fetchUrl: string,
|
|
38
27
|
fetchOptions: FetchOptions,
|
|
39
28
|
timeout: number
|
|
40
|
-
) => {
|
|
29
|
+
): Promise<Response> => {
|
|
41
30
|
const controller = createAbortController();
|
|
42
31
|
fetchOptions.signal = controller.signal;
|
|
43
32
|
|
|
@@ -45,9 +34,8 @@ const fetchWithoutWorker = async (
|
|
|
45
34
|
|
|
46
35
|
// The promise will resolve with one of these two promises (the fetch or the timeout), whichever completes first.
|
|
47
36
|
return Promise.race([
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
new Promise((_, reject) => {
|
|
37
|
+
fetch(fetchUrl, fetchOptions),
|
|
38
|
+
new Promise<never>((_, reject) => {
|
|
51
39
|
timeoutId = setTimeout(() => {
|
|
52
40
|
controller.abort();
|
|
53
41
|
reject(new Error("Timeout when executing 'fetch'"));
|
|
@@ -58,6 +46,24 @@ const fetchWithoutWorker = async (
|
|
|
58
46
|
});
|
|
59
47
|
};
|
|
60
48
|
|
|
49
|
+
const fetchWithoutWorker = async (
|
|
50
|
+
fetchUrl: string,
|
|
51
|
+
fetchOptions: FetchOptions,
|
|
52
|
+
timeout: number
|
|
53
|
+
) => {
|
|
54
|
+
const response = await fetchWithTimeout(fetchUrl, fetchOptions, timeout);
|
|
55
|
+
return {
|
|
56
|
+
ok: response.ok,
|
|
57
|
+
json: await response.json(),
|
|
58
|
+
/**
|
|
59
|
+
* This is not needed, but do it anyway so the object shape is the
|
|
60
|
+
* same as when using a Web Worker (which *does* need this, see
|
|
61
|
+
* src/worker/token.worker.ts).
|
|
62
|
+
*/
|
|
63
|
+
headers: fromEntries(response.headers)
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
61
67
|
const fetchWithWorker = async (
|
|
62
68
|
fetchUrl: string,
|
|
63
69
|
audience: string,
|
|
@@ -70,6 +76,7 @@ const fetchWithWorker = async (
|
|
|
70
76
|
) => {
|
|
71
77
|
return sendMessage(
|
|
72
78
|
{
|
|
79
|
+
type: 'refresh',
|
|
73
80
|
auth: {
|
|
74
81
|
audience,
|
|
75
82
|
scope
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '2.
|
|
1
|
+
export default '2.19.1';
|