@imtbl/auth 2.10.7-alpha.5 → 2.10.7-alpha.6

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/src/Auth.ts CHANGED
@@ -1,22 +1,139 @@
1
- import AuthManager from './authManager';
1
+ import {
2
+ ErrorResponse,
3
+ ErrorTimeout,
4
+ InMemoryWebStorage,
5
+ User as OidcUser,
6
+ UserManager,
7
+ UserManagerSettings,
8
+ WebStorageStateStore,
9
+ } from 'oidc-client-ts';
10
+ import axios from 'axios';
11
+ import jwt_decode from 'jwt-decode';
12
+ import localForage from 'localforage';
13
+ import {
14
+ Detail,
15
+ getDetail,
16
+ identify,
17
+ track,
18
+ trackError,
19
+ } from '@imtbl/metrics';
2
20
  import { AuthConfiguration, IAuthConfiguration } from './config';
3
21
  import {
4
- AuthModuleConfiguration, User, DirectLoginOptions, DeviceTokenResponse, LoginOptions, AuthEventMap, AuthEvents,
22
+ AuthModuleConfiguration,
23
+ User,
24
+ DirectLoginOptions,
25
+ DeviceTokenResponse,
26
+ LoginOptions,
27
+ AuthEventMap,
28
+ AuthEvents,
29
+ UserZkEvm,
30
+ OidcConfiguration,
31
+ PassportMetadata,
32
+ IdTokenPayload,
33
+ isUserZkEvm,
5
34
  } from './types';
6
35
  import EmbeddedLoginPrompt from './login/embeddedLoginPrompt';
7
36
  import TypedEventEmitter from './utils/typedEventEmitter';
8
37
  import { withMetricsAsync } from './utils/metrics';
9
- import { identify, track, trackError } from '@imtbl/metrics';
38
+ import DeviceCredentialsManager from './storage/device_credentials_manager';
39
+ import { PassportError, PassportErrorType, withPassportError } from './errors';
10
40
  import logger from './utils/logger';
41
+ import { isAccessTokenExpiredOrExpiring } from './utils/token';
42
+ import LoginPopupOverlay from './overlay/loginPopupOverlay';
43
+ import { LocalForageAsyncStorage } from './storage/LocalForageAsyncStorage';
44
+
45
+ const LOGIN_POPUP_CLOSED_POLLING_DURATION = 500;
46
+
47
+ const formUrlEncodedHeader = {
48
+ headers: {
49
+ 'Content-Type': 'application/x-www-form-urlencoded',
50
+ },
51
+ };
52
+
53
+ const logoutEndpoint = '/v2/logout';
54
+ const crossSdkBridgeLogoutEndpoint = '/im-logged-out';
55
+ const authorizeEndpoint = '/authorize';
56
+
57
+ const getLogoutEndpointPath = (crossSdkBridgeEnabled: boolean): string => (
58
+ crossSdkBridgeEnabled ? crossSdkBridgeLogoutEndpoint : logoutEndpoint
59
+ );
60
+
61
+ const getAuthConfiguration = (config: IAuthConfiguration): UserManagerSettings => {
62
+ const { authenticationDomain, oidcConfiguration } = config;
63
+
64
+ let store;
65
+ if (config.crossSdkBridgeEnabled) {
66
+ store = new LocalForageAsyncStorage('ImmutableSDKPassport', localForage.INDEXEDDB);
67
+ } else if (typeof window !== 'undefined') {
68
+ store = window.localStorage;
69
+ } else {
70
+ store = new InMemoryWebStorage();
71
+ }
72
+ const userStore = new WebStorageStateStore({ store });
73
+
74
+ const endSessionEndpoint = new URL(
75
+ getLogoutEndpointPath(config.crossSdkBridgeEnabled),
76
+ authenticationDomain.replace(/^(?:https?:\/\/)?(.*)/, 'https://$1'),
77
+ );
78
+ endSessionEndpoint.searchParams.set('client_id', oidcConfiguration.clientId);
79
+ if (oidcConfiguration.logoutRedirectUri) {
80
+ endSessionEndpoint.searchParams.set('returnTo', oidcConfiguration.logoutRedirectUri);
81
+ }
82
+
83
+ return {
84
+ authority: authenticationDomain,
85
+ redirect_uri: oidcConfiguration.redirectUri,
86
+ popup_redirect_uri: oidcConfiguration.popupRedirectUri || oidcConfiguration.redirectUri,
87
+ client_id: oidcConfiguration.clientId,
88
+ metadata: {
89
+ authorization_endpoint: `${authenticationDomain}/authorize`,
90
+ token_endpoint: `${authenticationDomain}/oauth/token`,
91
+ userinfo_endpoint: `${authenticationDomain}/userinfo`,
92
+ end_session_endpoint: endSessionEndpoint.toString(),
93
+ revocation_endpoint: `${authenticationDomain}/oauth/revoke`,
94
+ },
95
+ automaticSilentRenew: false,
96
+ scope: oidcConfiguration.scope,
97
+ userStore,
98
+ revokeTokenTypes: ['refresh_token'],
99
+ extraQueryParams: {
100
+ ...(oidcConfiguration.audience ? { audience: oidcConfiguration.audience } : {}),
101
+ },
102
+ } as UserManagerSettings;
103
+ };
104
+
105
+ function base64URLEncode(str: ArrayBuffer | Uint8Array) {
106
+ return btoa(String.fromCharCode(...new Uint8Array(str)))
107
+ .replace(/\+/g, '-')
108
+ .replace(/\//g, '_')
109
+ .replace(/=/g, '');
110
+ }
111
+
112
+ async function sha256(buffer: string) {
113
+ const encoder = new TextEncoder();
114
+ const data = encoder.encode(buffer);
115
+ return window.crypto.subtle.digest('SHA-256', data);
116
+ }
11
117
 
12
118
  /**
13
119
  * Public-facing Auth class for authentication
14
- * Wraps AuthManager with a simpler API
120
+ * Provides login/logout helpers and exposes auth state events
15
121
  */
16
122
  export class Auth {
17
- private authManager: AuthManager;
123
+ private readonly config: IAuthConfiguration;
124
+
125
+ private readonly userManager: UserManager;
126
+
127
+ private readonly deviceCredentialsManager: DeviceCredentialsManager;
18
128
 
19
- private config: IAuthConfiguration;
129
+ private readonly embeddedLoginPrompt: EmbeddedLoginPrompt;
130
+
131
+ private readonly logoutMode: Exclude<OidcConfiguration['logoutMode'], undefined>;
132
+
133
+ /**
134
+ * Promise that is used to prevent multiple concurrent calls to the refresh token endpoint.
135
+ */
136
+ private refreshingPromise: Promise<User | null> | null = null;
20
137
 
21
138
  /**
22
139
  * Event emitter for authentication events (LOGGED_IN, LOGGED_OUT)
@@ -44,8 +161,10 @@ export class Auth {
44
161
  */
45
162
  constructor(config: AuthModuleConfiguration) {
46
163
  this.config = new AuthConfiguration(config);
47
- const embeddedLoginPrompt = new EmbeddedLoginPrompt(this.config);
48
- this.authManager = new AuthManager(this.config, embeddedLoginPrompt);
164
+ this.embeddedLoginPrompt = new EmbeddedLoginPrompt(this.config);
165
+ this.userManager = new UserManager(getAuthConfiguration(this.config));
166
+ this.deviceCredentialsManager = new DeviceCredentialsManager();
167
+ this.logoutMode = this.config.oidcConfiguration.logoutMode || 'redirect';
49
168
  this.eventEmitter = new TypedEventEmitter<AuthEventMap>();
50
169
  track('passport', 'initialise');
51
170
  }
@@ -63,7 +182,7 @@ export class Auth {
63
182
 
64
183
  // Try to get cached user
65
184
  try {
66
- user = await this.authManager.getUser();
185
+ user = await this.getUserInternal();
67
186
  } catch (error: any) {
68
187
  if (error instanceof Error && !error.message.includes('Unknown or invalid refresh token')) {
69
188
  trackError('passport', 'login', error);
@@ -76,13 +195,13 @@ export class Auth {
76
195
 
77
196
  // If no cached user, try silent login or regular login
78
197
  if (!user && useSilentLogin) {
79
- user = await this.authManager.forceUserRefresh();
198
+ user = await this.forceUserRefreshInternal();
80
199
  } else if (!user && !useCachedSession) {
81
200
  if (options?.useRedirectFlow) {
82
- await this.authManager.loginWithRedirect(options?.directLoginOptions);
201
+ await this.loginWithRedirectInternal(options?.directLoginOptions);
83
202
  return null; // Redirect doesn't return user immediately
84
203
  }
85
- user = await this.authManager.login(options?.directLoginOptions);
204
+ user = await this.loginWithPopup(options?.directLoginOptions);
86
205
  }
87
206
 
88
207
  // Emit LOGGED_IN event and identify user if logged in
@@ -102,22 +221,21 @@ export class Auth {
102
221
  * @returns Promise that resolves when redirect is initiated
103
222
  */
104
223
  async loginWithRedirect(directLoginOptions?: DirectLoginOptions): Promise<void> {
105
- await this.authManager.loginWithRedirect(directLoginOptions);
224
+ await this.loginWithRedirectInternal(directLoginOptions);
106
225
  }
107
226
 
108
227
  /**
109
228
  * Login callback handler
110
- * Call this in your redirect URI page
111
- * @returns Promise that resolves with the authenticated user
229
+ * Call this in your redirect or popup callback page
230
+ * @returns Promise that resolves with the authenticated user or undefined (for popup flows)
112
231
  */
113
- async loginCallback(): Promise<User> {
232
+ async loginCallback(): Promise<User | undefined> {
114
233
  return withMetricsAsync(async () => {
115
- const user = await this.authManager.loginCallback();
116
- if (!user) {
117
- throw new Error('Login callback failed - no user returned');
234
+ const user = await this.loginCallbackInternal();
235
+ if (user) {
236
+ this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
237
+ identify({ passportId: user.profile.sub });
118
238
  }
119
- this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
120
- identify({ passportId: user.profile.sub });
121
239
  return user;
122
240
  }, 'loginCallback');
123
241
  }
@@ -128,7 +246,7 @@ export class Auth {
128
246
  */
129
247
  async logout(): Promise<void> {
130
248
  await withMetricsAsync(async () => {
131
- await this.authManager.logout();
249
+ await this.logoutInternal();
132
250
  this.eventEmitter.emit(AuthEvents.LOGGED_OUT);
133
251
  }, 'logout');
134
252
  }
@@ -138,7 +256,34 @@ export class Auth {
138
256
  * @returns Promise that resolves with the user or null if not authenticated
139
257
  */
140
258
  async getUser(): Promise<User | null> {
141
- return withMetricsAsync(async () => this.authManager.getUser(), 'getUserInfo', false);
259
+ return withMetricsAsync(async () => this.getUserInternal(), 'getUserInfo', false);
260
+ }
261
+
262
+ /**
263
+ * Get the current authenticated user or initiate login if needed
264
+ * @returns Promise that resolves with an authenticated user
265
+ */
266
+ async getUserOrLogin(): Promise<User> {
267
+ let user: User | null = null;
268
+ try {
269
+ user = await this.getUserInternal();
270
+ } catch (err) {
271
+ logger.warn('Failed to retrieve a cached user session', err);
272
+ }
273
+
274
+ if (user) {
275
+ return user;
276
+ }
277
+
278
+ return this.loginWithPopup();
279
+ }
280
+
281
+ /**
282
+ * Get the current authenticated zkEVM user
283
+ * @returns Promise that resolves with a zkEVM-capable user
284
+ */
285
+ async getUserZkEvm(): Promise<UserZkEvm> {
286
+ return this.getUserZkEvmInternal();
142
287
  }
143
288
 
144
289
  /**
@@ -147,7 +292,7 @@ export class Auth {
147
292
  */
148
293
  async getIdToken(): Promise<string | undefined> {
149
294
  return withMetricsAsync(async () => {
150
- const user = await this.authManager.getUser();
295
+ const user = await this.getUserInternal();
151
296
  return user?.idToken;
152
297
  }, 'getIdToken', false);
153
298
  }
@@ -158,7 +303,7 @@ export class Auth {
158
303
  */
159
304
  async getAccessToken(): Promise<string | undefined> {
160
305
  return withMetricsAsync(async () => {
161
- const user = await this.authManager.getUser();
306
+ const user = await this.getUserInternal();
162
307
  return user?.accessToken;
163
308
  }, 'getAccessToken', false, false);
164
309
  }
@@ -177,7 +322,14 @@ export class Auth {
177
322
  * @returns Promise that resolves with the user or null if refresh fails
178
323
  */
179
324
  async forceUserRefresh(): Promise<User | null> {
180
- return this.authManager.forceUserRefresh();
325
+ return this.forceUserRefreshInternal();
326
+ }
327
+
328
+ /**
329
+ * Trigger a background user refresh without awaiting the result
330
+ */
331
+ forceUserRefreshInBackground(): void {
332
+ this.forceUserRefreshInBackgroundInternal();
181
333
  }
182
334
 
183
335
  /**
@@ -188,7 +340,7 @@ export class Auth {
188
340
  */
189
341
  async loginWithPKCEFlow(directLoginOptions?: DirectLoginOptions, imPassportTraceId?: string): Promise<string> {
190
342
  return withMetricsAsync(
191
- async () => this.authManager.getPKCEAuthorizationUrl(directLoginOptions, imPassportTraceId),
343
+ async () => this.getPKCEAuthorizationUrl(directLoginOptions, imPassportTraceId),
192
344
  'loginWithPKCEFlow',
193
345
  );
194
346
  }
@@ -201,7 +353,7 @@ export class Auth {
201
353
  */
202
354
  async loginWithPKCEFlowCallback(authorizationCode: string, state: string): Promise<User> {
203
355
  return withMetricsAsync(async () => {
204
- const user = await this.authManager.loginWithPKCEFlowCallback(authorizationCode, state);
356
+ const user = await this.loginWithPKCEFlowCallbackInternal(authorizationCode, state);
205
357
  this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
206
358
  identify({ passportId: user.profile.sub });
207
359
  return user;
@@ -215,7 +367,7 @@ export class Auth {
215
367
  */
216
368
  async storeTokens(tokenResponse: DeviceTokenResponse): Promise<User> {
217
369
  return withMetricsAsync(async () => {
218
- const user = await this.authManager.storeTokens(tokenResponse);
370
+ const user = await this.storeTokensInternal(tokenResponse);
219
371
  this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
220
372
  identify({ passportId: user.profile.sub });
221
373
  return user;
@@ -228,9 +380,9 @@ export class Auth {
228
380
  */
229
381
  async getLogoutUrl(): Promise<string | undefined> {
230
382
  return withMetricsAsync(async () => {
231
- await this.authManager.removeUser();
383
+ await this.userManager.removeUser();
232
384
  this.eventEmitter.emit(AuthEvents.LOGGED_OUT);
233
- const url = await this.authManager.getLogoutUrl();
385
+ const url = await this.getLogoutUrlInternal();
234
386
  return url || undefined;
235
387
  }, 'getLogoutUrl');
236
388
  }
@@ -241,16 +393,7 @@ export class Auth {
241
393
  * @returns Promise that resolves when callback is handled
242
394
  */
243
395
  async logoutSilentCallback(url: string): Promise<void> {
244
- return withMetricsAsync(() => this.authManager.logoutSilentCallback(url), 'logoutSilentCallback');
245
- }
246
-
247
- /**
248
- * Get internal AuthManager instance
249
- * @internal
250
- * @returns AuthManager instance for advanced use cases
251
- */
252
- getAuthManager(): AuthManager {
253
- return this.authManager;
396
+ return withMetricsAsync(() => this.userManager.signoutSilentCallback(url), 'logoutSilentCallback');
254
397
  }
255
398
 
256
399
  /**
@@ -261,4 +404,427 @@ export class Auth {
261
404
  getConfig(): IAuthConfiguration {
262
405
  return this.config;
263
406
  }
407
+
408
+ /**
409
+ * Get the configured OIDC client ID
410
+ * @returns Promise that resolves with the client ID string
411
+ */
412
+ async getClientId(): Promise<string> {
413
+ return this.config.oidcConfiguration.clientId;
414
+ }
415
+
416
+ private buildExtraQueryParams(
417
+ directLoginOptions?: DirectLoginOptions,
418
+ imPassportTraceId?: string,
419
+ ): Record<string, string> {
420
+ const params: Record<string, string> = {
421
+ ...(this.userManager.settings?.extraQueryParams ?? {}),
422
+ rid: getDetail(Detail.RUNTIME_ID) || '',
423
+ third_party_a_id: '',
424
+ };
425
+
426
+ if (directLoginOptions) {
427
+ if (directLoginOptions.directLoginMethod === 'email') {
428
+ const emailValue = directLoginOptions.email;
429
+ if (emailValue) {
430
+ params.direct = directLoginOptions.directLoginMethod;
431
+ params.email = emailValue;
432
+ }
433
+ } else {
434
+ params.direct = directLoginOptions.directLoginMethod;
435
+ }
436
+ if (directLoginOptions.marketingConsentStatus) {
437
+ params.marketingConsent = directLoginOptions.marketingConsentStatus;
438
+ }
439
+ }
440
+
441
+ if (imPassportTraceId) {
442
+ params.im_passport_trace_id = imPassportTraceId;
443
+ }
444
+
445
+ return params;
446
+ }
447
+
448
+ private async loginWithRedirectInternal(directLoginOptions?: DirectLoginOptions): Promise<void> {
449
+ await this.userManager.clearStaleState();
450
+ await withPassportError<void>(async () => {
451
+ const extraQueryParams = this.buildExtraQueryParams(directLoginOptions);
452
+ await this.userManager.signinRedirect({ extraQueryParams });
453
+ }, PassportErrorType.AUTHENTICATION_ERROR);
454
+ }
455
+
456
+ private async loginWithPopup(directLoginOptions?: DirectLoginOptions): Promise<User> {
457
+ return withPassportError<User>(async () => {
458
+ let directLoginOptionsToUse: DirectLoginOptions | undefined;
459
+ let imPassportTraceId: string | undefined;
460
+ if (directLoginOptions) {
461
+ directLoginOptionsToUse = directLoginOptions;
462
+ } else if (!this.config.popupOverlayOptions?.disableHeadlessLoginPromptOverlay) {
463
+ const {
464
+ imPassportTraceId: embeddedLoginPromptTraceId,
465
+ ...embeddedLoginPromptDirectLoginOptions
466
+ } = await this.embeddedLoginPrompt.displayEmbeddedLoginPrompt();
467
+ directLoginOptionsToUse = embeddedLoginPromptDirectLoginOptions;
468
+ imPassportTraceId = embeddedLoginPromptTraceId;
469
+ }
470
+
471
+ const popupWindowTarget = window.crypto.randomUUID();
472
+ const signinPopup = async () => {
473
+ const extraQueryParams = this.buildExtraQueryParams(directLoginOptionsToUse, imPassportTraceId);
474
+
475
+ const userPromise = this.userManager.signinPopup({
476
+ extraQueryParams,
477
+ popupWindowFeatures: {
478
+ width: 410,
479
+ height: 450,
480
+ },
481
+ popupWindowTarget,
482
+ });
483
+
484
+ const popupRef = window.open('', popupWindowTarget);
485
+ if (popupRef) {
486
+ const popupClosedPromise = new Promise<never>((_, reject) => {
487
+ const timer = setInterval(() => {
488
+ if (popupRef.closed) {
489
+ clearInterval(timer);
490
+ reject(new Error('Popup closed by user'));
491
+ }
492
+ }, LOGIN_POPUP_CLOSED_POLLING_DURATION);
493
+
494
+ userPromise.finally(() => {
495
+ clearInterval(timer);
496
+ popupRef.close();
497
+ });
498
+ });
499
+
500
+ return Promise.race([userPromise, popupClosedPromise]);
501
+ }
502
+
503
+ return userPromise;
504
+ };
505
+
506
+ return new Promise((resolve, reject) => {
507
+ signinPopup()
508
+ .then((oidcUser) => resolve(Auth.mapOidcUserToDomainModel(oidcUser)))
509
+ .catch((error: unknown) => {
510
+ if (!(error instanceof Error) || error.message !== 'Attempted to navigate on a disposed window') {
511
+ reject(error);
512
+ return;
513
+ }
514
+
515
+ let popupHasBeenOpened = false;
516
+ const overlay = new LoginPopupOverlay(this.config.popupOverlayOptions || {}, true);
517
+ overlay.append(
518
+ async () => {
519
+ try {
520
+ if (!popupHasBeenOpened) {
521
+ popupHasBeenOpened = true;
522
+ const oidcUser = await signinPopup();
523
+ overlay.remove();
524
+ resolve(Auth.mapOidcUserToDomainModel(oidcUser));
525
+ } else {
526
+ window.open('', popupWindowTarget);
527
+ }
528
+ } catch (retryError) {
529
+ overlay.remove();
530
+ reject(retryError);
531
+ }
532
+ },
533
+ () => {
534
+ overlay.remove();
535
+ reject(new Error('Popup closed by user'));
536
+ },
537
+ );
538
+ });
539
+ });
540
+ }, PassportErrorType.AUTHENTICATION_ERROR);
541
+ }
542
+
543
+ private static mapOidcUserToDomainModel = (oidcUser: OidcUser): User => {
544
+ let passport: PassportMetadata | undefined;
545
+ if (oidcUser.id_token) {
546
+ passport = jwt_decode<IdTokenPayload>(oidcUser.id_token)?.passport;
547
+ }
548
+
549
+ const user: User = {
550
+ expired: oidcUser.expired,
551
+ idToken: oidcUser.id_token,
552
+ accessToken: oidcUser.access_token,
553
+ refreshToken: oidcUser.refresh_token,
554
+ profile: {
555
+ sub: oidcUser.profile.sub,
556
+ email: oidcUser.profile.email,
557
+ nickname: oidcUser.profile.nickname,
558
+ },
559
+ };
560
+ if (passport?.zkevm_eth_address && passport?.zkevm_user_admin_address) {
561
+ user.zkEvm = {
562
+ ethAddress: passport.zkevm_eth_address,
563
+ userAdminAddress: passport.zkevm_user_admin_address,
564
+ };
565
+ }
566
+ return user;
567
+ };
568
+
569
+ private static mapDeviceTokenResponseToOidcUser = (tokenResponse: DeviceTokenResponse): OidcUser => {
570
+ const idTokenPayload: IdTokenPayload = jwt_decode(tokenResponse.id_token);
571
+ return new OidcUser({
572
+ id_token: tokenResponse.id_token,
573
+ access_token: tokenResponse.access_token,
574
+ refresh_token: tokenResponse.refresh_token,
575
+ token_type: tokenResponse.token_type,
576
+ profile: {
577
+ sub: idTokenPayload.sub,
578
+ iss: idTokenPayload.iss,
579
+ aud: idTokenPayload.aud,
580
+ exp: idTokenPayload.exp,
581
+ iat: idTokenPayload.iat,
582
+ email: idTokenPayload.email,
583
+ nickname: idTokenPayload.nickname,
584
+ passport: idTokenPayload.passport,
585
+ },
586
+ });
587
+ };
588
+
589
+ private static shouldUseSigninPopupCallback(): boolean {
590
+ try {
591
+ const urlParams = new URLSearchParams(window.location.search);
592
+ const stateParam = urlParams.get('state');
593
+ const localStorageKey = `oidc.${stateParam}`;
594
+ const localStorageValue = localStorage.getItem(localStorageKey);
595
+ const loginState = JSON.parse(localStorageValue || '{}');
596
+ return loginState?.request_type === 'si:p';
597
+ } catch (err) {
598
+ return false;
599
+ }
600
+ }
601
+
602
+ private async loginCallbackInternal(): Promise<User | undefined> {
603
+ return withPassportError(async () => {
604
+ if (Auth.shouldUseSigninPopupCallback()) {
605
+ await this.userManager.signinPopupCallback(undefined, true);
606
+ return undefined;
607
+ }
608
+ const oidcUser = await this.userManager.signinCallback();
609
+ if (!oidcUser) {
610
+ return undefined;
611
+ }
612
+ return Auth.mapOidcUserToDomainModel(oidcUser);
613
+ }, PassportErrorType.AUTHENTICATION_ERROR);
614
+ }
615
+
616
+ private async getPKCEAuthorizationUrl(
617
+ directLoginOptions?: DirectLoginOptions,
618
+ imPassportTraceId?: string,
619
+ ): Promise<string> {
620
+ const verifier = base64URLEncode(window.crypto.getRandomValues(new Uint8Array(32)));
621
+ const challenge = base64URLEncode(await sha256(verifier));
622
+ const state = base64URLEncode(window.crypto.getRandomValues(new Uint8Array(32)));
623
+
624
+ const {
625
+ redirectUri, scope, audience, clientId,
626
+ } = this.config.oidcConfiguration;
627
+
628
+ this.deviceCredentialsManager.savePKCEData({ state, verifier });
629
+
630
+ const pkceAuthorizationUrl = new URL(authorizeEndpoint, this.config.authenticationDomain);
631
+ pkceAuthorizationUrl.searchParams.set('response_type', 'code');
632
+ pkceAuthorizationUrl.searchParams.set('code_challenge', challenge);
633
+ pkceAuthorizationUrl.searchParams.set('code_challenge_method', 'S256');
634
+ pkceAuthorizationUrl.searchParams.set('client_id', clientId);
635
+ pkceAuthorizationUrl.searchParams.set('redirect_uri', redirectUri);
636
+ pkceAuthorizationUrl.searchParams.set('state', state);
637
+
638
+ if (scope) pkceAuthorizationUrl.searchParams.set('scope', scope);
639
+ if (audience) pkceAuthorizationUrl.searchParams.set('audience', audience);
640
+
641
+ if (directLoginOptions) {
642
+ if (directLoginOptions.directLoginMethod === 'email') {
643
+ const emailValue = directLoginOptions.email;
644
+ if (emailValue) {
645
+ pkceAuthorizationUrl.searchParams.set('direct', directLoginOptions.directLoginMethod);
646
+ pkceAuthorizationUrl.searchParams.set('email', emailValue);
647
+ }
648
+ } else {
649
+ pkceAuthorizationUrl.searchParams.set('direct', directLoginOptions.directLoginMethod);
650
+ }
651
+ if (directLoginOptions.marketingConsentStatus) {
652
+ pkceAuthorizationUrl.searchParams.set('marketingConsent', directLoginOptions.marketingConsentStatus);
653
+ }
654
+ }
655
+
656
+ if (imPassportTraceId) {
657
+ pkceAuthorizationUrl.searchParams.set('im_passport_trace_id', imPassportTraceId);
658
+ }
659
+
660
+ return pkceAuthorizationUrl.toString();
661
+ }
662
+
663
+ private async loginWithPKCEFlowCallbackInternal(authorizationCode: string, state: string): Promise<User> {
664
+ return withPassportError(async () => {
665
+ const pkceData = this.deviceCredentialsManager.getPKCEData();
666
+ if (!pkceData) {
667
+ throw new Error('No code verifier or state for PKCE');
668
+ }
669
+
670
+ if (state !== pkceData.state) {
671
+ throw new Error('Provided state does not match stored state');
672
+ }
673
+
674
+ const tokenResponse = await this.getPKCEToken(authorizationCode, pkceData.verifier);
675
+ const oidcUser = Auth.mapDeviceTokenResponseToOidcUser(tokenResponse);
676
+ const user = Auth.mapOidcUserToDomainModel(oidcUser);
677
+ await this.userManager.storeUser(oidcUser);
678
+
679
+ return user;
680
+ }, PassportErrorType.AUTHENTICATION_ERROR);
681
+ }
682
+
683
+ private async getPKCEToken(authorizationCode: string, codeVerifier: string): Promise<DeviceTokenResponse> {
684
+ const response = await axios.post<DeviceTokenResponse>(
685
+ `${this.config.authenticationDomain}/oauth/token`,
686
+ {
687
+ client_id: this.config.oidcConfiguration.clientId,
688
+ grant_type: 'authorization_code',
689
+ code_verifier: codeVerifier,
690
+ code: authorizationCode,
691
+ redirect_uri: this.config.oidcConfiguration.redirectUri,
692
+ },
693
+ formUrlEncodedHeader,
694
+ );
695
+ return response.data;
696
+ }
697
+
698
+ private async storeTokensInternal(tokenResponse: DeviceTokenResponse): Promise<User> {
699
+ return withPassportError(async () => {
700
+ const oidcUser = Auth.mapDeviceTokenResponseToOidcUser(tokenResponse);
701
+ const user = Auth.mapOidcUserToDomainModel(oidcUser);
702
+ await this.userManager.storeUser(oidcUser);
703
+ return user;
704
+ }, PassportErrorType.AUTHENTICATION_ERROR);
705
+ }
706
+
707
+ private async logoutInternal(): Promise<void> {
708
+ await withPassportError(async () => {
709
+ await this.userManager.revokeTokens(['refresh_token']);
710
+ if (this.logoutMode === 'silent') {
711
+ await this.userManager.signoutSilent();
712
+ } else {
713
+ await this.userManager.signoutRedirect();
714
+ }
715
+ }, PassportErrorType.LOGOUT_ERROR);
716
+ }
717
+
718
+ private async getLogoutUrlInternal(): Promise<string | null> {
719
+ const endSessionEndpoint = this.userManager.settings?.metadata?.end_session_endpoint;
720
+ if (!endSessionEndpoint) {
721
+ logger.warn('Failed to get logout URL');
722
+ return null;
723
+ }
724
+ return endSessionEndpoint;
725
+ }
726
+
727
+ private forceUserRefreshInBackgroundInternal() {
728
+ this.refreshTokenAndUpdatePromise().catch((error) => {
729
+ logger.warn('Failed to refresh user token', error);
730
+ });
731
+ }
732
+
733
+ private async forceUserRefreshInternal(): Promise<User | null> {
734
+ return this.refreshTokenAndUpdatePromise().catch((error) => {
735
+ logger.warn('Failed to refresh user token', error);
736
+ return null;
737
+ });
738
+ }
739
+
740
+ private async refreshTokenAndUpdatePromise(): Promise<User | null> {
741
+ if (this.refreshingPromise) {
742
+ return this.refreshingPromise;
743
+ }
744
+
745
+ this.refreshingPromise = new Promise((resolve, reject) => {
746
+ (async () => {
747
+ try {
748
+ const newOidcUser = await this.userManager.signinSilent();
749
+ if (newOidcUser) {
750
+ resolve(Auth.mapOidcUserToDomainModel(newOidcUser));
751
+ return;
752
+ }
753
+ resolve(null);
754
+ } catch (err) {
755
+ let passportErrorType = PassportErrorType.AUTHENTICATION_ERROR;
756
+ let errorMessage = 'Failed to refresh token';
757
+ let removeUser = true;
758
+
759
+ if (err instanceof ErrorTimeout) {
760
+ passportErrorType = PassportErrorType.SILENT_LOGIN_ERROR;
761
+ errorMessage = `${errorMessage}: ${err.message}`;
762
+ removeUser = false;
763
+ } else if (err instanceof ErrorResponse) {
764
+ passportErrorType = PassportErrorType.NOT_LOGGED_IN_ERROR;
765
+ errorMessage = `${errorMessage}: ${err.message || err.error_description}`;
766
+ } else if (err instanceof Error) {
767
+ errorMessage = `${errorMessage}: ${err.message}`;
768
+ } else if (typeof err === 'string') {
769
+ errorMessage = `${errorMessage}: ${err}`;
770
+ }
771
+
772
+ if (removeUser) {
773
+ try {
774
+ await this.userManager.removeUser();
775
+ } catch (removeUserError) {
776
+ if (removeUserError instanceof Error) {
777
+ errorMessage = `${errorMessage}: Failed to remove user: ${removeUserError.message}`;
778
+ }
779
+ }
780
+ }
781
+
782
+ reject(new PassportError(errorMessage, passportErrorType));
783
+ } finally {
784
+ this.refreshingPromise = null;
785
+ }
786
+ })();
787
+ });
788
+
789
+ return this.refreshingPromise;
790
+ }
791
+
792
+ private async getUserInternal<T extends User>(
793
+ typeAssertion: (user: User) => user is T = (user: User): user is T => true,
794
+ ): Promise<T | null> {
795
+ if (this.refreshingPromise) {
796
+ const refreshingUser = await this.refreshingPromise;
797
+ if (refreshingUser && typeAssertion(refreshingUser)) {
798
+ return refreshingUser;
799
+ }
800
+ return null;
801
+ }
802
+
803
+ const oidcUser = await this.userManager.getUser();
804
+ if (!oidcUser) return null;
805
+
806
+ if (!isAccessTokenExpiredOrExpiring(oidcUser)) {
807
+ const user = Auth.mapOidcUserToDomainModel(oidcUser);
808
+ if (user && typeAssertion(user)) {
809
+ return user;
810
+ }
811
+ }
812
+
813
+ if (oidcUser.refresh_token) {
814
+ const refreshedUser = await this.refreshTokenAndUpdatePromise();
815
+ if (refreshedUser && typeAssertion(refreshedUser)) {
816
+ return refreshedUser;
817
+ }
818
+ }
819
+
820
+ return null;
821
+ }
822
+
823
+ private async getUserZkEvmInternal(): Promise<UserZkEvm> {
824
+ const user = await this.getUserInternal(isUserZkEvm);
825
+ if (!user) {
826
+ throw new Error('Failed to obtain a User with the required ZkEvm attributes');
827
+ }
828
+ return user;
829
+ }
264
830
  }