@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.
@@ -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<RedirectLoginResult<TAppState>> {
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 { state, code, error, error_description } = parseAuthenticationResult(
511
- queryStringFragments.join('')
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
- throw new MissingRefreshTokenError(
1182
+ const missingScopes = getMissingScopes(
1183
+ scopesToRequest,
1184
+ tokenResult.scope,
1185
+ );
1186
+
1187
+ throw new MissingScopesError(
1068
1188
  options.authorizationParams.audience || 'default',
1069
- options.authorizationParams.scope,
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 {
@@ -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` config, use whatever the URL the `Request` came with.
123
+ // No `baseUrl`? We can use `info` and `init` as is.
107
124
  if (!this.config.baseUrl) {
108
- return request;
125
+ return new Request(info, init);
109
126
  }
110
127
 
111
- return new Request(
112
- this.buildUrl(this.config.baseUrl, request.url),
113
- request
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 async setAuthorizationHeader(
140
+ protected setAuthorizationHeader(
118
141
  request: Request,
119
- accessToken: string
120
- ): Promise<void> {
142
+ accessToken: string,
143
+ tokenType: string = TokenType.Bearer
144
+ ): void {
121
145
  request.headers.set(
122
146
  'authorization',
123
- `${this.config.dpopNonceId ? 'DPoP' : 'Bearer'} ${accessToken}`
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 accessToken = await this.getAccessToken(authParams);
149
-
150
- this.setAuthorizationHeader(request, accessToken);
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
- await this.setDpopProofHeader(request, accessToken);
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(