@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.
- package/CHANGELOG.md +23 -1
- package/appcontainer/AppContainerModel.ts +2 -2
- package/build/types/core/HoistComponent.d.ts +4 -8
- package/build/types/core/HoistProps.d.ts +3 -1
- package/build/types/core/elem.d.ts +1 -3
- package/build/types/core/types/Types.d.ts +1 -0
- package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +1 -0
- package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +1 -0
- package/build/types/icon/Icon.d.ts +7 -4
- package/build/types/security/BaseOAuthClient.d.ts +63 -46
- package/build/types/security/{TokenInfo.d.ts → Token.d.ts} +5 -4
- package/build/types/security/authzero/AuthZeroClient.d.ts +12 -7
- package/build/types/security/authzero/index.d.ts +1 -0
- package/build/types/security/msal/MsalClient.d.ts +63 -10
- package/build/types/security/msal/index.d.ts +1 -0
- package/build/types/svc/FetchService.d.ts +5 -5
- package/build/types/utils/react/LayoutPropUtils.d.ts +4 -4
- package/core/HoistComponent.ts +4 -8
- package/core/HoistProps.ts +4 -1
- package/core/elem.ts +0 -4
- package/core/types/Types.ts +1 -0
- package/desktop/cmp/input/ButtonGroupInput.ts +4 -3
- package/icon/Icon.ts +7 -4
- package/kit/blueprint/styles.scss +31 -18
- package/mobile/appcontainer/ToastSource.ts +2 -1
- package/package.json +1 -1
- package/security/BaseOAuthClient.ts +148 -165
- package/security/{TokenInfo.ts → Token.ts} +12 -9
- package/security/authzero/AuthZeroClient.ts +92 -80
- package/security/authzero/index.ts +7 -0
- package/security/msal/MsalClient.ts +204 -94
- package/security/msal/index.ts +7 -0
- package/svc/FetchService.ts +14 -11
- package/svc/IdentityService.ts +1 -1
- package/svc/LocalStorageService.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
68
|
+
this.logDebug('Failed to load tokens on init, fall back to login', e.message ?? e);
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
71
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
// 2) otherwise full-login
|
|
73
|
+
this.logDebug('Logging in');
|
|
74
|
+
await this.loginAsync();
|
|
57
75
|
|
|
58
|
-
|
|
59
|
-
this.
|
|
76
|
+
// 3) return tokens
|
|
77
|
+
return this.fetchAllTokensAsync();
|
|
78
|
+
}
|
|
60
79
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
|
132
|
+
return new Token(response.id_token);
|
|
72
133
|
}
|
|
73
134
|
|
|
74
|
-
override async
|
|
135
|
+
protected override async fetchAccessTokenAsync(
|
|
75
136
|
spec: AuthZeroTokenSpec,
|
|
76
137
|
useCache: boolean = true
|
|
77
|
-
): Promise<
|
|
78
|
-
const
|
|
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
|
|
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
|
-
//
|
|
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
|
|
122
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
183
|
-
|
|
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
|
}
|
|
@@ -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 {
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
this.initialTokenLoad = true;
|
|
137
|
+
this.logDebug('Attempting silent token load.');
|
|
138
|
+
return await this.fetchAllTokensAsync();
|
|
58
139
|
} catch (e) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
this.
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
210
|
+
scopes: this.idScopes,
|
|
211
|
+
forceRefresh: !useCache,
|
|
212
|
+
prompt: 'none',
|
|
213
|
+
...this.refreshOffsetArgs
|
|
80
214
|
});
|
|
81
|
-
|
|
82
|
-
return new TokenInfo(ret.idToken);
|
|
215
|
+
return new Token(ret.idToken);
|
|
83
216
|
}
|
|
84
217
|
|
|
85
|
-
override async
|
|
218
|
+
protected override async fetchAccessTokenAsync(
|
|
86
219
|
spec: MsalTokenSpec,
|
|
87
220
|
useCache: boolean = true
|
|
88
|
-
): Promise<
|
|
221
|
+
): Promise<Token> {
|
|
89
222
|
const ret = await this.client.acquireTokenSilent({
|
|
90
|
-
scopes: spec.scopes,
|
|
91
223
|
account: this.account,
|
|
92
|
-
|
|
224
|
+
scopes: spec.scopes,
|
|
225
|
+
forceRefresh: !useCache,
|
|
226
|
+
prompt: 'none',
|
|
227
|
+
...this.refreshOffsetArgs
|
|
93
228
|
});
|
|
94
|
-
|
|
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,
|
|
232
|
+
protected override async doLogoutAsync(): Promise<void> {
|
|
233
|
+
const {postLogoutRedirectUrl, client, account, loginMethod} = this;
|
|
100
234
|
await client.clearCache({account});
|
|
101
|
-
|
|
235
|
+
loginMethod == 'REDIRECT'
|
|
102
236
|
? await client.logoutRedirect({account, postLogoutRedirectUri: postLogoutRedirectUrl})
|
|
103
237
|
: await client.logoutPopup({account});
|
|
104
238
|
}
|
|
105
239
|
|
|
106
240
|
//------------------------
|
|
107
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
205
|
-
return uniq(
|
|
206
|
-
|
|
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
|
}
|