@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/dist/browser/index.js +29 -29
- package/dist/node/index.cjs +34 -36
- package/dist/node/index.js +23 -23
- package/dist/types/Auth.d.ts +56 -26
- package/dist/types/index.d.ts +2 -3
- package/dist/types/types.d.ts +10 -17
- package/dist/types/utils/metrics.d.ts +2 -0
- package/package.json +4 -4
- package/src/Auth.ts +676 -98
- package/src/index.ts +1 -5
- package/src/types.ts +7 -17
- package/src/utils/metrics.ts +29 -0
- package/dist/types/authManager.d.ts +0 -61
- package/src/authManager.ts +0 -657
package/src/authManager.ts
DELETED
|
@@ -1,657 +0,0 @@
|
|
|
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 { getDetail, Detail } from '@imtbl/metrics';
|
|
13
|
-
import localForage from 'localforage';
|
|
14
|
-
import DeviceCredentialsManager from './storage/device_credentials_manager';
|
|
15
|
-
import logger from './utils/logger';
|
|
16
|
-
import { isAccessTokenExpiredOrExpiring } from './utils/token';
|
|
17
|
-
import { PassportError, PassportErrorType, withPassportError } from './errors';
|
|
18
|
-
import {
|
|
19
|
-
DirectLoginOptions,
|
|
20
|
-
PassportMetadata,
|
|
21
|
-
User,
|
|
22
|
-
DeviceTokenResponse,
|
|
23
|
-
IdTokenPayload,
|
|
24
|
-
OidcConfiguration,
|
|
25
|
-
UserZkEvm,
|
|
26
|
-
isUserZkEvm,
|
|
27
|
-
UserImx,
|
|
28
|
-
isUserImx,
|
|
29
|
-
} from './types';
|
|
30
|
-
import { IAuthConfiguration } from './config';
|
|
31
|
-
import LoginPopupOverlay from './overlay/loginPopupOverlay';
|
|
32
|
-
import EmbeddedLoginPrompt from './login/embeddedLoginPrompt';
|
|
33
|
-
import { LocalForageAsyncStorage } from './storage/LocalForageAsyncStorage';
|
|
34
|
-
|
|
35
|
-
const LOGIN_POPUP_CLOSED_POLLING_DURATION = 500;
|
|
36
|
-
|
|
37
|
-
const formUrlEncodedHeader = {
|
|
38
|
-
headers: {
|
|
39
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const logoutEndpoint = '/v2/logout';
|
|
44
|
-
const crossSdkBridgeLogoutEndpoint = '/im-logged-out';
|
|
45
|
-
const authorizeEndpoint = '/authorize';
|
|
46
|
-
|
|
47
|
-
const getLogoutEndpointPath = (crossSdkBridgeEnabled: boolean): string => (
|
|
48
|
-
crossSdkBridgeEnabled ? crossSdkBridgeLogoutEndpoint : logoutEndpoint
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const getAuthConfiguration = (config: IAuthConfiguration): UserManagerSettings => {
|
|
52
|
-
const { authenticationDomain, oidcConfiguration } = config;
|
|
53
|
-
|
|
54
|
-
let store;
|
|
55
|
-
if (config.crossSdkBridgeEnabled) {
|
|
56
|
-
store = new LocalForageAsyncStorage('ImmutableSDKPassport', localForage.INDEXEDDB);
|
|
57
|
-
} else if (typeof window !== 'undefined') {
|
|
58
|
-
store = window.localStorage;
|
|
59
|
-
} else {
|
|
60
|
-
store = new InMemoryWebStorage();
|
|
61
|
-
}
|
|
62
|
-
const userStore = new WebStorageStateStore({ store });
|
|
63
|
-
|
|
64
|
-
const endSessionEndpoint = new URL(
|
|
65
|
-
getLogoutEndpointPath(config.crossSdkBridgeEnabled),
|
|
66
|
-
authenticationDomain.replace(/^(?:https?:\/\/)?(.*)/, 'https://$1'),
|
|
67
|
-
);
|
|
68
|
-
endSessionEndpoint.searchParams.set('client_id', oidcConfiguration.clientId);
|
|
69
|
-
if (oidcConfiguration.logoutRedirectUri) {
|
|
70
|
-
endSessionEndpoint.searchParams.set('returnTo', oidcConfiguration.logoutRedirectUri);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const baseConfiguration: UserManagerSettings = {
|
|
74
|
-
authority: authenticationDomain,
|
|
75
|
-
redirect_uri: oidcConfiguration.redirectUri,
|
|
76
|
-
popup_redirect_uri: oidcConfiguration.popupRedirectUri || oidcConfiguration.redirectUri,
|
|
77
|
-
client_id: oidcConfiguration.clientId,
|
|
78
|
-
metadata: {
|
|
79
|
-
authorization_endpoint: `${authenticationDomain}/authorize`,
|
|
80
|
-
token_endpoint: `${authenticationDomain}/oauth/token`,
|
|
81
|
-
userinfo_endpoint: `${authenticationDomain}/userinfo`,
|
|
82
|
-
end_session_endpoint: endSessionEndpoint.toString(),
|
|
83
|
-
revocation_endpoint: `${authenticationDomain}/oauth/revoke`,
|
|
84
|
-
},
|
|
85
|
-
automaticSilentRenew: false, // Disabled until https://github.com/authts/oidc-client-ts/issues/430 has been resolved
|
|
86
|
-
scope: oidcConfiguration.scope,
|
|
87
|
-
userStore,
|
|
88
|
-
revokeTokenTypes: ['refresh_token'],
|
|
89
|
-
extraQueryParams: {
|
|
90
|
-
...(oidcConfiguration.audience ? { audience: oidcConfiguration.audience } : {}),
|
|
91
|
-
},
|
|
92
|
-
} as UserManagerSettings;
|
|
93
|
-
|
|
94
|
-
return baseConfiguration;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
function base64URLEncode(str: ArrayBuffer | Uint8Array) {
|
|
98
|
-
return btoa(String.fromCharCode(...new Uint8Array(str)))
|
|
99
|
-
.replace(/\+/g, '-')
|
|
100
|
-
.replace(/\//g, '_')
|
|
101
|
-
.replace(/=/g, '');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function sha256(buffer: string) {
|
|
105
|
-
const encoder = new TextEncoder();
|
|
106
|
-
const data = encoder.encode(buffer);
|
|
107
|
-
return await window.crypto.subtle.digest('SHA-256', data);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export default class AuthManager {
|
|
111
|
-
private userManager;
|
|
112
|
-
|
|
113
|
-
private deviceCredentialsManager: DeviceCredentialsManager;
|
|
114
|
-
|
|
115
|
-
private readonly config: IAuthConfiguration;
|
|
116
|
-
|
|
117
|
-
private readonly embeddedLoginPrompt: EmbeddedLoginPrompt;
|
|
118
|
-
|
|
119
|
-
private readonly logoutMode: Exclude<OidcConfiguration['logoutMode'], undefined>;
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Promise that is used to prevent multiple concurrent calls to the refresh token endpoint.
|
|
123
|
-
*/
|
|
124
|
-
private refreshingPromise: Promise<User | null> | null = null;
|
|
125
|
-
|
|
126
|
-
constructor(config: IAuthConfiguration, embeddedLoginPrompt: EmbeddedLoginPrompt) {
|
|
127
|
-
this.config = config;
|
|
128
|
-
this.userManager = new UserManager(getAuthConfiguration(config));
|
|
129
|
-
this.deviceCredentialsManager = new DeviceCredentialsManager();
|
|
130
|
-
this.embeddedLoginPrompt = embeddedLoginPrompt;
|
|
131
|
-
this.logoutMode = config.oidcConfiguration.logoutMode || 'redirect';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
private static mapOidcUserToDomainModel = (oidcUser: OidcUser): User => {
|
|
135
|
-
let passport: PassportMetadata | undefined;
|
|
136
|
-
if (oidcUser.id_token) {
|
|
137
|
-
passport = jwt_decode<IdTokenPayload>(oidcUser.id_token)?.passport;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const user: User = {
|
|
141
|
-
expired: oidcUser.expired,
|
|
142
|
-
idToken: oidcUser.id_token,
|
|
143
|
-
accessToken: oidcUser.access_token,
|
|
144
|
-
refreshToken: oidcUser.refresh_token,
|
|
145
|
-
profile: {
|
|
146
|
-
sub: oidcUser.profile.sub,
|
|
147
|
-
email: oidcUser.profile.email,
|
|
148
|
-
nickname: oidcUser.profile.nickname,
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
if (passport?.imx_eth_address) {
|
|
152
|
-
user.imx = {
|
|
153
|
-
ethAddress: passport.imx_eth_address,
|
|
154
|
-
starkAddress: passport.imx_stark_address,
|
|
155
|
-
userAdminAddress: passport.imx_user_admin_address,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
if (passport?.zkevm_eth_address) {
|
|
159
|
-
user.zkEvm = {
|
|
160
|
-
ethAddress: passport?.zkevm_eth_address,
|
|
161
|
-
userAdminAddress: passport?.zkevm_user_admin_address,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return user;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
private static mapDeviceTokenResponseToOidcUser = (tokenResponse: DeviceTokenResponse): OidcUser => {
|
|
168
|
-
const idTokenPayload: IdTokenPayload = jwt_decode(tokenResponse.id_token);
|
|
169
|
-
|
|
170
|
-
return new OidcUser({
|
|
171
|
-
id_token: tokenResponse.id_token,
|
|
172
|
-
access_token: tokenResponse.access_token,
|
|
173
|
-
refresh_token: tokenResponse.refresh_token,
|
|
174
|
-
token_type: tokenResponse.token_type,
|
|
175
|
-
profile: {
|
|
176
|
-
sub: idTokenPayload.sub,
|
|
177
|
-
iss: idTokenPayload.iss,
|
|
178
|
-
aud: idTokenPayload.aud,
|
|
179
|
-
exp: idTokenPayload.exp,
|
|
180
|
-
iat: idTokenPayload.iat,
|
|
181
|
-
email: idTokenPayload.email,
|
|
182
|
-
nickname: idTokenPayload.nickname,
|
|
183
|
-
passport: idTokenPayload.passport,
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
private buildExtraQueryParams(
|
|
189
|
-
directLoginOptions?: DirectLoginOptions,
|
|
190
|
-
imPassportTraceId?: string,
|
|
191
|
-
): Record<string, string> {
|
|
192
|
-
const params: Record<string, string> = {
|
|
193
|
-
...(this.userManager.settings?.extraQueryParams ?? {}),
|
|
194
|
-
rid: getDetail(Detail.RUNTIME_ID) || '',
|
|
195
|
-
third_party_a_id: '',
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
if (directLoginOptions) {
|
|
199
|
-
// If method is email, only include direct login params if email is valid
|
|
200
|
-
if (directLoginOptions.directLoginMethod === 'email') {
|
|
201
|
-
const emailValue = directLoginOptions.email;
|
|
202
|
-
if (emailValue) {
|
|
203
|
-
params.direct = directLoginOptions.directLoginMethod;
|
|
204
|
-
params.email = emailValue;
|
|
205
|
-
}
|
|
206
|
-
// If email method but no valid email, disregard both direct and email params
|
|
207
|
-
} else {
|
|
208
|
-
// For non-email methods (social login), always include direct param
|
|
209
|
-
params.direct = directLoginOptions.directLoginMethod;
|
|
210
|
-
}
|
|
211
|
-
if (directLoginOptions.marketingConsentStatus) {
|
|
212
|
-
params.marketingConsent = directLoginOptions.marketingConsentStatus;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (imPassportTraceId) {
|
|
217
|
-
params.im_passport_trace_id = imPassportTraceId;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return params;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
public async loginWithRedirect(directLoginOptions?: DirectLoginOptions): Promise<void> {
|
|
224
|
-
await this.userManager.clearStaleState();
|
|
225
|
-
return withPassportError<void>(async () => {
|
|
226
|
-
const extraQueryParams = this.buildExtraQueryParams(directLoginOptions);
|
|
227
|
-
|
|
228
|
-
await this.userManager.signinRedirect({
|
|
229
|
-
extraQueryParams,
|
|
230
|
-
});
|
|
231
|
-
}, PassportErrorType.AUTHENTICATION_ERROR);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* login
|
|
236
|
-
* @param directLoginOptions If provided, contains login method and marketing consent options
|
|
237
|
-
* @param directLoginOptions.directLoginMethod The login method to use (e.g., 'google', 'apple', 'email')
|
|
238
|
-
* @param directLoginOptions.marketingConsentStatus Marketing consent status ('opted_in' or 'unsubscribed')
|
|
239
|
-
* @param directLoginOptions.email Required when directLoginMethod is 'email'
|
|
240
|
-
*/
|
|
241
|
-
public async login(directLoginOptions?: DirectLoginOptions): Promise<User> {
|
|
242
|
-
return withPassportError<User>(async () => {
|
|
243
|
-
// If directLoginOptions are provided, then the consumer has rendered their own initial login screen.
|
|
244
|
-
// If not, display the embedded login prompt and pass the returned direct login options and imPassportTraceId to the login popup.
|
|
245
|
-
let directLoginOptionsToUse: DirectLoginOptions | undefined;
|
|
246
|
-
let imPassportTraceId: string | undefined;
|
|
247
|
-
if (directLoginOptions) {
|
|
248
|
-
directLoginOptionsToUse = directLoginOptions;
|
|
249
|
-
} else if (!this.config.popupOverlayOptions?.disableHeadlessLoginPromptOverlay) {
|
|
250
|
-
const {
|
|
251
|
-
imPassportTraceId: embeddedLoginPromptImPassportTraceId,
|
|
252
|
-
...embeddedLoginPromptDirectLoginOptions
|
|
253
|
-
} = await this.embeddedLoginPrompt.displayEmbeddedLoginPrompt();
|
|
254
|
-
directLoginOptionsToUse = embeddedLoginPromptDirectLoginOptions;
|
|
255
|
-
imPassportTraceId = embeddedLoginPromptImPassportTraceId;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const popupWindowTarget = window.crypto.randomUUID();
|
|
259
|
-
const signinPopup = async () => {
|
|
260
|
-
const extraQueryParams = this.buildExtraQueryParams(directLoginOptionsToUse, imPassportTraceId);
|
|
261
|
-
|
|
262
|
-
const userPromise = this.userManager.signinPopup({
|
|
263
|
-
extraQueryParams,
|
|
264
|
-
popupWindowFeatures: {
|
|
265
|
-
width: 410,
|
|
266
|
-
height: 450,
|
|
267
|
-
},
|
|
268
|
-
popupWindowTarget,
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// ID-3950: https://github.com/authts/oidc-client-ts/issues/2043
|
|
272
|
-
// The promise returned from `signinPopup` no longer rejects when the popup is closed.
|
|
273
|
-
// We can prevent this from impacting consumers by obtaining a reference to the popup and rejecting the promise
|
|
274
|
-
// that is returned by this method if the popup is closed by the user.
|
|
275
|
-
|
|
276
|
-
// Attempt to get a reference to the popup window
|
|
277
|
-
const popupRef = window.open('', popupWindowTarget);
|
|
278
|
-
if (popupRef) {
|
|
279
|
-
// Create a promise that rejects when popup is closed
|
|
280
|
-
const popupClosedPromise = new Promise<never>((_, reject) => {
|
|
281
|
-
const timer = setInterval(() => {
|
|
282
|
-
if (popupRef.closed) {
|
|
283
|
-
clearInterval(timer);
|
|
284
|
-
reject(new Error('Popup closed by user'));
|
|
285
|
-
}
|
|
286
|
-
}, LOGIN_POPUP_CLOSED_POLLING_DURATION);
|
|
287
|
-
|
|
288
|
-
// Clean up timer when the user promise resolves/rejects
|
|
289
|
-
userPromise.finally(() => {
|
|
290
|
-
clearInterval(timer);
|
|
291
|
-
popupRef.close();
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
// Race between user authentication and popup being closed
|
|
296
|
-
return Promise.race([userPromise, popupClosedPromise]);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return userPromise;
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
// This promise attempts to open the signin popup, and displays the blocked popup overlay if necessary.
|
|
303
|
-
return new Promise((resolve, reject) => {
|
|
304
|
-
signinPopup()
|
|
305
|
-
.then((oidcUser) => {
|
|
306
|
-
resolve(AuthManager.mapOidcUserToDomainModel(oidcUser));
|
|
307
|
-
})
|
|
308
|
-
.catch((error: unknown) => {
|
|
309
|
-
// Reject with the error if it is not caused by a blocked popup
|
|
310
|
-
if (!(error instanceof Error) || error.message !== 'Attempted to navigate on a disposed window') {
|
|
311
|
-
reject(error);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Popup was blocked; append the blocked popup overlay to allow the user to try again.
|
|
316
|
-
let popupHasBeenOpened: boolean = false;
|
|
317
|
-
const overlay = new LoginPopupOverlay(this.config.popupOverlayOptions || {}, true);
|
|
318
|
-
overlay.append(
|
|
319
|
-
async () => {
|
|
320
|
-
try {
|
|
321
|
-
if (!popupHasBeenOpened) {
|
|
322
|
-
// The user is attempting to open the popup again. It's safe to assume that this will not fail,
|
|
323
|
-
// as there are no async operations between the button interaction & the popup being opened.
|
|
324
|
-
popupHasBeenOpened = true;
|
|
325
|
-
const oidcUser = await signinPopup();
|
|
326
|
-
overlay.remove();
|
|
327
|
-
resolve(AuthManager.mapOidcUserToDomainModel(oidcUser));
|
|
328
|
-
} else {
|
|
329
|
-
// The popup has already been opened. By calling `window.open` with the same target as the
|
|
330
|
-
// previously opened popup, no new window will be opened. Instead, the existing popup
|
|
331
|
-
// will be focused. This works as expected in most browsers at the time of implementation, but
|
|
332
|
-
// the following exceptions do exist:
|
|
333
|
-
// - Safari: Only the initial call will focus the window, subsequent calls will do nothing.
|
|
334
|
-
// - Firefox: The window will not be focussed, nothing will happen.
|
|
335
|
-
window.open('', popupWindowTarget);
|
|
336
|
-
}
|
|
337
|
-
} catch (retryError: unknown) {
|
|
338
|
-
overlay.remove();
|
|
339
|
-
reject(retryError);
|
|
340
|
-
}
|
|
341
|
-
},
|
|
342
|
-
() => {
|
|
343
|
-
overlay.remove();
|
|
344
|
-
reject(new Error('Popup closed by user'));
|
|
345
|
-
},
|
|
346
|
-
);
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
}, PassportErrorType.AUTHENTICATION_ERROR);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
public async getUserOrLogin(): Promise<User> {
|
|
353
|
-
let user: User | null = null;
|
|
354
|
-
try {
|
|
355
|
-
user = await this.getUser();
|
|
356
|
-
} catch (err) {
|
|
357
|
-
logger.warn('Failed to retrieve a cached user session', err);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return user || this.login();
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
private static shouldUseSigninPopupCallback(): boolean {
|
|
364
|
-
// ID-3950: https://github.com/authts/oidc-client-ts/issues/2043
|
|
365
|
-
// Detect when the login was initiated via a popup
|
|
366
|
-
try {
|
|
367
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
368
|
-
const stateParam = urlParams.get('state');
|
|
369
|
-
const localStorageKey = `oidc.${stateParam}`;
|
|
370
|
-
|
|
371
|
-
const localStorageValue = localStorage.getItem(localStorageKey);
|
|
372
|
-
const loginState = JSON.parse(localStorageValue || '{}');
|
|
373
|
-
|
|
374
|
-
return loginState?.request_type === 'si:p';
|
|
375
|
-
} catch (err) {
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
public async loginCallback(): Promise<undefined | User> {
|
|
381
|
-
return withPassportError<undefined | User>(async () => {
|
|
382
|
-
// ID-3950: https://github.com/authts/oidc-client-ts/issues/2043
|
|
383
|
-
// When using `signinPopup` to initiate a login, call the `signinPopupCallback` method and
|
|
384
|
-
// set the `keepOpen` flag to `true`, as the `login` method is now responsible for closing the popup.
|
|
385
|
-
// See the comment in the `login` method for more details.
|
|
386
|
-
if (AuthManager.shouldUseSigninPopupCallback()) {
|
|
387
|
-
await this.userManager.signinPopupCallback(undefined, true);
|
|
388
|
-
return undefined;
|
|
389
|
-
}
|
|
390
|
-
const oidcUser = await this.userManager.signinCallback();
|
|
391
|
-
if (!oidcUser) {
|
|
392
|
-
return undefined;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return AuthManager.mapOidcUserToDomainModel(oidcUser);
|
|
396
|
-
}, PassportErrorType.AUTHENTICATION_ERROR);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
public async getPKCEAuthorizationUrl(
|
|
400
|
-
directLoginOptions?: DirectLoginOptions,
|
|
401
|
-
imPassportTraceId?: string,
|
|
402
|
-
): Promise<string> {
|
|
403
|
-
const verifier = base64URLEncode(window.crypto.getRandomValues(new Uint8Array(32)));
|
|
404
|
-
const challenge = base64URLEncode(await sha256(verifier));
|
|
405
|
-
|
|
406
|
-
// https://auth0.com/docs/secure/attack-protection/state-parameters
|
|
407
|
-
const state = base64URLEncode(window.crypto.getRandomValues(new Uint8Array(32)));
|
|
408
|
-
|
|
409
|
-
const {
|
|
410
|
-
redirectUri, scope, audience, clientId,
|
|
411
|
-
} = this.config.oidcConfiguration;
|
|
412
|
-
|
|
413
|
-
this.deviceCredentialsManager.savePKCEData({ state, verifier });
|
|
414
|
-
|
|
415
|
-
const pKCEAuthorizationUrl = new URL(authorizeEndpoint, this.config.authenticationDomain);
|
|
416
|
-
pKCEAuthorizationUrl.searchParams.set('response_type', 'code');
|
|
417
|
-
pKCEAuthorizationUrl.searchParams.set('code_challenge', challenge);
|
|
418
|
-
pKCEAuthorizationUrl.searchParams.set('code_challenge_method', 'S256');
|
|
419
|
-
pKCEAuthorizationUrl.searchParams.set('client_id', clientId);
|
|
420
|
-
pKCEAuthorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
|
421
|
-
pKCEAuthorizationUrl.searchParams.set('state', state);
|
|
422
|
-
|
|
423
|
-
if (scope) pKCEAuthorizationUrl.searchParams.set('scope', scope);
|
|
424
|
-
if (audience) pKCEAuthorizationUrl.searchParams.set('audience', audience);
|
|
425
|
-
|
|
426
|
-
if (directLoginOptions) {
|
|
427
|
-
// If method is email, only include direct login params if email is valid
|
|
428
|
-
if (directLoginOptions.directLoginMethod === 'email') {
|
|
429
|
-
const emailValue = directLoginOptions.email;
|
|
430
|
-
if (emailValue) {
|
|
431
|
-
pKCEAuthorizationUrl.searchParams.set('direct', directLoginOptions.directLoginMethod);
|
|
432
|
-
pKCEAuthorizationUrl.searchParams.set('email', emailValue);
|
|
433
|
-
}
|
|
434
|
-
} else {
|
|
435
|
-
// For non-email methods (social login), always include direct param
|
|
436
|
-
pKCEAuthorizationUrl.searchParams.set('direct', directLoginOptions.directLoginMethod);
|
|
437
|
-
}
|
|
438
|
-
if (directLoginOptions.marketingConsentStatus) {
|
|
439
|
-
pKCEAuthorizationUrl.searchParams.set('marketingConsent', directLoginOptions.marketingConsentStatus);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (imPassportTraceId) {
|
|
444
|
-
pKCEAuthorizationUrl.searchParams.set('im_passport_trace_id', imPassportTraceId);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return pKCEAuthorizationUrl.toString();
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
public async loginWithPKCEFlowCallback(authorizationCode: string, state: string): Promise<User> {
|
|
451
|
-
return withPassportError<User>(async () => {
|
|
452
|
-
const pkceData = this.deviceCredentialsManager.getPKCEData();
|
|
453
|
-
if (!pkceData) {
|
|
454
|
-
throw new Error('No code verifier or state for PKCE');
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (state !== pkceData.state) {
|
|
458
|
-
throw new Error('Provided state does not match stored state');
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const tokenResponse = await this.getPKCEToken(authorizationCode, pkceData.verifier);
|
|
462
|
-
const oidcUser = AuthManager.mapDeviceTokenResponseToOidcUser(tokenResponse);
|
|
463
|
-
const user = AuthManager.mapOidcUserToDomainModel(oidcUser);
|
|
464
|
-
await this.userManager.storeUser(oidcUser);
|
|
465
|
-
|
|
466
|
-
return user;
|
|
467
|
-
}, PassportErrorType.AUTHENTICATION_ERROR);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
private async getPKCEToken(authorizationCode: string, codeVerifier: string): Promise<DeviceTokenResponse> {
|
|
471
|
-
const response = await axios.post<DeviceTokenResponse>(
|
|
472
|
-
`${this.config.authenticationDomain}/oauth/token`,
|
|
473
|
-
{
|
|
474
|
-
client_id: this.config.oidcConfiguration.clientId,
|
|
475
|
-
grant_type: 'authorization_code',
|
|
476
|
-
code_verifier: codeVerifier,
|
|
477
|
-
code: authorizationCode,
|
|
478
|
-
redirect_uri: this.config.oidcConfiguration.redirectUri,
|
|
479
|
-
},
|
|
480
|
-
formUrlEncodedHeader,
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
return response.data;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
public async storeTokens(tokenResponse: DeviceTokenResponse): Promise<User> {
|
|
487
|
-
return withPassportError<User>(async () => {
|
|
488
|
-
const oidcUser = AuthManager.mapDeviceTokenResponseToOidcUser(tokenResponse);
|
|
489
|
-
const user = AuthManager.mapOidcUserToDomainModel(oidcUser);
|
|
490
|
-
await this.userManager.storeUser(oidcUser);
|
|
491
|
-
|
|
492
|
-
return user;
|
|
493
|
-
}, PassportErrorType.AUTHENTICATION_ERROR);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
public async logout(): Promise<void> {
|
|
497
|
-
return withPassportError<void>(async () => {
|
|
498
|
-
await this.userManager.revokeTokens(['refresh_token']);
|
|
499
|
-
|
|
500
|
-
if (this.logoutMode === 'silent') {
|
|
501
|
-
await this.userManager.signoutSilent();
|
|
502
|
-
} else {
|
|
503
|
-
await this.userManager.signoutRedirect();
|
|
504
|
-
}
|
|
505
|
-
}, PassportErrorType.LOGOUT_ERROR);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
public async logoutSilentCallback(url: string): Promise<void> {
|
|
509
|
-
return this.userManager.signoutSilentCallback(url);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
public async removeUser(): Promise<void> {
|
|
513
|
-
return this.userManager.removeUser();
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
public async getLogoutUrl(): Promise<string | null> {
|
|
517
|
-
const endSessionEndpoint = this.userManager.settings?.metadata?.end_session_endpoint;
|
|
518
|
-
|
|
519
|
-
if (!endSessionEndpoint) {
|
|
520
|
-
logger.warn('Failed to get logout URL');
|
|
521
|
-
return null;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
return endSessionEndpoint;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
public forceUserRefreshInBackground() {
|
|
528
|
-
this.refreshTokenAndUpdatePromise().catch((error) => {
|
|
529
|
-
logger.warn('Failed to refresh user token', error);
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
public async forceUserRefresh(): Promise<User | null> {
|
|
534
|
-
return this.refreshTokenAndUpdatePromise().catch((error) => {
|
|
535
|
-
logger.warn('Failed to refresh user token', error);
|
|
536
|
-
return null;
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Refreshes the token and returns the user.
|
|
542
|
-
* If the token is already being refreshed, returns the existing promise.
|
|
543
|
-
*/
|
|
544
|
-
private async refreshTokenAndUpdatePromise(): Promise<User | null> {
|
|
545
|
-
if (this.refreshingPromise) return this.refreshingPromise;
|
|
546
|
-
|
|
547
|
-
// eslint-disable-next-line no-async-promise-executor
|
|
548
|
-
this.refreshingPromise = new Promise(async (resolve, reject) => {
|
|
549
|
-
try {
|
|
550
|
-
const newOidcUser = await this.userManager.signinSilent();
|
|
551
|
-
if (newOidcUser) {
|
|
552
|
-
resolve(AuthManager.mapOidcUserToDomainModel(newOidcUser));
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
resolve(null);
|
|
556
|
-
} catch (err) {
|
|
557
|
-
let passportErrorType = PassportErrorType.AUTHENTICATION_ERROR;
|
|
558
|
-
let errorMessage = 'Failed to refresh token';
|
|
559
|
-
let removeUser = true;
|
|
560
|
-
|
|
561
|
-
if (err instanceof ErrorTimeout) {
|
|
562
|
-
passportErrorType = PassportErrorType.SILENT_LOGIN_ERROR;
|
|
563
|
-
errorMessage = `${errorMessage}: ${err.message}`;
|
|
564
|
-
removeUser = false;
|
|
565
|
-
} else if (err instanceof ErrorResponse) {
|
|
566
|
-
passportErrorType = PassportErrorType.NOT_LOGGED_IN_ERROR;
|
|
567
|
-
errorMessage = `${errorMessage}: ${err.message || err.error_description}`;
|
|
568
|
-
} else if (err instanceof Error) {
|
|
569
|
-
errorMessage = `${errorMessage}: ${err.message}`;
|
|
570
|
-
} else if (typeof err === 'string') {
|
|
571
|
-
errorMessage = `${errorMessage}: ${err}`;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
if (removeUser) {
|
|
575
|
-
try {
|
|
576
|
-
await this.userManager.removeUser();
|
|
577
|
-
} catch (removeUserError) {
|
|
578
|
-
if (removeUserError instanceof Error) {
|
|
579
|
-
errorMessage = `${errorMessage}: Failed to remove user: ${removeUserError.message}`;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
reject(new PassportError(errorMessage, passportErrorType));
|
|
585
|
-
} finally {
|
|
586
|
-
this.refreshingPromise = null; // Reset the promise after completion
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
return this.refreshingPromise;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
*
|
|
595
|
-
* @param typeAssertion {(user: User) => boolean} - Optional. If provided, then the User will be checked against
|
|
596
|
-
* the typeAssertion. If the user meets the requirements, then it will be typed as T and returned. If the User
|
|
597
|
-
* does NOT meet the type assertion, then execution will continue, and we will attempt to obtain a User that does
|
|
598
|
-
* meet the type assertion.
|
|
599
|
-
*
|
|
600
|
-
* This function will attempt to obtain a User in the following order:
|
|
601
|
-
* 1. If the User is currently refreshing, wait for the refresh to complete.
|
|
602
|
-
* 2. Attempt to obtain a User from storage that has not expired.
|
|
603
|
-
* 3. Attempt to refresh the User if a refresh token is present.
|
|
604
|
-
* 4. Return null if no valid User can be obtained.
|
|
605
|
-
*/
|
|
606
|
-
public async getUser<T extends User>(
|
|
607
|
-
typeAssertion: (user: User) => user is T = (user: User): user is T => true,
|
|
608
|
-
): Promise<T | null> {
|
|
609
|
-
if (this.refreshingPromise) {
|
|
610
|
-
const user = await this.refreshingPromise;
|
|
611
|
-
if (user && typeAssertion(user)) {
|
|
612
|
-
return user;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const oidcUser = await this.userManager.getUser();
|
|
619
|
-
if (!oidcUser) return null;
|
|
620
|
-
|
|
621
|
-
// if the token is not expired or expiring in 30 seconds or less, return the user
|
|
622
|
-
if (!isAccessTokenExpiredOrExpiring(oidcUser)) {
|
|
623
|
-
const user = AuthManager.mapOidcUserToDomainModel(oidcUser);
|
|
624
|
-
if (user && typeAssertion(user)) {
|
|
625
|
-
return user;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// if the token is expired or expiring in 30 seconds or less, refresh the token
|
|
630
|
-
if (oidcUser.refresh_token) {
|
|
631
|
-
const user = await this.refreshTokenAndUpdatePromise();
|
|
632
|
-
if (user && typeAssertion(user)) {
|
|
633
|
-
return user;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return null;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
public async getUserZkEvm(): Promise<UserZkEvm> {
|
|
641
|
-
const user = await this.getUser(isUserZkEvm);
|
|
642
|
-
if (!user) {
|
|
643
|
-
throw new Error('Failed to obtain a User with the required ZkEvm attributes');
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
return user;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
public async getUserImx(): Promise<UserImx> {
|
|
650
|
-
const user = await this.getUser(isUserImx);
|
|
651
|
-
if (!user) {
|
|
652
|
-
throw new Error('Failed to obtain a User with the required IMX attributes');
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return user;
|
|
656
|
-
}
|
|
657
|
-
}
|