@xh/hoist 64.0.1 → 64.0.4

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/appcontainer/AppContainerModel.ts +2 -2
  3. package/build/types/core/HoistComponent.d.ts +4 -8
  4. package/build/types/core/HoistProps.d.ts +3 -1
  5. package/build/types/core/elem.d.ts +1 -3
  6. package/build/types/core/types/Types.d.ts +1 -0
  7. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +1 -0
  8. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +1 -0
  9. package/build/types/icon/Icon.d.ts +7 -4
  10. package/build/types/security/BaseOAuthClient.d.ts +63 -46
  11. package/build/types/security/{TokenInfo.d.ts → Token.d.ts} +5 -4
  12. package/build/types/security/authzero/AuthZeroClient.d.ts +12 -7
  13. package/build/types/security/authzero/index.d.ts +1 -0
  14. package/build/types/security/msal/MsalClient.d.ts +63 -10
  15. package/build/types/security/msal/index.d.ts +1 -0
  16. package/build/types/svc/FetchService.d.ts +5 -5
  17. package/build/types/utils/react/LayoutPropUtils.d.ts +4 -4
  18. package/core/HoistComponent.ts +4 -8
  19. package/core/HoistProps.ts +4 -1
  20. package/core/elem.ts +0 -4
  21. package/core/types/Types.ts +1 -0
  22. package/desktop/cmp/input/ButtonGroupInput.ts +4 -3
  23. package/icon/Icon.ts +7 -4
  24. package/kit/blueprint/styles.scss +31 -18
  25. package/mobile/appcontainer/ToastSource.ts +2 -1
  26. package/package.json +1 -1
  27. package/security/BaseOAuthClient.ts +148 -165
  28. package/security/{TokenInfo.ts → Token.ts} +12 -9
  29. package/security/authzero/AuthZeroClient.ts +92 -80
  30. package/security/authzero/index.ts +7 -0
  31. package/security/msal/MsalClient.ts +204 -94
  32. package/security/msal/index.ts +7 -0
  33. package/svc/FetchService.ts +14 -11
  34. package/svc/IdentityService.ts +1 -1
  35. package/svc/LocalStorageService.ts +1 -1
  36. package/tsconfig.tsbuildinfo +1 -1
  37. package/utils/react/LayoutPropUtils.ts +4 -4
@@ -7,7 +7,7 @@
7
7
  import {Auth0Client} from '@auth0/auth0-spa-js';
8
8
  import {XH} from '@xh/hoist/core';
9
9
  import {never, wait} from '@xh/hoist/promise';
10
- import {TokenInfo} from '@xh/hoist/security/TokenInfo';
10
+ import {Token, TokenMap} from '@xh/hoist/security/Token';
11
11
  import {SECONDS} from '@xh/hoist/utils/datetime';
12
12
  import {throwIf} from '@xh/hoist/utils/js';
13
13
  import {flatMap, union} from 'lodash';
@@ -36,53 +36,114 @@ export interface AuthZeroTokenSpec {
36
36
  * via Google, GitHub, Microsoft, and various other OAuth providers *or* via a username/password
37
37
  * combo stored and managed within Auth0's own database. Supported options will depend on the
38
38
  * configuration of your Auth0 app.
39
+ *
40
+ * Note: If developing on localhost and using Access Tokens will need to configure your browser to
41
+ * allow third-party cookies.
39
42
  */
40
43
  export class AuthZeroClient extends BaseOAuthClient<AuthZeroClientConfig, AuthZeroTokenSpec> {
41
44
  private client: Auth0Client;
42
45
 
43
- override async doInitAsync(): Promise<void> {
46
+ //-------------------------------------------
47
+ // Implementations of core lifecycle methods
48
+ //-------------------------------------------
49
+ protected override async doInitAsync(): Promise<TokenMap> {
44
50
  const client = (this.client = this.createClient());
45
51
 
52
+ // 0) Returning - call client to complete redirect, and restore state
53
+ if (this.returningFromRedirect()) {
54
+ this.logDebug('Completing Redirect login');
55
+ const {appState} = await client.handleRedirectCallback();
56
+ this.restoreRedirectState(appState);
57
+ await this.noteUserAuthenticatedAsync();
58
+ return this.fetchAllTokensAsync();
59
+ }
60
+
61
+ // 1) If we are logged in, try to just reload tokens silently. This is the happy path on
62
+ // recent refresh.
46
63
  if (await client.isAuthenticated()) {
47
64
  try {
48
- return await this.loadTokensAsync();
65
+ this.logDebug('Attempting silent token load.');
66
+ return await this.fetchAllTokensAsync();
49
67
  } catch (e) {
50
- this.logDebug('Failed to load tokens on init, falling back on login', e);
68
+ this.logDebug('Failed to load tokens on init, fall back to login', e.message ?? e);
51
69
  }
52
70
  }
53
71
 
54
- this.usesRedirect
55
- ? await this.completeViaRedirectAsync()
56
- : await this.completeViaPopupAsync();
72
+ // 2) otherwise full-login
73
+ this.logDebug('Logging in');
74
+ await this.loginAsync();
57
75
 
58
- const user = await client.getUser();
59
- this.logDebug(`(Re)authenticated OK via Auth0`, user.email, user);
76
+ // 3) return tokens
77
+ return this.fetchAllTokensAsync();
78
+ }
60
79
 
61
- // Second-time (after login) the charm!
62
- await this.loadTokensAsync();
80
+ protected override async doLoginRedirectAsync(): Promise<void> {
81
+ const appState = this.captureRedirectState();
82
+ await this.client.loginWithRedirect({
83
+ appState,
84
+ authorizationParams: {scope: this.loginScope}
85
+ });
86
+ await never();
63
87
  }
64
88
 
65
- override async getIdTokenAsync(useCache: boolean = true): Promise<TokenInfo> {
89
+ protected override async doLoginPopupAsync(): Promise<void> {
90
+ try {
91
+ await this.client.loginWithPopup({authorizationParams: {scope: this.loginScope}});
92
+ await this.noteUserAuthenticatedAsync();
93
+ } catch (e) {
94
+ const msg = e.message?.toLowerCase();
95
+ e.popup?.close();
96
+ if (msg === 'timeout') {
97
+ throw XH.exception({
98
+ name: 'Auth0 Login Error',
99
+ message:
100
+ 'Login popup window timed out. Please reload this tab in your browser to try again.',
101
+ cause: e
102
+ });
103
+ }
104
+
105
+ if (msg === 'popup closed') {
106
+ throw XH.exception({
107
+ name: 'Auth0 Login Error',
108
+ message:
109
+ 'Login popup window closed. Please reload this tab in your browser to try again.',
110
+ cause: e
111
+ });
112
+ }
113
+
114
+ if (msg.includes('unable to open a popup')) {
115
+ throw XH.exception({
116
+ name: 'Auth0 Login Error',
117
+ message: this.popupBlockerErrorMessage,
118
+ cause: e
119
+ });
120
+ }
121
+
122
+ throw e;
123
+ }
124
+ }
125
+
126
+ protected override async fetchIdTokenAsync(useCache: boolean = true): Promise<Token> {
66
127
  const response = await this.client.getTokenSilently({
67
128
  authorizationParams: {scope: this.idScopes.join(' ')},
68
129
  cacheMode: useCache ? 'on' : 'off',
69
130
  detailedResponse: true
70
131
  });
71
- return new TokenInfo(response.id_token);
132
+ return new Token(response.id_token);
72
133
  }
73
134
 
74
- override async getAccessTokenAsync(
135
+ protected override async fetchAccessTokenAsync(
75
136
  spec: AuthZeroTokenSpec,
76
137
  useCache: boolean = true
77
- ): Promise<TokenInfo> {
78
- const token = await this.client.getTokenSilently({
138
+ ): Promise<Token> {
139
+ const value = await this.client.getTokenSilently({
79
140
  authorizationParams: {scope: spec.scopes.join(' '), audience: spec.audience},
80
141
  cacheMode: useCache ? 'on' : 'off'
81
142
  });
82
- return new TokenInfo(token);
143
+ return new Token(value);
83
144
  }
84
145
 
85
- override async doLogoutAsync(): Promise<void> {
146
+ protected override async doLogoutAsync(): Promise<void> {
86
147
  const {client} = this;
87
148
  if (!(await client.isAuthenticated())) return;
88
149
  await client.logout({
@@ -95,9 +156,9 @@ export class AuthZeroClient extends BaseOAuthClient<AuthZeroClientConfig, AuthZe
95
156
  await wait(10 * SECONDS);
96
157
  }
97
158
 
98
- //------------------
99
- // Implementation
100
- //-----------------
159
+ //------------------------
160
+ // Private implementation
161
+ //------------------------
101
162
  private createClient(): Auth0Client {
102
163
  const config = this.config,
103
164
  {clientId, domain} = config;
@@ -118,68 +179,19 @@ export class AuthZeroClient extends BaseOAuthClient<AuthZeroClientConfig, AuthZe
118
179
  return ret;
119
180
  }
120
181
 
121
- private async completeViaRedirectAsync(): Promise<void> {
122
- const {client} = this;
123
-
124
- // Determine if we are on back end of redirect (recipe from Auth0 docs)
125
- const {search} = window.location,
126
- isReturning =
127
- (search.includes('state=') && search.includes('code=')) ||
128
- search.includes('error=');
129
-
130
- if (!isReturning) {
131
- // 1) Initiating - grab state and initiate redirect
132
- const appState = this.captureRedirectState();
133
- await client.loginWithRedirect({
134
- appState,
135
- authorizationParams: {scope: this.loginScope}
136
- });
137
- await never();
138
- } else {
139
- // 2) Returning - call client to complete redirect, and restore state
140
- const {appState} = await client.handleRedirectCallback();
141
- this.restoreRedirectState(appState);
142
- }
182
+ private get loginScope(): string {
183
+ return union(this.idScopes, flatMap(this.config.accessTokens, 'scopes')).join(' ');
143
184
  }
144
185
 
145
- private async completeViaPopupAsync(): Promise<void> {
146
- const {client} = this;
147
- try {
148
- await client.loginWithPopup({authorizationParams: {scope: this.loginScope}});
149
- } catch (e) {
150
- const msg = e.message?.toLowerCase();
151
- e.popup?.close();
152
- if (msg === 'timeout') {
153
- throw XH.exception({
154
- name: 'Auth0 Login Error',
155
- message:
156
- 'Login popup window timed out. Please reload this tab in your browser to try again.',
157
- cause: e
158
- });
159
- }
160
-
161
- if (msg === 'popup closed') {
162
- throw XH.exception({
163
- name: 'Auth0 Login Error',
164
- message:
165
- 'Login popup window closed. Please reload this tab in your browser to try again.',
166
- cause: e
167
- });
168
- }
169
-
170
- if (msg.includes('unable to open a popup')) {
171
- throw XH.exception({
172
- name: 'Auth0 Login Error',
173
- message: this.popupBlockerErrorMessage,
174
- cause: e
175
- });
176
- }
177
-
178
- throw e;
179
- }
186
+ private returningFromRedirect(): boolean {
187
+ // Determine if we are on back end of redirect (recipe from Auth0 docs)
188
+ const {search} = window.location;
189
+ return (search.includes('state=') && search.includes('code=')) || search.includes('error=');
180
190
  }
181
191
 
182
- private get loginScope(): string {
183
- return union(this.idScopes, flatMap(this.config.accessTokens, 'scopes')).join(' ');
192
+ private async noteUserAuthenticatedAsync() {
193
+ const user = await this.client.getUser();
194
+ this.setSelectedUsername(user.email);
195
+ this.logDebug('User Authenticated', user.email);
184
196
  }
185
197
  }
@@ -0,0 +1,7 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2024 Extremely Heavy Industries Inc.
6
+ */
7
+ export * from './AuthZeroClient';
@@ -7,15 +7,15 @@
7
7
  import * as msal from '@azure/msal-browser';
8
8
  import {
9
9
  AccountInfo,
10
- InteractionRequiredAuthError,
11
10
  IPublicClientApplication,
11
+ LogLevel,
12
12
  PopupRequest,
13
- RedirectRequest
13
+ RedirectRequest,
14
+ SilentRequest
14
15
  } from '@azure/msal-browser';
15
- import {LogLevel} from '@azure/msal-common';
16
16
  import {XH} from '@xh/hoist/core';
17
17
  import {never} from '@xh/hoist/promise';
18
- import {TokenInfo} from '@xh/hoist/security/TokenInfo';
18
+ import {Token, TokenMap} from '@xh/hoist/security/Token';
19
19
  import {logDebug, logError, logInfo, logWarn, throwIf} from '@xh/hoist/utils/js';
20
20
  import {flatMap, union, uniq} from 'lodash';
21
21
  import {BaseOAuthClient, BaseOAuthClientConfig} from '../BaseOAuthClient';
@@ -31,88 +31,234 @@ export interface MsalClientConfig extends BaseOAuthClientConfig<MsalTokenSpec> {
31
31
  */
32
32
  authority?: string;
33
33
 
34
- /** The log level of MSAL. Default is LogLevel.Info (2). */
34
+ /**
35
+ * A hint about the tenant or domain that the user should use to sign in.
36
+ * The value of the domain hint is a registered domain for the tenant.
37
+ */
38
+ domainHint?: string;
39
+
40
+ /**
41
+ * If specified, the client will use this value when initializing the app to enforce a minimum
42
+ * amount of time during which no further auth flow with the provider should be necessary.
43
+ *
44
+ * Use this argument to front-load any necessary auth flow to the apps initialization stage
45
+ * thereby minimizing disruption to user activity during application use.
46
+ *
47
+ * This value may be set to anything up to 86400 (24 hours), the maximum lifetime
48
+ * of an Azure refresh token. Set to -1 to disable (default).
49
+ *
50
+ * Note that setting to *any* non-disabled amount will require the app to do *some* communication
51
+ * with the login provider at *every* app load. This may just involve loading new tokens via
52
+ * fetch, however, setting to higher values will increase the frequency with which
53
+ * a new refresh token will also need to be requested via a hidden iframe/redirect/popup. This
54
+ * can be time-consuming and potentially disruptive and applications should therefore use with
55
+ * care and typically set to some value significantly less than the max.
56
+ *
57
+ * See https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/token-lifetimes.md
58
+ */
59
+ initRefreshTokenExpirationOffsetSecs?: number;
60
+
61
+ /** The log level of MSAL. Default is LogLevel.Warning. */
35
62
  msalLogLevel?: LogLevel;
36
63
  }
37
64
 
38
65
  export interface MsalTokenSpec {
39
66
  /** Scopes for the desired access token. */
40
67
  scopes: string[];
68
+
69
+ /**
70
+ * Scopes to be added to the scopes requested during interactive and SSO logins.
71
+ * See the `scopes` property on `PopupRequest`, `RedirectRequest`, and `SSORequest`
72
+ * for more info.
73
+ */
74
+ loginScopes?: string[];
75
+
76
+ /**
77
+ * Scopes to be added to the scopes requested during interactive and SSO login.
78
+ *
79
+ * See the `extraScopesToConsent` property on `PopupRequest`, `RedirectRequest`, and
80
+ * `SSORequest` for more info.
81
+ */
82
+ extraScopesToConsent?: string[];
41
83
  }
42
84
 
43
85
  /**
44
86
  * Service to implement OAuth authentication via MSAL.
87
+ *
88
+ * See the following helpful information relevant to our use of this tricky API --
89
+ * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/
90
+ * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/token-lifetimes.md
91
+ * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/login-user.md
92
+ *
93
+ * TODO: The handling of `ssoSilent` and `initRefreshTokenExpirationOffsetSecs` in this library
94
+ * require 3rd party cookies to be enabled in the browser so that MSAL can load contact in a
95
+ * hidden iFrame If its *not* enabled, we may be doing extra work. Consider checking 3rd party
96
+ * cookie support and adding conditional behavior?
45
97
  */
46
98
  export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec> {
47
99
  private client: IPublicClientApplication;
48
- private account: AccountInfo; // Authenticated account, as most recent auth call with Azure.
100
+ private account: AccountInfo; // Authenticated account
101
+ private initialTokenLoad: boolean;
102
+
103
+ constructor(config: MsalClientConfig) {
104
+ super({
105
+ initRefreshTokenExpirationOffsetSecs: -1,
106
+ msalLogLevel: LogLevel.Warning,
107
+ domainHint: null,
108
+ ...config
109
+ });
110
+ }
49
111
 
50
- override async doInitAsync(): Promise<void> {
112
+ //-------------------------------------------
113
+ // Implementations of core lifecycle methods
114
+ //-------------------------------------------
115
+ protected override async doInitAsync(): Promise<TokenMap> {
51
116
  const client = (this.client = await this.createClientAsync());
52
117
 
53
- this.account = client.getAllAccounts()[0];
118
+ // 0) Handle redirect return
119
+ const redirectResp = await client.handleRedirectPromise();
120
+ if (redirectResp) {
121
+ this.logDebug('Completing Redirect login');
122
+ this.noteUserAuthenticated(redirectResp.account);
123
+ this.restoreRedirectState(redirectResp.state);
124
+ return this.fetchAllTokensAsync();
125
+ }
54
126
 
127
+ // 1) If we are logged in, try to just reload tokens silently. This is the happy path on
128
+ // recent refresh. This should never trigger popup/redirect, but if
129
+ // 'initRefreshTokenExpirationOffsetSecs' is set, this may trigger a hidden iframe redirect
130
+ // to gain a new refreshToken (3rd party cookies required).
131
+ const accounts = client.getAllAccounts();
132
+ this.logDebug('Authenticated accounts available', accounts);
133
+ this.account = accounts.length == 1 ? accounts[0] : null;
55
134
  if (this.account) {
56
135
  try {
57
- return await this.loadTokensAsync();
136
+ this.initialTokenLoad = true;
137
+ this.logDebug('Attempting silent token load.');
138
+ return await this.fetchAllTokensAsync();
58
139
  } catch (e) {
59
- if (!(e instanceof InteractionRequiredAuthError)) {
60
- throw e;
61
- }
62
- this.logDebug('Failed to load tokens on init, falling back on login', e);
140
+ this.account = null;
141
+ this.logDebug('Failed to load tokens on init, fall back to login', e.message ?? e);
142
+ } finally {
143
+ this.initialTokenLoad = false;
63
144
  }
64
145
  }
65
146
 
66
- this.usesRedirect
67
- ? await this.completeViaRedirectAsync()
68
- : await this.completeViaPopupAsync();
69
- this.logDebug(`(Re)authenticated OK via Azure`, this.account.username, this.account);
147
+ // 2) Otherwise need to login.
148
+ // 2a) Try MSALs `ssoSilent` API, to potentially reuse logged-in user on other apps in same
149
+ // domain without interaction. This should never trigger popup/redirect, but will use an iFrame
150
+ // if available (3rd party cookies required). Will work if MSAL can resolve a single
151
+ // logged-in user with access to app and meeting all hint criteria.
152
+ try {
153
+ this.logDebug('Attempting SSO');
154
+ await this.loginSsoAsync();
155
+ } catch (e) {
156
+ this.logDebug('SSO failed', e.message ?? e);
157
+ }
158
+
159
+ // 2b) Otherwise do full interactive login. This may or may not require user involvement
160
+ // but will require at the very least a redirect or cursory auto-closing popup.
161
+ if (!this.account) {
162
+ this.logDebug('Logging in');
163
+ await this.loginAsync();
164
+ }
165
+
166
+ // 3) Return tokens
167
+ return this.fetchAllTokensAsync();
168
+ }
70
169
 
71
- // Second-time (after login) the charm!
72
- await this.loadTokensAsync();
170
+ protected override async doLoginPopupAsync(): Promise<void> {
171
+ const {client} = this,
172
+ opts: PopupRequest = {
173
+ loginHint: this.getSelectedUsername(),
174
+ domainHint: this.config.domainHint,
175
+ scopes: this.loginScopes,
176
+ extraScopesToConsent: this.loginExtraScopesToConsent
177
+ };
178
+ try {
179
+ const ret = await client.acquireTokenPopup(opts);
180
+ this.noteUserAuthenticated(ret.account);
181
+ } catch (e) {
182
+ if (e.message?.toLowerCase().includes('popup window')) {
183
+ throw XH.exception({
184
+ name: 'Azure Login Error',
185
+ message: this.popupBlockerErrorMessage,
186
+ cause: e
187
+ });
188
+ }
189
+ throw e;
190
+ }
73
191
  }
74
192
 
75
- override async getIdTokenAsync(useCache: boolean = true): Promise<TokenInfo> {
193
+ protected override async doLoginRedirectAsync(): Promise<void> {
194
+ const {client} = this,
195
+ state = this.captureRedirectState(),
196
+ opts: RedirectRequest = {
197
+ state,
198
+ loginHint: this.getSelectedUsername(),
199
+ domainHint: this.config.domainHint,
200
+ scopes: this.loginScopes,
201
+ extraScopesToConsent: this.loginExtraScopesToConsent
202
+ };
203
+ await client.acquireTokenRedirect(opts);
204
+ await never();
205
+ }
206
+
207
+ protected override async fetchIdTokenAsync(useCache: boolean = true): Promise<Token> {
76
208
  const ret = await this.client.acquireTokenSilent({
77
- scopes: this.idScopes,
78
209
  account: this.account,
79
- forceRefresh: !useCache
210
+ scopes: this.idScopes,
211
+ forceRefresh: !useCache,
212
+ prompt: 'none',
213
+ ...this.refreshOffsetArgs
80
214
  });
81
- this.account = ret.account;
82
- return new TokenInfo(ret.idToken);
215
+ return new Token(ret.idToken);
83
216
  }
84
217
 
85
- override async getAccessTokenAsync(
218
+ protected override async fetchAccessTokenAsync(
86
219
  spec: MsalTokenSpec,
87
220
  useCache: boolean = true
88
- ): Promise<TokenInfo> {
221
+ ): Promise<Token> {
89
222
  const ret = await this.client.acquireTokenSilent({
90
- scopes: spec.scopes,
91
223
  account: this.account,
92
- forceRefresh: !useCache
224
+ scopes: spec.scopes,
225
+ forceRefresh: !useCache,
226
+ prompt: 'none',
227
+ ...this.refreshOffsetArgs
93
228
  });
94
- this.account = ret.account;
95
- return new TokenInfo(ret.accessToken);
229
+ return new Token(ret.accessToken);
96
230
  }
97
231
 
98
- override async doLogoutAsync(): Promise<void> {
99
- const {postLogoutRedirectUrl, client, account, usesRedirect} = this;
232
+ protected override async doLogoutAsync(): Promise<void> {
233
+ const {postLogoutRedirectUrl, client, account, loginMethod} = this;
100
234
  await client.clearCache({account});
101
- usesRedirect
235
+ loginMethod == 'REDIRECT'
102
236
  ? await client.logoutRedirect({account, postLogoutRedirectUri: postLogoutRedirectUrl})
103
237
  : await client.logoutPopup({account});
104
238
  }
105
239
 
106
240
  //------------------------
107
- // Implementation
241
+ // Private implementation
108
242
  //------------------------
243
+ private async loginSsoAsync(): Promise<void> {
244
+ const result = await this.client.ssoSilent({
245
+ loginHint: this.getSelectedUsername(),
246
+ domainHint: this.config.domainHint,
247
+ redirectUri: this.redirectUrl, // Recommended by MS, not used?
248
+ scopes: this.loginScopes,
249
+ extraScopesToConsent: this.loginExtraScopesToConsent,
250
+ prompt: 'none'
251
+ });
252
+ this.noteUserAuthenticated(result.account);
253
+ }
254
+
109
255
  private async createClientAsync(): Promise<IPublicClientApplication> {
110
256
  const config = this.config,
111
257
  {clientId, authority, msalLogLevel} = config;
112
258
 
113
259
  throwIf(!authority, 'Missing MSAL authority. Please review your configuration.');
114
260
 
115
- const ret = await msal.PublicClientApplication.createPublicClientApplication({
261
+ return msal.PublicClientApplication.createPublicClientApplication({
116
262
  auth: {
117
263
  clientId,
118
264
  authority,
@@ -122,60 +268,13 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
122
268
  system: {
123
269
  loggerOptions: {
124
270
  loggerCallback: this.logFromMsal,
125
- logLevel: msalLogLevel ?? 1
271
+ logLevel: msalLogLevel
126
272
  }
273
+ },
274
+ cache: {
275
+ cacheLocation: 'localStorage' // allows sharing auth info across tabs.
127
276
  }
128
277
  });
129
- return ret;
130
- }
131
-
132
- private async completeViaRedirectAsync(): Promise<void> {
133
- const {client, account} = this,
134
- redirectResp = await client.handleRedirectPromise();
135
-
136
- if (!redirectResp) {
137
- // 1) Initiating - grab state and initiate redirect
138
- const state = this.captureRedirectState(),
139
- opts: RedirectRequest = {
140
- state,
141
- scopes: this.loginScopes,
142
- extraScopesToConsent: this.loginExtraScopes
143
- };
144
- account
145
- ? await client.acquireTokenRedirect({...opts, account})
146
- : await client.loginRedirect(opts);
147
-
148
- await never();
149
- } else {
150
- // 2) Returning - just restore state
151
- this.account = redirectResp.account;
152
- const redirectState = redirectResp.state;
153
- this.restoreRedirectState(redirectState);
154
- }
155
- }
156
-
157
- private async completeViaPopupAsync(): Promise<void> {
158
- const {client, account} = this,
159
- opts: PopupRequest = {
160
- scopes: this.loginScopes,
161
- extraScopesToConsent: this.loginExtraScopes
162
- };
163
- try {
164
- const ret = account
165
- ? await client.acquireTokenPopup({...opts, account})
166
- : await client.loginPopup(opts);
167
-
168
- this.account = ret.account;
169
- } catch (e) {
170
- if (e.message?.toLowerCase().includes('popup window')) {
171
- throw XH.exception({
172
- name: 'Azure Login Error',
173
- message: this.popupBlockerErrorMessage,
174
- cause: e
175
- });
176
- }
177
- throw e;
178
- }
179
278
  }
180
279
 
181
280
  private logFromMsal(level: LogLevel, message: string) {
@@ -193,17 +292,28 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
193
292
  }
194
293
 
195
294
  private get loginScopes(): string[] {
196
- return union(
197
- this.idScopes,
198
- flatMap(this.config.accessTokens, spec =>
199
- spec.scopes.filter(s => !s.startsWith('api:'))
295
+ return uniq(
296
+ union(
297
+ this.idScopes,
298
+ flatMap(this.config.accessTokens, spec => spec.loginScopes ?? [])
200
299
  )
201
300
  );
202
301
  }
203
302
 
204
- private get loginExtraScopes(): string[] {
205
- return uniq(
206
- flatMap(this.config.accessTokens, spec => spec.scopes.filter(s => s.startsWith('api:')))
207
- );
303
+ private get loginExtraScopesToConsent(): string[] {
304
+ return uniq(flatMap(this.config.accessTokens, spec => spec.extraScopesToConsent ?? []));
305
+ }
306
+
307
+ private get refreshOffsetArgs(): Partial<SilentRequest> {
308
+ const offset = this.config.initRefreshTokenExpirationOffsetSecs;
309
+ return offset > 0 && this.initialTokenLoad
310
+ ? {forceRefresh: true, refreshTokenExpirationOffsetSeconds: offset}
311
+ : {};
312
+ }
313
+
314
+ private noteUserAuthenticated(account: AccountInfo) {
315
+ this.account = account;
316
+ this.setSelectedUsername(account.username);
317
+ this.logDebug('User Authenticated', account.username);
208
318
  }
209
319
  }
@@ -0,0 +1,7 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2024 Extremely Heavy Industries Inc.
6
+ */
7
+ export * from './MsalClient';