@imtbl/auth 2.10.7-alpha.4 → 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,20 +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
- import { identify, track } from '@imtbl/metrics';
37
+ import { withMetricsAsync } from './utils/metrics';
38
+ import DeviceCredentialsManager from './storage/device_credentials_manager';
39
+ import { PassportError, PassportErrorType, withPassportError } from './errors';
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
+ }
9
117
 
10
118
  /**
11
119
  * Public-facing Auth class for authentication
12
- * Wraps AuthManager with a simpler API
120
+ * Provides login/logout helpers and exposes auth state events
13
121
  */
14
122
  export class Auth {
15
- private authManager: AuthManager;
123
+ private readonly config: IAuthConfiguration;
124
+
125
+ private readonly userManager: UserManager;
126
+
127
+ private readonly deviceCredentialsManager: DeviceCredentialsManager;
16
128
 
17
- 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;
18
137
 
19
138
  /**
20
139
  * Event emitter for authentication events (LOGGED_IN, LOGGED_OUT)
@@ -42,87 +161,83 @@ export class Auth {
42
161
  */
43
162
  constructor(config: AuthModuleConfiguration) {
44
163
  this.config = new AuthConfiguration(config);
45
- const embeddedLoginPrompt = new EmbeddedLoginPrompt(this.config);
46
- 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';
47
168
  this.eventEmitter = new TypedEventEmitter<AuthEventMap>();
48
169
  track('passport', 'initialise');
49
170
  }
50
171
 
51
172
  /**
52
- * Login with popup
53
- * Opens a popup window for authentication
54
- * @param directLoginOptions - Optional direct login options
55
- * @returns Promise that resolves with the authenticated user
56
- */
57
- async login(directLoginOptions?: DirectLoginOptions): Promise<User> {
58
- return this.authManager.login(directLoginOptions);
59
- }
60
-
61
- /**
62
- * Login with redirect
63
- * Redirects the page for authentication
64
- * @param directLoginOptions - Optional direct login options
65
- * @returns Promise that resolves when redirect is initiated
66
- */
67
- async loginWithRedirect(directLoginOptions?: DirectLoginOptions): Promise<void> {
68
- await this.authManager.loginWithRedirect(directLoginOptions);
69
- }
70
-
71
- /**
72
- * Enhanced login method with extended options
73
- * Supports cached sessions, silent login, and redirect flow
173
+ * Login the user with extended options
174
+ * Supports cached sessions, silent login, redirect flow, and direct login
74
175
  * @param options - Extended login options
75
176
  * @returns Promise that resolves with the user or null
76
177
  */
77
- async loginWithOptions(options?: LoginOptions): Promise<User | null> {
78
- const { useCachedSession = false, useSilentLogin } = options || {};
79
- let user: User | null = null;
178
+ async login(options?: LoginOptions): Promise<User | null> {
179
+ return withMetricsAsync(async () => {
180
+ const { useCachedSession = false, useSilentLogin } = options || {};
181
+ let user: User | null = null;
80
182
 
81
- // Try to get cached user
82
- try {
83
- user = await this.authManager.getUser();
84
- } catch (error: any) {
85
- if (useCachedSession) {
86
- throw error;
183
+ // Try to get cached user
184
+ try {
185
+ user = await this.getUserInternal();
186
+ } catch (error: any) {
187
+ if (error instanceof Error && !error.message.includes('Unknown or invalid refresh token')) {
188
+ trackError('passport', 'login', error);
189
+ }
190
+ if (useCachedSession) {
191
+ throw error;
192
+ }
193
+ logger.warn('Failed to retrieve a cached user session', error);
87
194
  }
88
- // Silently ignore errors if not requiring cached session
89
- }
90
195
 
91
- // If no cached user, try silent login or regular login
92
- if (!user && useSilentLogin) {
93
- user = await this.authManager.forceUserRefresh();
94
- }
196
+ // If no cached user, try silent login or regular login
197
+ if (!user && useSilentLogin) {
198
+ user = await this.forceUserRefreshInternal();
199
+ } else if (!user && !useCachedSession) {
200
+ if (options?.useRedirectFlow) {
201
+ await this.loginWithRedirectInternal(options?.directLoginOptions);
202
+ return null; // Redirect doesn't return user immediately
203
+ }
204
+ user = await this.loginWithPopup(options?.directLoginOptions);
205
+ }
95
206
 
96
- if (!user && !useCachedSession) {
97
- if (options?.useRedirectFlow) {
98
- await this.authManager.loginWithRedirect(options?.directLoginOptions);
99
- return null; // Redirect doesn't return user immediately
207
+ // Emit LOGGED_IN event and identify user if logged in
208
+ if (user) {
209
+ this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
210
+ identify({ passportId: user.profile.sub });
100
211
  }
101
- user = await this.authManager.login(options?.directLoginOptions);
102
- }
103
212
 
104
- // Emit LOGGED_IN event and identify user if logged in
105
- if (user) {
106
- this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
107
- identify({ passportId: user.profile.sub });
108
- }
213
+ return user;
214
+ }, 'login');
215
+ }
109
216
 
110
- return user;
217
+ /**
218
+ * Login with redirect
219
+ * Redirects the page for authentication
220
+ * @param directLoginOptions - Optional direct login options
221
+ * @returns Promise that resolves when redirect is initiated
222
+ */
223
+ async loginWithRedirect(directLoginOptions?: DirectLoginOptions): Promise<void> {
224
+ await this.loginWithRedirectInternal(directLoginOptions);
111
225
  }
112
226
 
113
227
  /**
114
228
  * Login callback handler
115
- * Call this in your redirect URI page
116
- * @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)
117
231
  */
118
- async loginCallback(): Promise<User> {
119
- const user = await this.authManager.loginCallback();
120
- if (!user) {
121
- throw new Error('Login callback failed - no user returned');
122
- }
123
- this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
124
- identify({ passportId: user.profile.sub });
125
- return user;
232
+ async loginCallback(): Promise<User | undefined> {
233
+ return withMetricsAsync(async () => {
234
+ const user = await this.loginCallbackInternal();
235
+ if (user) {
236
+ this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
237
+ identify({ passportId: user.profile.sub });
238
+ }
239
+ return user;
240
+ }, 'loginCallback');
126
241
  }
127
242
 
128
243
  /**
@@ -130,8 +245,10 @@ export class Auth {
130
245
  * @returns Promise that resolves when logout is complete
131
246
  */
132
247
  async logout(): Promise<void> {
133
- await this.authManager.logout();
134
- this.eventEmitter.emit(AuthEvents.LOGGED_OUT);
248
+ await withMetricsAsync(async () => {
249
+ await this.logoutInternal();
250
+ this.eventEmitter.emit(AuthEvents.LOGGED_OUT);
251
+ }, 'logout');
135
252
  }
136
253
 
137
254
  /**
@@ -139,7 +256,34 @@ export class Auth {
139
256
  * @returns Promise that resolves with the user or null if not authenticated
140
257
  */
141
258
  async getUser(): Promise<User | null> {
142
- return this.authManager.getUser();
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();
143
287
  }
144
288
 
145
289
  /**
@@ -147,8 +291,10 @@ export class Auth {
147
291
  * @returns Promise that resolves with the ID token or undefined
148
292
  */
149
293
  async getIdToken(): Promise<string | undefined> {
150
- const user = await this.authManager.getUser();
151
- return user?.idToken;
294
+ return withMetricsAsync(async () => {
295
+ const user = await this.getUserInternal();
296
+ return user?.idToken;
297
+ }, 'getIdToken', false);
152
298
  }
153
299
 
154
300
  /**
@@ -156,8 +302,10 @@ export class Auth {
156
302
  * @returns Promise that resolves with the access token or undefined
157
303
  */
158
304
  async getAccessToken(): Promise<string | undefined> {
159
- const user = await this.authManager.getUser();
160
- return user?.accessToken;
305
+ return withMetricsAsync(async () => {
306
+ const user = await this.getUserInternal();
307
+ return user?.accessToken;
308
+ }, 'getAccessToken', false, false);
161
309
  }
162
310
 
163
311
  /**
@@ -174,7 +322,14 @@ export class Auth {
174
322
  * @returns Promise that resolves with the user or null if refresh fails
175
323
  */
176
324
  async forceUserRefresh(): Promise<User | null> {
177
- 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();
178
333
  }
179
334
 
180
335
  /**
@@ -184,7 +339,10 @@ export class Auth {
184
339
  * @returns Promise that resolves with the authorization URL
185
340
  */
186
341
  async loginWithPKCEFlow(directLoginOptions?: DirectLoginOptions, imPassportTraceId?: string): Promise<string> {
187
- return this.authManager.getPKCEAuthorizationUrl(directLoginOptions, imPassportTraceId);
342
+ return withMetricsAsync(
343
+ async () => this.getPKCEAuthorizationUrl(directLoginOptions, imPassportTraceId),
344
+ 'loginWithPKCEFlow',
345
+ );
188
346
  }
189
347
 
190
348
  /**
@@ -194,10 +352,12 @@ export class Auth {
194
352
  * @returns Promise that resolves with the authenticated user
195
353
  */
196
354
  async loginWithPKCEFlowCallback(authorizationCode: string, state: string): Promise<User> {
197
- const user = await this.authManager.loginWithPKCEFlowCallback(authorizationCode, state);
198
- this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
199
- identify({ passportId: user.profile.sub });
200
- return user;
355
+ return withMetricsAsync(async () => {
356
+ const user = await this.loginWithPKCEFlowCallbackInternal(authorizationCode, state);
357
+ this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
358
+ identify({ passportId: user.profile.sub });
359
+ return user;
360
+ }, 'loginWithPKCEFlowCallback');
201
361
  }
202
362
 
203
363
  /**
@@ -206,10 +366,12 @@ export class Auth {
206
366
  * @returns Promise that resolves with the authenticated user
207
367
  */
208
368
  async storeTokens(tokenResponse: DeviceTokenResponse): Promise<User> {
209
- const user = await this.authManager.storeTokens(tokenResponse);
210
- this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
211
- identify({ passportId: user.profile.sub });
212
- return user;
369
+ return withMetricsAsync(async () => {
370
+ const user = await this.storeTokensInternal(tokenResponse);
371
+ this.eventEmitter.emit(AuthEvents.LOGGED_IN, user);
372
+ identify({ passportId: user.profile.sub });
373
+ return user;
374
+ }, 'storeTokens');
213
375
  }
214
376
 
215
377
  /**
@@ -217,10 +379,12 @@ export class Auth {
217
379
  * @returns Promise that resolves with the logout URL or undefined if not available
218
380
  */
219
381
  async getLogoutUrl(): Promise<string | undefined> {
220
- await this.authManager.removeUser();
221
- this.eventEmitter.emit(AuthEvents.LOGGED_OUT);
222
- const url = await this.authManager.getLogoutUrl();
223
- return url || undefined;
382
+ return withMetricsAsync(async () => {
383
+ await this.userManager.removeUser();
384
+ this.eventEmitter.emit(AuthEvents.LOGGED_OUT);
385
+ const url = await this.getLogoutUrlInternal();
386
+ return url || undefined;
387
+ }, 'getLogoutUrl');
224
388
  }
225
389
 
226
390
  /**
@@ -229,16 +393,7 @@ export class Auth {
229
393
  * @returns Promise that resolves when callback is handled
230
394
  */
231
395
  async logoutSilentCallback(url: string): Promise<void> {
232
- return this.authManager.logoutSilentCallback(url);
233
- }
234
-
235
- /**
236
- * Get internal AuthManager instance
237
- * @internal
238
- * @returns AuthManager instance for advanced use cases
239
- */
240
- getAuthManager(): AuthManager {
241
- return this.authManager;
396
+ return withMetricsAsync(() => this.userManager.signoutSilentCallback(url), 'logoutSilentCallback');
242
397
  }
243
398
 
244
399
  /**
@@ -249,4 +404,427 @@ export class Auth {
249
404
  getConfig(): IAuthConfiguration {
250
405
  return this.config;
251
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
+ }
252
830
  }