@auth0/auth0-spa-js 2.5.0 → 2.7.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 +212 -21
- 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 +1 -1
- 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 +224 -22
- package/dist/lib/auth0-spa-js.cjs.js.map +1 -1
- package/dist/typings/Auth0Client.d.ts +42 -2
- package/dist/typings/Auth0Client.utils.d.ts +6 -0
- package/dist/typings/MyAccountApiClient.d.ts +92 -0
- package/dist/typings/errors.d.ts +18 -0
- package/dist/typings/fetcher.d.ts +5 -3
- package/dist/typings/global.d.ts +93 -0
- package/dist/typings/index.d.ts +2 -1
- package/dist/typings/transaction-manager.d.ts +15 -4
- package/dist/typings/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Auth0Client.ts +202 -22
- package/src/Auth0Client.utils.ts +14 -0
- package/src/MyAccountApiClient.ts +158 -0
- package/src/errors.ts +33 -0
- package/src/fetcher.ts +53 -18
- package/src/global.ts +105 -3
- package/src/index.ts +5 -0
- package/src/transaction-manager.ts +17 -4
- package/src/utils.ts +1 -0
- package/src/version.ts +1 -1
- package/src/worker/token.worker.ts +1 -1
package/src/Auth0Client.ts
CHANGED
|
@@ -31,12 +31,14 @@ import {
|
|
|
31
31
|
DecodedToken
|
|
32
32
|
} from './cache';
|
|
33
33
|
|
|
34
|
-
import { TransactionManager } from './transaction-manager';
|
|
34
|
+
import { ConnectAccountTransaction, LoginTransaction, TransactionManager } from './transaction-manager';
|
|
35
35
|
import { verify as verifyIdToken } from './jwt';
|
|
36
36
|
import {
|
|
37
37
|
AuthenticationError,
|
|
38
|
+
ConnectError,
|
|
38
39
|
GenericError,
|
|
39
40
|
MissingRefreshTokenError,
|
|
41
|
+
MissingScopesError,
|
|
40
42
|
TimeoutError
|
|
41
43
|
} from './errors';
|
|
42
44
|
|
|
@@ -76,7 +78,11 @@ import {
|
|
|
76
78
|
User,
|
|
77
79
|
IdToken,
|
|
78
80
|
GetTokenSilentlyVerboseResponse,
|
|
79
|
-
TokenEndpointResponse
|
|
81
|
+
TokenEndpointResponse,
|
|
82
|
+
AuthenticationResult,
|
|
83
|
+
ConnectAccountRedirectResult,
|
|
84
|
+
RedirectConnectAccountOptions,
|
|
85
|
+
ResponseType
|
|
80
86
|
} from './global';
|
|
81
87
|
|
|
82
88
|
// @ts-ignore
|
|
@@ -93,7 +99,8 @@ import {
|
|
|
93
99
|
patchOpenUrlWithOnRedirect,
|
|
94
100
|
getScopeToRequest,
|
|
95
101
|
allScopesAreIncluded,
|
|
96
|
-
isRefreshWithMrrt
|
|
102
|
+
isRefreshWithMrrt,
|
|
103
|
+
getMissingScopes
|
|
97
104
|
} from './Auth0Client.utils';
|
|
98
105
|
import { CustomTokenExchangeOptions } from './TokenExchange';
|
|
99
106
|
import { Dpop } from './dpop/dpop';
|
|
@@ -102,6 +109,7 @@ import {
|
|
|
102
109
|
type FetcherConfig,
|
|
103
110
|
type CustomFetchMinimalOutput
|
|
104
111
|
} from './fetcher';
|
|
112
|
+
import { MyAccountApiClient } from './MyAccountApiClient';
|
|
105
113
|
|
|
106
114
|
/**
|
|
107
115
|
* @ignore
|
|
@@ -138,6 +146,7 @@ export class Auth0Client {
|
|
|
138
146
|
authorizationParams: AuthorizationParams;
|
|
139
147
|
};
|
|
140
148
|
private readonly userCache: ICache = new InMemoryCache().enclosedCache;
|
|
149
|
+
private readonly myAccountApi: MyAccountApiClient;
|
|
141
150
|
|
|
142
151
|
private worker?: Worker;
|
|
143
152
|
private readonly defaultOptions: Partial<Auth0ClientOptions> = {
|
|
@@ -238,6 +247,23 @@ export class Auth0Client {
|
|
|
238
247
|
this.domainUrl = getDomain(this.options.domain);
|
|
239
248
|
this.tokenIssuer = getTokenIssuer(this.options.issuer, this.domainUrl);
|
|
240
249
|
|
|
250
|
+
const myAccountApiIdentifier = `${this.domainUrl}/me/`;
|
|
251
|
+
const myAccountFetcher = this.createFetcher({
|
|
252
|
+
...(this.options.useDpop && { dpopNonceId: '__auth0_my_account_api__' }),
|
|
253
|
+
getAccessToken: () =>
|
|
254
|
+
this.getTokenSilently({
|
|
255
|
+
authorizationParams: {
|
|
256
|
+
scope: 'create:me:connected_accounts',
|
|
257
|
+
audience: myAccountApiIdentifier
|
|
258
|
+
},
|
|
259
|
+
detailedResponse: true
|
|
260
|
+
})
|
|
261
|
+
});
|
|
262
|
+
this.myAccountApi = new MyAccountApiClient(
|
|
263
|
+
myAccountFetcher,
|
|
264
|
+
myAccountApiIdentifier
|
|
265
|
+
);
|
|
266
|
+
|
|
241
267
|
// Don't use web workers unless using refresh tokens in memory
|
|
242
268
|
if (
|
|
243
269
|
typeof window !== 'undefined' &&
|
|
@@ -477,9 +503,10 @@ export class Auth0Client {
|
|
|
477
503
|
urlOptions.authorizationParams || {}
|
|
478
504
|
);
|
|
479
505
|
|
|
480
|
-
this.transactionManager.create({
|
|
506
|
+
this.transactionManager.create<LoginTransaction>({
|
|
481
507
|
...transaction,
|
|
482
508
|
appState,
|
|
509
|
+
response_type: ResponseType.Code,
|
|
483
510
|
...(organization && { organization })
|
|
484
511
|
});
|
|
485
512
|
|
|
@@ -500,18 +527,18 @@ export class Auth0Client {
|
|
|
500
527
|
*/
|
|
501
528
|
public async handleRedirectCallback<TAppState = any>(
|
|
502
529
|
url: string = window.location.href
|
|
503
|
-
): Promise<
|
|
530
|
+
): Promise<
|
|
531
|
+
RedirectLoginResult<TAppState> | ConnectAccountRedirectResult<TAppState>
|
|
532
|
+
> {
|
|
504
533
|
const queryStringFragments = url.split('?').slice(1);
|
|
505
534
|
|
|
506
535
|
if (queryStringFragments.length === 0) {
|
|
507
536
|
throw new Error('There are no query params available for parsing.');
|
|
508
537
|
}
|
|
509
538
|
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
);
|
|
513
|
-
|
|
514
|
-
const transaction = this.transactionManager.get();
|
|
539
|
+
const transaction = this.transactionManager.get<
|
|
540
|
+
LoginTransaction | ConnectAccountTransaction
|
|
541
|
+
>();
|
|
515
542
|
|
|
516
543
|
if (!transaction) {
|
|
517
544
|
throw new GenericError('missing_transaction', 'Invalid state');
|
|
@@ -519,6 +546,38 @@ export class Auth0Client {
|
|
|
519
546
|
|
|
520
547
|
this.transactionManager.remove();
|
|
521
548
|
|
|
549
|
+
const authenticationResult = parseAuthenticationResult(
|
|
550
|
+
queryStringFragments.join('')
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
if (transaction.response_type === ResponseType.ConnectCode) {
|
|
554
|
+
return this._handleConnectAccountRedirectCallback<TAppState>(
|
|
555
|
+
authenticationResult,
|
|
556
|
+
transaction
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
return this._handleLoginRedirectCallback<TAppState>(
|
|
560
|
+
authenticationResult,
|
|
561
|
+
transaction
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Handles the redirect callback from the login flow.
|
|
567
|
+
*
|
|
568
|
+
* @template AppState - The application state persisted from the /authorize redirect.
|
|
569
|
+
* @param {string} authenticationResult - The parsed authentication result from the URL.
|
|
570
|
+
* @param {string} transaction - The login transaction.
|
|
571
|
+
*
|
|
572
|
+
* @returns {RedirectLoginResult} Resolves with the persisted app state.
|
|
573
|
+
* @throws {GenericError | Error} If the transaction is missing, invalid, or the code exchange fails.
|
|
574
|
+
*/
|
|
575
|
+
private async _handleLoginRedirectCallback<TAppState>(
|
|
576
|
+
authenticationResult: AuthenticationResult,
|
|
577
|
+
transaction: LoginTransaction
|
|
578
|
+
): Promise<RedirectLoginResult<TAppState>> {
|
|
579
|
+
const { code, state, error, error_description } = authenticationResult;
|
|
580
|
+
|
|
522
581
|
if (error) {
|
|
523
582
|
throw new AuthenticationError(
|
|
524
583
|
error,
|
|
@@ -553,7 +612,63 @@ export class Auth0Client {
|
|
|
553
612
|
);
|
|
554
613
|
|
|
555
614
|
return {
|
|
556
|
-
appState: transaction.appState
|
|
615
|
+
appState: transaction.appState,
|
|
616
|
+
response_type: ResponseType.Code
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Handles the redirect callback from the connect account flow.
|
|
622
|
+
* This works the same as the redirect from the login flow expect it verifies the `connect_code`
|
|
623
|
+
* with the My Account API rather than the `code` with the Authorization Server.
|
|
624
|
+
*
|
|
625
|
+
* @template AppState - The application state persisted from the connect redirect.
|
|
626
|
+
* @param {string} connectResult - The parsed connect accounts result from the URL.
|
|
627
|
+
* @param {string} transaction - The login transaction.
|
|
628
|
+
* @returns {Promise<ConnectAccountRedirectResult>} The result of the My Account API, including any persisted app state.
|
|
629
|
+
* @throws {GenericError | MyAccountApiError} If the transaction is missing, invalid, or an error is returned from the My Account API.
|
|
630
|
+
*/
|
|
631
|
+
private async _handleConnectAccountRedirectCallback<TAppState>(
|
|
632
|
+
connectResult: AuthenticationResult,
|
|
633
|
+
transaction: ConnectAccountTransaction
|
|
634
|
+
): Promise<ConnectAccountRedirectResult<TAppState>> {
|
|
635
|
+
const { connect_code, state, error, error_description } = connectResult;
|
|
636
|
+
|
|
637
|
+
if (error) {
|
|
638
|
+
throw new ConnectError(
|
|
639
|
+
error,
|
|
640
|
+
error_description || error,
|
|
641
|
+
transaction.connection,
|
|
642
|
+
state,
|
|
643
|
+
transaction.appState
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (!connect_code) {
|
|
648
|
+
throw new GenericError('missing_connect_code', 'Missing connect code');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (
|
|
652
|
+
!transaction.code_verifier ||
|
|
653
|
+
!transaction.state ||
|
|
654
|
+
!transaction.auth_session ||
|
|
655
|
+
!transaction.redirect_uri ||
|
|
656
|
+
transaction.state !== state
|
|
657
|
+
) {
|
|
658
|
+
throw new GenericError('state_mismatch', 'Invalid state');
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const data = await this.myAccountApi.completeAccount({
|
|
662
|
+
auth_session: transaction.auth_session,
|
|
663
|
+
connect_code,
|
|
664
|
+
redirect_uri: transaction.redirect_uri,
|
|
665
|
+
code_verifier: transaction.code_verifier
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
...data,
|
|
670
|
+
appState: transaction.appState,
|
|
671
|
+
response_type: ResponseType.ConnectCode,
|
|
557
672
|
};
|
|
558
673
|
}
|
|
559
674
|
|
|
@@ -1032,7 +1147,7 @@ export class Auth0Client {
|
|
|
1032
1147
|
}
|
|
1033
1148
|
);
|
|
1034
1149
|
|
|
1035
|
-
// If is refreshed with MRRT, we update all entries that have the old
|
|
1150
|
+
// If is refreshed with MRRT, we update all entries that have the old
|
|
1036
1151
|
// refresh_token with the new one if the server responded with one
|
|
1037
1152
|
if (tokenResult.refresh_token && this.options.useMrrt && cache?.refresh_token) {
|
|
1038
1153
|
await this.cacheManager.updateEntry(
|
|
@@ -1064,9 +1179,14 @@ export class Auth0Client {
|
|
|
1064
1179
|
return await this._getTokenFromIFrame(options);
|
|
1065
1180
|
}
|
|
1066
1181
|
|
|
1067
|
-
|
|
1182
|
+
const missingScopes = getMissingScopes(
|
|
1183
|
+
scopesToRequest,
|
|
1184
|
+
tokenResult.scope,
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
throw new MissingScopesError(
|
|
1068
1188
|
options.authorizationParams.audience || 'default',
|
|
1069
|
-
|
|
1189
|
+
missingScopes,
|
|
1070
1190
|
);
|
|
1071
1191
|
}
|
|
1072
1192
|
}
|
|
@@ -1369,12 +1489,6 @@ export class Auth0Client {
|
|
|
1369
1489
|
public createFetcher<TOutput extends CustomFetchMinimalOutput = Response>(
|
|
1370
1490
|
config: FetcherConfig<TOutput> = {}
|
|
1371
1491
|
): Fetcher<TOutput> {
|
|
1372
|
-
if (this.options.useDpop && !config.dpopNonceId) {
|
|
1373
|
-
throw new TypeError(
|
|
1374
|
-
'When `useDpop` is enabled, `dpopNonceId` must be set when calling `createFetcher()`.'
|
|
1375
|
-
);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
1492
|
return new Fetcher(config, {
|
|
1379
1493
|
isDpopEnabled: () => !!this.options.useDpop,
|
|
1380
1494
|
getAccessToken: authParams =>
|
|
@@ -1382,13 +1496,79 @@ export class Auth0Client {
|
|
|
1382
1496
|
authorizationParams: {
|
|
1383
1497
|
scope: authParams?.scope?.join(' '),
|
|
1384
1498
|
audience: authParams?.audience
|
|
1385
|
-
}
|
|
1499
|
+
},
|
|
1500
|
+
detailedResponse: true
|
|
1386
1501
|
}),
|
|
1387
1502
|
getDpopNonce: () => this.getDpopNonce(config.dpopNonceId),
|
|
1388
|
-
setDpopNonce: nonce => this.setDpopNonce(nonce),
|
|
1503
|
+
setDpopNonce: nonce => this.setDpopNonce(nonce, config.dpopNonceId),
|
|
1389
1504
|
generateDpopProof: params => this.generateDpopProof(params)
|
|
1390
1505
|
});
|
|
1391
1506
|
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Initiates a redirect to connect the user's account with a specified connection.
|
|
1510
|
+
* This method generates PKCE parameters, creates a transaction, and redirects to the /connect endpoint.
|
|
1511
|
+
*
|
|
1512
|
+
* @template TAppState - The application state to persist through the transaction.
|
|
1513
|
+
* @param {RedirectConnectAccountOptions<TAppState>} options - Options for the connect account redirect flow.
|
|
1514
|
+
* @param {string} options.connection - The name of the connection to link (e.g. 'google-oauth2').
|
|
1515
|
+
* @param {AuthorizationParams} [options.authorization_params] - Additional authorization parameters for the request to the upstream IdP.
|
|
1516
|
+
* @param {string} [options.redirectUri] - The URI to redirect back to after connecting the account.
|
|
1517
|
+
* @param {TAppState} [options.appState] - Application state to persist through the transaction.
|
|
1518
|
+
* @param {(url: string) => Promise<void>} [options.openUrl] - Custom function to open the URL.
|
|
1519
|
+
*
|
|
1520
|
+
* @returns {Promise<void>} Resolves when the redirect is initiated.
|
|
1521
|
+
* @throws {MyAccountApiError} If the connect request to the My Account API fails.
|
|
1522
|
+
*/
|
|
1523
|
+
public async connectAccountWithRedirect<TAppState = any>(
|
|
1524
|
+
options: RedirectConnectAccountOptions<TAppState>
|
|
1525
|
+
) {
|
|
1526
|
+
const {
|
|
1527
|
+
openUrl,
|
|
1528
|
+
appState,
|
|
1529
|
+
connection,
|
|
1530
|
+
authorization_params,
|
|
1531
|
+
redirectUri = this.options.authorizationParams.redirect_uri ||
|
|
1532
|
+
window.location.origin
|
|
1533
|
+
} = options;
|
|
1534
|
+
|
|
1535
|
+
if (!connection) {
|
|
1536
|
+
throw new Error('connection is required');
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const state = encode(createRandomString());
|
|
1540
|
+
const code_verifier = createRandomString();
|
|
1541
|
+
const code_challengeBuffer = await sha256(code_verifier);
|
|
1542
|
+
const code_challenge = bufferToBase64UrlEncoded(code_challengeBuffer);
|
|
1543
|
+
|
|
1544
|
+
const { connect_uri, connect_params, auth_session } =
|
|
1545
|
+
await this.myAccountApi.connectAccount({
|
|
1546
|
+
connection,
|
|
1547
|
+
redirect_uri: redirectUri,
|
|
1548
|
+
state,
|
|
1549
|
+
code_challenge,
|
|
1550
|
+
code_challenge_method: 'S256',
|
|
1551
|
+
authorization_params
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
this.transactionManager.create<ConnectAccountTransaction>({
|
|
1555
|
+
state,
|
|
1556
|
+
code_verifier,
|
|
1557
|
+
auth_session,
|
|
1558
|
+
redirect_uri: redirectUri,
|
|
1559
|
+
appState,
|
|
1560
|
+
connection,
|
|
1561
|
+
response_type: ResponseType.ConnectCode
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
const url = new URL(connect_uri);
|
|
1565
|
+
url.searchParams.set('ticket', connect_params.ticket);
|
|
1566
|
+
if (openUrl) {
|
|
1567
|
+
await openUrl(url.toString());
|
|
1568
|
+
} else {
|
|
1569
|
+
window.location.assign(url);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1392
1572
|
}
|
|
1393
1573
|
|
|
1394
1574
|
interface BaseRequestTokenOptions {
|
package/src/Auth0Client.utils.ts
CHANGED
|
@@ -108,6 +108,20 @@ export const allScopesAreIncluded = (scopeToInclude?: string, scopes?: string):
|
|
|
108
108
|
return scopesToInclude.every((key) => scopeGroup.includes(key));
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* @ignore
|
|
113
|
+
*
|
|
114
|
+
* Returns the scopes that are missing after a refresh
|
|
115
|
+
*/
|
|
116
|
+
export const getMissingScopes = (requestedScope?: string, respondedScope?: string): string => {
|
|
117
|
+
const requestedScopes = requestedScope?.split(" ") || [];
|
|
118
|
+
const respondedScopes = respondedScope?.split(" ") || [];
|
|
119
|
+
|
|
120
|
+
const missingScopes = requestedScopes.filter((scope) => respondedScopes.indexOf(scope) == -1);
|
|
121
|
+
|
|
122
|
+
return missingScopes.join(",");
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
/**
|
|
112
126
|
* @ignore
|
|
113
127
|
*
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { AuthorizationParams } from './global';
|
|
2
|
+
import { Fetcher } from './fetcher';
|
|
3
|
+
|
|
4
|
+
interface ConnectRequest {
|
|
5
|
+
/** The name of the connection to link the account with (e.g., 'google-oauth2', 'facebook'). */
|
|
6
|
+
connection: string;
|
|
7
|
+
/** The URI to redirect to after the connection process completes. */
|
|
8
|
+
redirect_uri: string;
|
|
9
|
+
/** An opaque value used to maintain state between the request and callback. */
|
|
10
|
+
state?: string;
|
|
11
|
+
/** A string value used to associate a Client session with an ID Token, and to mitigate replay attacks. */
|
|
12
|
+
nonce?: string;
|
|
13
|
+
/** The PKCE code challenge derived from the code verifier. */
|
|
14
|
+
code_challenge?: string;
|
|
15
|
+
/** The method used to derive the code challenge. Required when code_challenge is provided. */
|
|
16
|
+
code_challenge_method?: 'S256';
|
|
17
|
+
authorization_params?: AuthorizationParams;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ConnectResponse {
|
|
21
|
+
/** The base URI to initiate the account connection flow. */
|
|
22
|
+
connect_uri: string;
|
|
23
|
+
/** The authentication session identifier. */
|
|
24
|
+
auth_session: string;
|
|
25
|
+
/** Parameters to be used with the connect URI. */
|
|
26
|
+
connect_params: {
|
|
27
|
+
/** The ticket identifier to be used with the connection URI. */
|
|
28
|
+
ticket: string;
|
|
29
|
+
};
|
|
30
|
+
/** The number of seconds until the ticket expires. */
|
|
31
|
+
expires_in: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CompleteRequest {
|
|
35
|
+
/** The authentication session identifier */
|
|
36
|
+
auth_session: string;
|
|
37
|
+
/** The authorization code returned from the connect flow */
|
|
38
|
+
connect_code: string;
|
|
39
|
+
/** The redirect URI used in the original request */
|
|
40
|
+
redirect_uri: string;
|
|
41
|
+
/** The PKCE code verifier */
|
|
42
|
+
code_verifier?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CompleteResponse {
|
|
46
|
+
/** The unique identifier of the connected account */
|
|
47
|
+
id: string;
|
|
48
|
+
/** The connection name */
|
|
49
|
+
connection: string;
|
|
50
|
+
/** The access type, always 'offline' */
|
|
51
|
+
access_type: 'offline';
|
|
52
|
+
/** Array of scopes granted */
|
|
53
|
+
scopes?: string[];
|
|
54
|
+
/** ISO date string of when the connected account was created */
|
|
55
|
+
created_at: string;
|
|
56
|
+
/** ISO date string of when the refresh token expires (optional) */
|
|
57
|
+
expires_at?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validation error returned from MyAccount API
|
|
61
|
+
export interface ErrorResponse {
|
|
62
|
+
type: string;
|
|
63
|
+
status: number;
|
|
64
|
+
title: string;
|
|
65
|
+
detail: string;
|
|
66
|
+
validation_errors?: {
|
|
67
|
+
detail: string;
|
|
68
|
+
field?: string;
|
|
69
|
+
pointer?: string;
|
|
70
|
+
source?: string;
|
|
71
|
+
}[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Subset of the MyAccount API that handles the connect accounts flow.
|
|
76
|
+
*/
|
|
77
|
+
export class MyAccountApiClient {
|
|
78
|
+
constructor(
|
|
79
|
+
private myAccountFetcher: Fetcher<Response>,
|
|
80
|
+
private apiBase: string
|
|
81
|
+
) {}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get a ticket for the connect account flow.
|
|
85
|
+
*/
|
|
86
|
+
async connectAccount(params: ConnectRequest): Promise<ConnectResponse> {
|
|
87
|
+
const res = await this.myAccountFetcher.fetchWithAuth(
|
|
88
|
+
`${this.apiBase}v1/connected-accounts/connect`,
|
|
89
|
+
{
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'Content-Type': 'application/json' },
|
|
92
|
+
body: JSON.stringify(params)
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
return this._handleResponse(res);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Verify the redirect from the connect account flow and complete the connecting of the account.
|
|
100
|
+
*/
|
|
101
|
+
async completeAccount(params: CompleteRequest): Promise<CompleteResponse> {
|
|
102
|
+
const res = await this.myAccountFetcher.fetchWithAuth(
|
|
103
|
+
`${this.apiBase}v1/connected-accounts/complete`,
|
|
104
|
+
{
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify(params)
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
return this._handleResponse(res);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async _handleResponse(res: Response) {
|
|
114
|
+
let body: any;
|
|
115
|
+
try {
|
|
116
|
+
body = await res.text();
|
|
117
|
+
body = JSON.parse(body);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
throw new MyAccountApiError({
|
|
120
|
+
type: 'invalid_json',
|
|
121
|
+
status: res.status,
|
|
122
|
+
title: 'Invalid JSON response',
|
|
123
|
+
detail: body || String(err)
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (res.ok) {
|
|
128
|
+
return body;
|
|
129
|
+
} else {
|
|
130
|
+
throw new MyAccountApiError(body);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class MyAccountApiError extends Error {
|
|
136
|
+
public readonly type: string;
|
|
137
|
+
public readonly status: number;
|
|
138
|
+
public readonly title: string;
|
|
139
|
+
public readonly detail: string;
|
|
140
|
+
public readonly validation_errors?: ErrorResponse['validation_errors'];
|
|
141
|
+
|
|
142
|
+
constructor({
|
|
143
|
+
type,
|
|
144
|
+
status,
|
|
145
|
+
title,
|
|
146
|
+
detail,
|
|
147
|
+
validation_errors
|
|
148
|
+
}: ErrorResponse) {
|
|
149
|
+
super(detail);
|
|
150
|
+
this.name = 'MyAccountApiError';
|
|
151
|
+
this.type = type;
|
|
152
|
+
this.status = status;
|
|
153
|
+
this.title = title;
|
|
154
|
+
this.detail = detail;
|
|
155
|
+
this.validation_errors = validation_errors;
|
|
156
|
+
Object.setPrototypeOf(this, MyAccountApiError.prototype);
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -35,6 +35,24 @@ export class AuthenticationError extends GenericError {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Thrown when handling the redirect callback for the connect flow fails, will be one of Auth0's
|
|
40
|
+
* Authentication API's Standard Error Responses: https://auth0.com/docs/api/authentication?javascript#standard-error-responses
|
|
41
|
+
*/
|
|
42
|
+
export class ConnectError extends GenericError {
|
|
43
|
+
constructor(
|
|
44
|
+
error: string,
|
|
45
|
+
error_description: string,
|
|
46
|
+
public connection: string,
|
|
47
|
+
public state: string,
|
|
48
|
+
public appState: any = null
|
|
49
|
+
) {
|
|
50
|
+
super(error, error_description);
|
|
51
|
+
//https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
|
52
|
+
Object.setPrototypeOf(this, ConnectError.prototype);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
/**
|
|
39
57
|
* Thrown when silent auth times out (usually due to a configuration issue) or
|
|
40
58
|
* when network requests to the Auth server timeout.
|
|
@@ -96,6 +114,21 @@ export class MissingRefreshTokenError extends GenericError {
|
|
|
96
114
|
}
|
|
97
115
|
}
|
|
98
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Error thrown when there are missing scopes after refreshing a token
|
|
119
|
+
*/
|
|
120
|
+
export class MissingScopesError extends GenericError {
|
|
121
|
+
constructor(public audience: string, public scope: string) {
|
|
122
|
+
super(
|
|
123
|
+
'missing_scopes',
|
|
124
|
+
`Missing requested scopes after refresh (audience: '${valueOrEmptyString(audience, [
|
|
125
|
+
'default'
|
|
126
|
+
])}', missing scope: '${valueOrEmptyString(scope)}')`
|
|
127
|
+
);
|
|
128
|
+
Object.setPrototypeOf(this, MissingScopesError.prototype);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
/**
|
|
100
133
|
* Error thrown when the wrong DPoP nonce is used and a potential subsequent retry wasn't able to fix it.
|
|
101
134
|
*/
|
package/src/fetcher.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DPOP_NONCE_HEADER } from './dpop/utils';
|
|
2
2
|
import { UseDpopNonceError } from './errors';
|
|
3
|
+
import { GetTokenSilentlyVerboseResponse } from './global';
|
|
3
4
|
|
|
4
5
|
export type ResponseHeaders =
|
|
5
6
|
| Record<string, string | null | undefined>
|
|
@@ -20,7 +21,12 @@ export type AuthParams = {
|
|
|
20
21
|
audience?: string;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
type AccessTokenFactory = (authParams?: AuthParams) => Promise<string>;
|
|
24
|
+
type AccessTokenFactory = (authParams?: AuthParams) => Promise<string | GetTokenSilentlyVerboseResponse>;
|
|
25
|
+
|
|
26
|
+
enum TokenType {
|
|
27
|
+
Bearer = 'Bearer',
|
|
28
|
+
DPoP = 'DPoP'
|
|
29
|
+
}
|
|
24
30
|
|
|
25
31
|
export type FetcherConfig<TOutput extends CustomFetchMinimalOutput> = {
|
|
26
32
|
getAccessToken?: AccessTokenFactory;
|
|
@@ -88,12 +94,24 @@ export class Fetcher<TOutput extends CustomFetchMinimalOutput> {
|
|
|
88
94
|
throw new TypeError('`url` must be absolute or `baseUrl` non-empty.');
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
protected getAccessToken(authParams?: AuthParams): Promise<string> {
|
|
97
|
+
protected getAccessToken(authParams?: AuthParams): Promise<string | GetTokenSilentlyVerboseResponse> {
|
|
92
98
|
return this.config.getAccessToken
|
|
93
99
|
? this.config.getAccessToken(authParams)
|
|
94
100
|
: this.hooks.getAccessToken(authParams);
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
protected extractUrl(info: RequestInfo | URL): string {
|
|
104
|
+
if (typeof info === 'string') {
|
|
105
|
+
return info;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (info instanceof URL) {
|
|
109
|
+
return info.href;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return info.url;
|
|
113
|
+
}
|
|
114
|
+
|
|
97
115
|
protected buildBaseRequest(
|
|
98
116
|
info: RequestInfo | URL,
|
|
99
117
|
init: RequestInit | undefined
|
|
@@ -101,26 +119,32 @@ export class Fetcher<TOutput extends CustomFetchMinimalOutput> {
|
|
|
101
119
|
// In the native `fetch()` behavior, `init` can override `info` and the result
|
|
102
120
|
// is the merge of both. So let's replicate that behavior by passing those into
|
|
103
121
|
// a fresh `Request` object.
|
|
104
|
-
const request = new Request(info, init);
|
|
105
122
|
|
|
106
|
-
// No `baseUrl
|
|
123
|
+
// No `baseUrl`? We can use `info` and `init` as is.
|
|
107
124
|
if (!this.config.baseUrl) {
|
|
108
|
-
return
|
|
125
|
+
return new Request(info, init);
|
|
109
126
|
}
|
|
110
127
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
128
|
+
// But if `baseUrl` is present, first we have to build the final URL...
|
|
129
|
+
const finalUrl = this.buildUrl(this.config.baseUrl, this.extractUrl(info));
|
|
130
|
+
|
|
131
|
+
// ... and then overwrite `info`'s URL with it, making sure we keep any other
|
|
132
|
+
// properties that might be there already (headers, etc).
|
|
133
|
+
const finalInfo = info instanceof Request
|
|
134
|
+
? new Request(finalUrl, info)
|
|
135
|
+
: finalUrl;
|
|
136
|
+
|
|
137
|
+
return new Request(finalInfo, init);
|
|
115
138
|
}
|
|
116
139
|
|
|
117
|
-
protected
|
|
140
|
+
protected setAuthorizationHeader(
|
|
118
141
|
request: Request,
|
|
119
|
-
accessToken: string
|
|
120
|
-
|
|
142
|
+
accessToken: string,
|
|
143
|
+
tokenType: string = TokenType.Bearer
|
|
144
|
+
): void {
|
|
121
145
|
request.headers.set(
|
|
122
146
|
'authorization',
|
|
123
|
-
`${
|
|
147
|
+
`${tokenType} ${accessToken}`
|
|
124
148
|
);
|
|
125
149
|
}
|
|
126
150
|
|
|
@@ -145,11 +169,22 @@ export class Fetcher<TOutput extends CustomFetchMinimalOutput> {
|
|
|
145
169
|
}
|
|
146
170
|
|
|
147
171
|
protected async prepareRequest(request: Request, authParams?: AuthParams) {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
172
|
+
const accessTokenResponse = await this.getAccessToken(authParams);
|
|
173
|
+
|
|
174
|
+
let tokenType: string;
|
|
175
|
+
let accessToken: string;
|
|
176
|
+
if (typeof accessTokenResponse === 'string') {
|
|
177
|
+
tokenType = this.config.dpopNonceId ? TokenType.DPoP : TokenType.Bearer;
|
|
178
|
+
accessToken = accessTokenResponse;
|
|
179
|
+
} else {
|
|
180
|
+
tokenType = accessTokenResponse.token_type;
|
|
181
|
+
accessToken = accessTokenResponse.access_token;
|
|
182
|
+
}
|
|
151
183
|
|
|
152
|
-
|
|
184
|
+
this.setAuthorizationHeader(request, accessToken, tokenType);
|
|
185
|
+
if (tokenType === TokenType.DPoP) {
|
|
186
|
+
await this.setDpopProofHeader(request, accessToken);
|
|
187
|
+
}
|
|
153
188
|
}
|
|
154
189
|
|
|
155
190
|
protected getHeader(headers: ResponseHeaders, name: string): string {
|
|
@@ -171,7 +206,7 @@ export class Fetcher<TOutput extends CustomFetchMinimalOutput> {
|
|
|
171
206
|
|
|
172
207
|
const wwwAuthHeader = this.getHeader(response.headers, 'www-authenticate');
|
|
173
208
|
|
|
174
|
-
return wwwAuthHeader.includes('use_dpop_nonce');
|
|
209
|
+
return wwwAuthHeader.includes('invalid_dpop_nonce') || wwwAuthHeader.includes('use_dpop_nonce');
|
|
175
210
|
}
|
|
176
211
|
|
|
177
212
|
protected async handleResponse(
|