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