@authress/login 2.2.196

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/index.js ADDED
@@ -0,0 +1,518 @@
1
+ const cookieManager = require('cookie');
2
+ const take = require('lodash.take');
3
+
4
+ const HttpClient = require('./httpClient');
5
+ const jwtManager = require('./jwtManager');
6
+ const userIdentityTokenStorageManager = require('./userIdentityTokenStorageManager');
7
+
8
+ let userSessionResolver;
9
+ let userSessionPromise = new Promise(resolve => userSessionResolver = resolve);
10
+
11
+ let userSessionSequencePromise = null;
12
+
13
+ const AuthenticationRequestNonceKey = 'AuthenticationRequestNonce';
14
+
15
+ class LoginClient {
16
+ /**
17
+ * @constructor constructs the LoginClient with a given configuration
18
+ * @param {Object} settings
19
+ * @param {String} settings.authressLoginHostUrl Your Authress custom domain - see https://authress.io/app/#/manage?focus=applications
20
+ * @param {String} settings.applicationId the Authress applicationId for this app - see https://authress.io/app/#/manage?focus=applications
21
+ * @param {Object} [logger] a configured logger object, optionally `console`, which can used to display debug and warning messages.
22
+ */
23
+ constructor(settings, logger) {
24
+ this.settings = Object.assign({}, settings);
25
+ this.logger = logger || console;
26
+ const hostUrl = this.settings.authressLoginHostUrl || this.settings.authenticationServiceUrl || '';
27
+
28
+ if (!hostUrl) {
29
+ throw Error('Missing required property "authressLoginHostUrl" in LoginClient constructor. Custom Authress Domain Host is required.');
30
+ }
31
+
32
+ this.hostUrl = `https://${hostUrl.replace(/^(https?:\/+)/, '')}`;
33
+ this.httpClient = new HttpClient(this.hostUrl);
34
+ this.lastSessionCheck = 0;
35
+
36
+ this.enableCredentials = this.getMatchingDomainInfo(this.hostUrl, typeof window !== 'undefined' ? window : undefined);
37
+
38
+ if (!settings.skipBackgroundCredentialsCheck) {
39
+ window.onload = async () => {
40
+ await this.userSessionExists(true);
41
+ };
42
+ }
43
+ }
44
+
45
+ isLocalHost() {
46
+ const isLocalHost = typeof window !== 'undefined' && window.location && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
47
+ return isLocalHost;
48
+ }
49
+
50
+ getMatchingDomainInfo(hostUrl, webWindow) {
51
+ const host = new URL(hostUrl);
52
+
53
+ if (this.isLocalHost()) {
54
+ return false;
55
+ }
56
+
57
+ if (typeof webWindow === 'undefined') {
58
+ return false;
59
+ }
60
+
61
+ if (webWindow.location.protocol !== 'https:') {
62
+ return false;
63
+ }
64
+
65
+ const tokenUrlList = host.host.toLowerCase().split('.').reverse();
66
+ // Login url may not be known all the time, in which case we will compare the token url to the appUrl
67
+ const appUrlList = webWindow.location.host.toLowerCase().split('.').reverse();
68
+
69
+ let reversedMatchSegments = [];
70
+ for (let segment of tokenUrlList) {
71
+ const urlToTest = take(appUrlList, reversedMatchSegments.length + 1).join('.');
72
+ const urlToMatch = reversedMatchSegments.concat(segment).join('.');
73
+ if (urlToMatch !== urlToTest) {
74
+ break;
75
+ }
76
+
77
+ reversedMatchSegments.push(segment);
78
+ }
79
+
80
+ if (reversedMatchSegments.length === tokenUrlList.length && reversedMatchSegments.length === appUrlList.length) {
81
+ return true;
82
+ }
83
+
84
+ // Quick match TLD assuming TLD is only one path part
85
+ if (reversedMatchSegments.length > 1) {
86
+ return true;
87
+ }
88
+
89
+ return false;
90
+ }
91
+
92
+ /**
93
+ * @description Gets the user's profile data and returns it if it exists. Should be called after {@link userSessionExists} or it will be empty.
94
+ * @return {Object} The user data object.
95
+ */
96
+ getUserIdentity() {
97
+ const cookies = cookieManager.parse(document.cookie);
98
+ const idToken = cookies && cookies.user;
99
+ // Cache the ID Token in the local storage as soon as we attempt to check for it.
100
+ // * We need this in the cache, and the best way to do this is right here, so it's in one place
101
+ // * While this isn't the optimal location, this will ensure that every fetch to the user identity correctly is cached and is returned to the caller.
102
+ if (idToken && jwtManager.decode(idToken)) {
103
+ const expiry = new Date(jwtManager.decode(idToken).exp * 1000) || new Date(Date.now() + 86400000);
104
+ userIdentityTokenStorageManager.set(idToken, expiry);
105
+ }
106
+
107
+ const userIdToken = userIdentityTokenStorageManager.get();
108
+ const userData = userIdToken && jwtManager.decode(userIdToken);
109
+ if (!userData) {
110
+ return null;
111
+ }
112
+ userData.userId = userData.sub;
113
+ return userData;
114
+ }
115
+
116
+ /**
117
+ * @description Gets the user's credentials that were generated as part of the connection provider. These credentials work directly with that provider.
118
+ * @return {Promise<UserCredentials?>} The user's connection credentials.
119
+ */
120
+ async getConnectionCredentials() {
121
+ await this.waitForUserSession();
122
+
123
+ try {
124
+ const token = await this.ensureToken();
125
+ const credentialsResult = await this.httpClient.get('/session/credentials', this.enableCredentials, { Authorization: token && `Bearer ${token}` });
126
+ return credentialsResult.data;
127
+ } catch (error) {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * @description Async wait for a user session to exist. Will block until {@link userSessionExists} or {@link authenticate} is called.
134
+ * @return {Promise<void>}
135
+ */
136
+ async waitForUserSession() {
137
+ try {
138
+ await userSessionPromise;
139
+ return true;
140
+ } catch (error) {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * @description Call this function on every route change. It will check if the user just logged in or is still logged in.
147
+ * @return {Promise<Boolean>} Returns truthy if there a valid existing session, falsy otherwise.
148
+ */
149
+ userSessionExists(backgroundTrigger) {
150
+ if (userSessionSequencePromise) {
151
+ // Prevent duplicate calls to checking the user session when they happen within the same 50ms time span
152
+ if (Date.now() - this.lastSessionCheck < 50) {
153
+ return userSessionSequencePromise;
154
+ }
155
+
156
+ this.lastSessionCheck = Date.now();
157
+ return userSessionSequencePromise = userSessionSequencePromise
158
+ .catch(() => { /* ignore since we always want to continue even after a failure */ })
159
+ .then(() => this.userSessionContinuation(backgroundTrigger));
160
+ }
161
+ this.lastSessionCheck = Date.now();
162
+ return userSessionSequencePromise = this.userSessionContinuation(backgroundTrigger);
163
+ }
164
+
165
+ async userSessionContinuation(backgroundTrigger) {
166
+ const urlSearchParams = new URLSearchParams(window.location.search);
167
+ const newUrl = new URL(window.location);
168
+
169
+ let authRequest = {};
170
+ try {
171
+ authRequest = JSON.parse(localStorage.getItem(AuthenticationRequestNonceKey) || '{}');
172
+ localStorage.removeItem(AuthenticationRequestNonceKey);
173
+ if (Object.hasOwnProperty.call(authRequest, 'enableCredentials')) {
174
+ this.enableCredentials = authRequest.enableCredentials;
175
+ }
176
+ } catch (error) {
177
+ this.logger && this.logger.debug('LocalStorage failed in Browser', error);
178
+ }
179
+
180
+ // Your app was redirected to from the Authress Hosted Login page. The next step is to show the user the login widget and enable them to login.
181
+ if (urlSearchParams.get('state') && urlSearchParams.get('flow') === 'oauthLogin') {
182
+ return false;
183
+ }
184
+
185
+ if (authRequest.nonce && urlSearchParams.get('code')) {
186
+ newUrl.searchParams.delete('nonce');
187
+ newUrl.searchParams.delete('iss');
188
+ newUrl.searchParams.delete('code');
189
+ history.replaceState({}, undefined, newUrl.toString());
190
+
191
+ // Compare the initial authentication requestId to the returned one. If they don't match either the nonce has been tampered with or this isn't the latest authentication request
192
+ // * This prevents canonical replay attacks, and fall through. If the user is already logged in, then the new log in attempt is ignored.
193
+ if (authRequest.nonce === urlSearchParams.get('nonce')) {
194
+ const code = urlSearchParams.get('code') === 'cookie' ? cookieManager.parse(document.cookie)['auth-code'] : urlSearchParams.get('code');
195
+ const request = { grant_type: 'authorization_code', redirect_uri: authRequest.redirectUrl, client_id: this.settings.applicationId, code, code_verifier: authRequest.codeVerifier };
196
+ const tokenResult = await this.httpClient.post(`/authentication/${authRequest.nonce}/tokens`, this.enableCredentials, request);
197
+ const idToken = jwtManager.decode(tokenResult.data.id_token);
198
+ const expiry = tokenResult.data.expires_in && new Date(Date.now() + tokenResult.data.expires_in * 1000) || new Date(idToken.exp * 1000);
199
+ document.cookie = cookieManager.serialize('authorization', tokenResult.data.access_token || '', { expires: expiry, path: '/' });
200
+ userIdentityTokenStorageManager.set(tokenResult.data.id_token, expiry);
201
+ userSessionResolver();
202
+ return true;
203
+ }
204
+ }
205
+
206
+ if (this.isLocalHost()) {
207
+ if (urlSearchParams.get('nonce') && urlSearchParams.get('access_token')) {
208
+ newUrl.searchParams.delete('iss');
209
+ newUrl.searchParams.delete('nonce');
210
+ newUrl.searchParams.delete('expires_in');
211
+ newUrl.searchParams.delete('access_token');
212
+ newUrl.searchParams.delete('id_token');
213
+ history.replaceState({}, undefined, newUrl.toString());
214
+
215
+ // Compare the initial authentication requestId to the returned one. If they don't match either the nonce has been tampered with or this isn't the latest authentication request
216
+ // * This prevents canonical replay attacks, and fall through. If the user is already logged in, then the new log in attempt is ignored.
217
+ if (!authRequest.nonce || authRequest.nonce === urlSearchParams.get('nonce')) {
218
+ const idToken = jwtManager.decode(urlSearchParams.get('id_token'));
219
+ const expiry = Number(urlSearchParams.get('expires_in')) && new Date(Date.now() + Number(urlSearchParams.get('expires_in')) * 1000) || new Date(idToken.exp * 1000);
220
+ document.cookie = cookieManager.serialize('authorization', urlSearchParams.get('access_token') || '', { expires: expiry, path: '/' });
221
+ userIdentityTokenStorageManager.set(urlSearchParams.get('id_token'), expiry);
222
+ userSessionResolver();
223
+ return true;
224
+ }
225
+ }
226
+ // Otherwise check cookies and then force the user to log in
227
+ }
228
+
229
+ // At this point the user identity should have been loaded through cookies (if the cookie mechanism was selected and it isn't local host.) So we'll first check if a login session just completed.
230
+ const userData = this.getUserIdentity();
231
+ // User is already logged in
232
+ if (userData) {
233
+ userSessionResolver();
234
+ return true;
235
+ }
236
+
237
+ if (!this.isLocalHost() && !backgroundTrigger) {
238
+ try {
239
+ const sessionResult = await this.httpClient.patch('/session', this.enableCredentials, {});
240
+ // In the case that the session contains non cookie based data, store it back to the cookie for this domain
241
+ if (sessionResult.data.access_token) {
242
+ const idToken = jwtManager.decode(sessionResult.data.id_token);
243
+ const expiry = sessionResult.data.expires_in && new Date(Date.now() + sessionResult.data.expires_in * 1000) || new Date(idToken.exp * 1000);
244
+ document.cookie = cookieManager.serialize('authorization', sessionResult.data.access_token || '', { expires: expiry, path: '/' });
245
+ userIdentityTokenStorageManager.set(sessionResult.data.id_token, expiry);
246
+ }
247
+ } catch (error) {
248
+ this.logger && this.logger.log({ title: 'Failed attempting to check if the user has an existing authentication session', error });
249
+ }
250
+ const newUserData = this.getUserIdentity();
251
+ // User session exists and now is logged in
252
+ if (newUserData) {
253
+ userSessionResolver();
254
+ return true;
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+
260
+ /**
261
+ * @description When a platform extension attempts to log a user in, the Authress Login page will redirect to your Platform defaultAuthenticationUrl. At this point, show the user the login screen, and then pass the results of the login to this method.
262
+ * @param {String} [state] The redirect to your login screen will contain two query parameters `state` and `flow`. Pass the state into this method.
263
+ * @param {String} [connectionId] Specify which provider connection that user would like to use to log in - see https://authress.io/app/#/manage?focus=connections
264
+ * @param {String} [tenantLookupIdentifier] Instead of connectionId, specify the tenant lookup identifier to log the user with the mapped tenant - see https://authress.io/app/#/manage?focus=tenants
265
+ * @param {Object} [connectionProperties] Connection specific properties to pass to the identity provider. Can be used to override default scopes for example.
266
+ */
267
+ async updateExtensionAuthenticationRequest({ state, connectionId, tenantLookupIdentifier, connectionProperties }) {
268
+ if (!connectionId && !tenantLookupIdentifier) {
269
+ const e = Error('connectionId or tenantLookupIdentifier must be specified');
270
+ e.code = 'InvalidConnection';
271
+ throw e;
272
+ }
273
+
274
+ const urlSearchParams = new URLSearchParams(window.location.search);
275
+ const authenticationRequestId = state || urlSearchParams.get('state');
276
+ if (!authenticationRequestId) {
277
+ const e = Error('The `state` parameters must be specified to update this authentication request');
278
+ e.code = 'InvalidAuthenticationRequest';
279
+ throw e;
280
+ }
281
+
282
+ try {
283
+ const requestOptions = await this.httpClient.patch(`/authentication/${authenticationRequestId}`, true, {
284
+ connectionId, tenantLookupIdentifier, connectionProperties
285
+ });
286
+
287
+ window.location.assign(requestOptions.data.authenticationUrl);
288
+ } catch (error) {
289
+ if (error.status >= 400 && error.status < 500) {
290
+ const e = Error(error.data.title || error.data.errorCode);
291
+ e.code = error.data.errorCode;
292
+ throw e;
293
+ }
294
+ throw error;
295
+ }
296
+
297
+ // Prevent the current UI from taking any action once we decided we need to log in.
298
+ await new Promise(resolve => setTimeout(resolve, 5000));
299
+ }
300
+
301
+ /**
302
+ * @description Unlink an identity from the user's account.
303
+ * @param {String} identityId Specify the provider connection id or the user id of that connection that user would like to unlink - see https://authress.io/app/#/manage?focus=connections
304
+ * @return {Promise<void>} Throws an error if identity cannot be unlinked.
305
+ */
306
+ async unlinkIdentity(identityId) {
307
+ if (!identityId) {
308
+ const e = Error('connectionId must be specified');
309
+ e.code = 'InvalidConnection';
310
+ throw e;
311
+ }
312
+
313
+ if (!this.getUserIdentity()) {
314
+ const e = Error('User must be logged into to unlink an account.');
315
+ e.code = 'NotLoggedIn';
316
+ throw e;
317
+ }
318
+
319
+ let accessToken;
320
+ try {
321
+ accessToken = await this.ensureToken({ timeoutInMillis: 100 });
322
+ } catch (error) {
323
+ if (error.code === 'TokenTimeout') {
324
+ const e = Error('User must be logged into an existing account before linking a second account.');
325
+ e.code = 'NotLoggedIn';
326
+ throw e;
327
+ }
328
+ }
329
+
330
+ const headers = this.enableCredentials && !this.isLocalHost() ? {} : {
331
+ Authorization: `Bearer ${accessToken}`
332
+ };
333
+
334
+ try {
335
+ await this.httpClient.delete(`/identities/${encodeURIComponent(identityId)}`, this.enableCredentials, headers);
336
+ } catch (error) {
337
+ if (error.status >= 400 && error.status < 500) {
338
+ const e = Error(error.data.title || error.data.errorCode);
339
+ e.code = error.data.errorCode;
340
+ throw e;
341
+ }
342
+ throw error;
343
+ }
344
+ }
345
+
346
+ /**
347
+ * @description Link a new identity to the currently logged in user. The user will be asked to authenticate to a new connection.
348
+ * @param {String} [connectionId] Specify which provider connection that user would like to use to log in - see https://authress.io/app/#/manage?focus=connections
349
+ * @param {String} [tenantLookupIdentifier] Instead of connectionId, specify the tenant lookup identifier to log the user with the mapped tenant - see https://authress.io/app/#/manage?focus=tenants
350
+ * @param {String} [redirectUrl=${window.location.href}] Specify where the provider should redirect to the user to in your application. If not specified, the default is the current location href. Must be a valid redirect url matching what is defined in the application in the Authress Management portal.
351
+ * @param {Object} [connectionProperties] Connection specific properties to pass to the identity provider. Can be used to override default scopes for example.
352
+ * @return {Promise<void>} Is there a valid existing session.
353
+ */
354
+ async linkIdentity({ connectionId, tenantLookupIdentifier, redirectUrl, connectionProperties }) {
355
+ if (!connectionId && !tenantLookupIdentifier) {
356
+ const e = Error('connectionId or tenantLookupIdentifier must be specified');
357
+ e.code = 'InvalidConnection';
358
+ throw e;
359
+ }
360
+
361
+ if (!this.getUserIdentity()) {
362
+ const e = Error('User must be logged into an existing account before linking a second account.');
363
+ e.code = 'NotLoggedIn';
364
+ throw e;
365
+ }
366
+
367
+ let accessToken;
368
+ try {
369
+ accessToken = await this.ensureToken({ timeoutInMillis: 100 });
370
+ } catch (error) {
371
+ if (error.code === 'TokenTimeout') {
372
+ const e = Error('User must be logged into an existing account before linking a second account.');
373
+ e.code = 'NotLoggedIn';
374
+ throw e;
375
+ }
376
+ }
377
+
378
+ const { codeChallenge } = await jwtManager.getAuthCodes();
379
+
380
+ try {
381
+ const normalizedRedirectUrl = redirectUrl && new URL(redirectUrl).toString();
382
+ const selectedRedirectUrl = normalizedRedirectUrl || window.location.href;
383
+ const headers = this.enableCredentials && !this.isLocalHost() ? {} : {
384
+ Authorization: `Bearer ${accessToken}`
385
+ };
386
+ const requestOptions = await this.httpClient.post('/authentication', this.enableCredentials, {
387
+ linkIdentity: true,
388
+ redirectUrl: selectedRedirectUrl, codeChallengeMethod: 'S256', codeChallenge,
389
+ connectionId, tenantLookupIdentifier,
390
+ connectionProperties,
391
+ applicationId: this.settings.applicationId
392
+ }, headers);
393
+ window.location.assign(requestOptions.data.authenticationUrl);
394
+ } catch (error) {
395
+ if (error.status >= 400 && error.status < 500) {
396
+ const e = Error(error.data.title || error.data.errorCode);
397
+ e.code = error.data.errorCode;
398
+ throw e;
399
+ }
400
+ throw error;
401
+ }
402
+
403
+ // Prevent the current UI from taking any action once we decided we need to log in.
404
+ await new Promise(resolve => setTimeout(resolve, 5000));
405
+ }
406
+
407
+ /**
408
+ * @description Logs a user in, if the user is not logged in, will redirect the user to their selected connection/provider and then redirect back to the {@link redirectUrl}.
409
+ * @param {String} [connectionId] Specify which provider connection that user would like to use to log in - see https://authress.io/app/#/manage?focus=connections
410
+ * @param {String} [tenantLookupIdentifier] Instead of connectionId, specify the tenant lookup identifier to log the user with the mapped tenant - see https://authress.io/app/#/manage?focus=tenants
411
+ * @param {String} [responseLocation=cookie] Store the credentials response in the specified location. Options are either 'cookie' or 'query'.
412
+ * @param {String} [flowType=token id_token] The type of credentials returned in the response. The list of options is any of 'code token id_token' separated by a space. Select token to receive an access_token, id_token to return the user identity in an JWT, and code for the authorization_code grant_type flow.
413
+ * @param {String} [redirectUrl=${window.location.href}] Specify where the provider should redirect to the user to in your application. If not specified, the default is the current location href. Must be a valid redirect url matching what is defined in the application in the Authress Management portal.
414
+ * @param {Object} [connectionProperties] Connection specific properties to pass to the identity provider. Can be used to override default scopes for example.
415
+ * @param {Boolean} [force=false] Force getting new credentials.
416
+ * @param {Boolean} [multiAccount=false] Enable multi-account login. The user will be prompted to login with their other account, if they are not logged in already.
417
+ * @param {Boolean} [clearUserDataBeforeLogin=true] Remove all cookies, LocalStorage, and SessionStorage related data before logging in. In most cases, this helps prevent corrupted browser state from affecting your user's experience.
418
+ * @return {Promise<Boolean>} Is there a valid existing session.
419
+ */
420
+ async authenticate(options = {}) {
421
+ const { connectionId, tenantLookupIdentifier, redirectUrl, force, responseLocation, flowType, connectionProperties, openType, multiAccount, clearUserDataBeforeLogin } = (options || {});
422
+ if (responseLocation && responseLocation !== 'cookie' && responseLocation !== 'query' && responseLocation !== 'none') {
423
+ const e = Error('Authentication response location is not valid');
424
+ e.code = 'InvalidResponseLocation';
425
+ throw e;
426
+ }
427
+
428
+ if (!force && !multiAccount && await this.userSessionExists()) {
429
+ return true;
430
+ }
431
+
432
+ const { codeVerifier, codeChallenge } = await jwtManager.getAuthCodes();
433
+
434
+ try {
435
+ const normalizedRedirectUrl = redirectUrl && new URL(redirectUrl).toString();
436
+ const selectedRedirectUrl = normalizedRedirectUrl || window.location.href;
437
+ if (clearUserDataBeforeLogin !== false) {
438
+ userIdentityTokenStorageManager.clear();
439
+ }
440
+
441
+ const authResponse = await this.httpClient.post('/authentication', false, {
442
+ redirectUrl: selectedRedirectUrl, codeChallengeMethod: 'S256', codeChallenge,
443
+ connectionId, tenantLookupIdentifier,
444
+ connectionProperties,
445
+ applicationId: this.settings.applicationId,
446
+ responseLocation, flowType, multiAccount
447
+ });
448
+ localStorage.setItem(AuthenticationRequestNonceKey, JSON.stringify({
449
+ nonce: authResponse.data.authenticationRequestId, codeVerifier, lastConnectionId: connectionId, tenantLookupIdentifier, redirectUrl: selectedRedirectUrl,
450
+ enableCredentials: authResponse.data.enableCredentials, multiAccount
451
+ }));
452
+ if (openType === 'tab') {
453
+ window.open(authResponse.data.authenticationUrl, '_blank');
454
+ } else {
455
+ window.location.assign(authResponse.data.authenticationUrl);
456
+ }
457
+ } catch (error) {
458
+ if (error.status >= 400 && error.status < 500) {
459
+ const e = Error(error.data.title || error.data.errorCode);
460
+ e.code = error.data.errorCode;
461
+ throw e;
462
+ }
463
+ throw error;
464
+ }
465
+
466
+ // Prevent the current UI from taking any action once we decided we need to log in.
467
+ await new Promise(resolve => setTimeout(resolve, 5000));
468
+ return false;
469
+ }
470
+
471
+ /**
472
+ * @description Ensures the user's bearer token exists. To be used in the Authorization header as a Bearer token. This method blocks on a valid user session being created, and expects {@link authenticate} to have been called first. Additionally, if the application configuration specifies that tokens should be secured from javascript, the token will be a hidden cookie only visible to service APIs and will not be returned. If the token is expired and the session is still valid, then it will automatically generate a new token directly from Authress.
473
+ * @param {Object} [options] Options for getting a token including timeout configuration.
474
+ * @param {Number} [options.timeoutInMillis=5000] Timeout waiting for user token to populate. After this time an error will be thrown.
475
+ * @return {Promise<String>} The Authorization Bearer token if allowed otherwise null.
476
+ */
477
+ async ensureToken(options) {
478
+ await this.userSessionExists();
479
+ const inputOptions = Object.assign({ timeoutInMillis: 5000 }, options || {});
480
+ const sessionWaiterAsync = this.waitForUserSession();
481
+ const timeoutAsync = new Promise((resolve, reject) => setTimeout(reject, inputOptions.timeoutInMillis || 0));
482
+ try {
483
+ await Promise.race([sessionWaiterAsync, timeoutAsync]);
484
+ } catch (timeout) {
485
+ const error = Error('No token retrieved after timeout');
486
+ error.code = 'TokenTimeout';
487
+ throw error;
488
+ }
489
+ const cookies = cookieManager.parse(document.cookie);
490
+ return cookies.authorization !== 'undefined' && cookies.authorization;
491
+ }
492
+
493
+ /**
494
+ * @description Log the user out removing the current user's session. If the user is not logged in this has no effect. If the user is logged in via secure session, the the redirect url will be ignored. If the user is logged in without a secure session the user agent will be redirected to the hosted login and then redirected to the {@link redirectUrl}.
495
+ * @param {String} [redirectUrl='window.location.href'] Optional redirect location to return the user to after logout. Will only be used for cross domain sessions.
496
+ */
497
+ async logout(redirectUrl) {
498
+ userIdentityTokenStorageManager.clear();
499
+
500
+ // Localhost also has enableCredentials set, so this path is only for cross domain logins
501
+ if (!this.enableCredentials) {
502
+ const fullLogoutUrl = new URL('/logout', this.hostUrl);
503
+ fullLogoutUrl.searchParams.set('redirect_uri', redirectUrl || window.location.href);
504
+ fullLogoutUrl.searchParams.set('client_id', this.settings.applicationId);
505
+ window.location.assign(fullLogoutUrl.toString());
506
+ return;
507
+ }
508
+
509
+ // Reset user local session
510
+ userSessionPromise = new Promise(resolve => userSessionResolver = resolve);
511
+ try {
512
+ await this.httpClient.delete('/session', this.enableCredentials);
513
+ } catch (error) { /**/ }
514
+ }
515
+ }
516
+
517
+ const ExtensionClient = require('./extensionClient');
518
+ module.exports = { LoginClient, ExtensionClient };
@@ -0,0 +1,32 @@
1
+ const base64url = require('./base64url');
2
+
3
+ class JwtManager {
4
+ decode(token) {
5
+ try {
6
+ return token && JSON.parse(base64url.decode(token.split('.')[1]));
7
+ } catch (error) {
8
+ return null;
9
+ }
10
+ }
11
+
12
+ decodeFull(token) {
13
+ try {
14
+ return token && {
15
+ header: JSON.parse(base64url.decode(token.split('.')[0])),
16
+ payload: JSON.parse(base64url.decode(token.split('.')[1]))
17
+ };
18
+ } catch (error) {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ async getAuthCodes() {
24
+ const codeVerifier = base64url.encode((window.crypto || window.msCrypto).getRandomValues(new Uint32Array(16)).toString());
25
+ // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
26
+ const hashBuffer = await (window.crypto || window.msCrypto).subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier));
27
+ const codeChallenge = base64url.encode(hashBuffer);
28
+ return { codeVerifier, codeChallenge };
29
+ }
30
+ }
31
+
32
+ module.exports = new JwtManager();
@@ -0,0 +1,79 @@
1
+ const cookieManager = require('cookie');
2
+ const jwtManager = require('./jwtManager');
3
+
4
+ const AuthenticationCredentialsStorageKey = 'AuthenticationCredentialsStorage';
5
+
6
+ class UserIdentityTokenStorageManager {
7
+ set(value, expiry) {
8
+ try {
9
+ const cookies = cookieManager.parse(document.cookie);
10
+ localStorage.setItem(AuthenticationCredentialsStorageKey, JSON.stringify({ idToken: value, expiry: expiry && expiry.getTime(), jsCookies: !!cookies.authorization }));
11
+ this.clearCookies('user');
12
+
13
+ const cookieDomain = window.location.hostname.split('.').reverse().slice(0, 2).reverse().join('.');
14
+ const userData = jwtManager.decode(value);
15
+ const newCookie = `AuthUserId=${userData && userData.sub || ''}; expires=${expiry.toUTCString()}; path=/; domain=${cookieDomain}`;
16
+ document.cookie = newCookie;
17
+ } catch (error) {
18
+ console.debug('LocalStorage failed in Browser', error);
19
+ }
20
+ }
21
+
22
+ get() {
23
+ try {
24
+ const { idToken, expiry, jsCookies } = JSON.parse(localStorage.getItem(AuthenticationCredentialsStorageKey) || '{}');
25
+ if (!idToken || expiry < Date.now()) {
26
+ return null;
27
+ }
28
+
29
+ const cookies = cookieManager.parse(document.cookie);
30
+ // If the authorization cookie was present when the identity was stored, then it must still be present after, otherwise we know that the user data saved isn't valid anymore
31
+ // * If the authorization cookie wasn't present, then it is because the application configuration restricts access to javascript.
32
+ // * That means that the implementation can't use the presence of the ID token information to make a decision about if the user is logged in.
33
+ if (jsCookies && !cookies.authorization) {
34
+ return null;
35
+ }
36
+
37
+ return idToken;
38
+ } catch (error) {
39
+ console.debug('LocalStorage failed in Browser', error);
40
+ return null;
41
+ }
42
+ }
43
+
44
+ delete() {
45
+ try {
46
+ localStorage.removeItem(AuthenticationCredentialsStorageKey);
47
+ } catch (error) {
48
+ console.debug('LocalStorage failed in Browser', error);
49
+ }
50
+ }
51
+
52
+ clear() {
53
+ this.clearCookies();
54
+ this.delete();
55
+ }
56
+
57
+ clearCookies(cookieName) {
58
+ const cookies = document.cookie.split('; ');
59
+ for (const cookie of cookies) {
60
+ // Remove only the cookies that are relevant to the client
61
+ if (!['user', 'authorization', 'auth-code'].includes(cookie.split('=')[0]) || cookieName && cookie.split('=')[0] !== cookieName) {
62
+ continue;
63
+ }
64
+ const domain = window.location.hostname.split('.');
65
+ while (domain.length > 0) {
66
+ const cookieBase = `${encodeURIComponent(cookie.split(';')[0].split('=')[0])}=; expires=Thu, 01-Jan-1970 00:00:01 GMT; domain=${domain.join('.')} ;path=`;
67
+ const path = location.pathname.split('/');
68
+ document.cookie = `${cookieBase}/`;
69
+ while (path.length > 0) {
70
+ document.cookie = cookieBase + path.join('/');
71
+ path.pop();
72
+ }
73
+ domain.shift();
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ module.exports = new UserIdentityTokenStorageManager();