/
msalImplicit.ts
232 lines (211 loc) · 7.48 KB
/
msalImplicit.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as msal from "msal";
import { AuthenticationRecord } from "../../client/msalClient";
import { credentialLogger } from "../../util/logging";
import {
BrowserLoginStyle,
InteractiveBrowserAuthenticateOptions
} from "../interactiveBrowserCredentialOptions";
import { IMSALBrowserFlow, IMSALToken, MSALOptions } from "./msalCommon";
const logger = credentialLogger("MSAL Browser v1 - Implicit Grant Flow");
/**
* Uses MSAL directly for browser authentication,
* which uses the [Implicit Grant Flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow)
*/
export class MSALImplicit implements IMSALBrowserFlow {
private config: msal.Configuration;
private app: msal.UserAgentApplication;
private loginStyle: BrowserLoginStyle;
private correlationId?: string;
private tenantId: string;
private account: AuthenticationRecord | undefined;
/**
* Sets up an MSAL object based on the given parameters.
* MSAL with Implicit Grant is not compatible with the account workflow we use from the MSAL Auth Code flow.
* In this case, any `authenticationRecord` received will be ignored.
* @param options - Parameters necessary and otherwise used to create the MSAL object.
*/
constructor(options: MSALOptions) {
this.loginStyle = options.loginStyle;
this.correlationId = options.correlationId;
this.tenantId = options.tenantId!;
this.config = {
auth: {
clientId: options.clientId!, // we just initialized it above
authority: `${options.authorityHost}/${options.tenantId}`,
knownAuthorities: options.knownAuthorities,
// If the users picked redirect as their login style,
// but they didn't provide a redirectUri,
// we can try to use the current page we're in as a default value.
redirectUri: options.redirectUri || window.location.origin,
postLogoutRedirectUri: options.postLogoutRedirectUri
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true // Set to true to improve the experience on IE11 and Edge.
}
};
this.app = new msal.UserAgentApplication(this.config);
}
/**
* Formats an MSAL 1 account into an `AuthenticationRecord`.
* @param account - The account in the shape defined by MSAL.
*/
private handleAccount(account: msal.Account): AuthenticationRecord {
return {
homeAccountId: account.homeAccountIdentifier,
environment: account.environment,
tenantId: this.tenantId,
localAccountId: account.accountIdentifier,
username: account.environment
};
}
/**
* Loads the account based on the result of the authentication.
* If no result was received, tries to load the account from the cache.
* @param result - Result object received from MSAL.
*/
private handleResult(result?: msal.AuthResponse): AuthenticationRecord | undefined {
if (result?.account) {
logger.info(`Authentication successful.`);
this.account = this.handleAccount(result?.account);
return this.account;
}
this.getActiveAccount();
if (this.account) {
return this.account;
}
return;
}
private redirectPromise: Promise<AuthenticationRecord | undefined> | undefined;
/**
* Attempts to handle a redirection request the least amount of times possible.
*/
public async handleRedirect(): Promise<AuthenticationRecord | undefined> {
if (this.account) {
return this.account;
}
if (this.redirectPromise) {
return this.redirectPromise;
}
if (!window.location.hash) {
return;
}
this.redirectPromise = new Promise((resolve, reject) => {
this.app.handleRedirectCallback((result) => {
if (!result?.account) {
const errorMessage = `Authentication failed. No redirect result.`;
logger.info(errorMessage);
reject(new Error(errorMessage));
}
this.handleResult(result!);
resolve(this.account);
}, reject);
});
return this.redirectPromise;
}
/**
* Uses MSAL to trigger a redirect or a popup login.
*/
public async login(): Promise<AuthenticationRecord | undefined> {
if (this.getActiveAccount()) {
return this.account;
}
switch (this.loginStyle) {
case "redirect": {
this.handleRedirect();
this.app.loginRedirect();
return this.redirectPromise;
}
case "popup":
return this.handleResult(await this.app.loginPopup());
}
}
/**
* Returns the existing account, attempts to load the account from MSAL.
*/
public getActiveAccount(): AuthenticationRecord | undefined {
if (this.account) {
return this.account;
}
const account = this.app.getAccount();
if (!account) {
return;
}
this.account = this.handleAccount(account);
return this.account;
}
/**
* Allows users to manually authenticate and retrieve the AuthenticationRecord.
* @param options - Optional parameters to authenticate with, like the scope.
*/
public async authenticate(): Promise<AuthenticationRecord | undefined> {
// We ensure that redirection is handled at this point.
await this.handleRedirect();
// If we've been able to retrieve the account, we return it.
if (this.account) {
return this.account;
}
// Otherwise we try to login.
return this.login();
}
/**
* Attempts to retrieve an authenticated token from MSAL.
* @param options - Properties useful to retrieve the token, like the scopes and the abortSignal.
*/
public async acquireToken(
options: InteractiveBrowserAuthenticateOptions
): Promise<IMSALToken | undefined> {
await this.authenticate();
const scopes = options.scopes;
if (!scopes) {
throw new Error(
`Invalid scopes in the acquireToken function of the MSAL Auth Code flow. Received: ${scopes}`
);
}
const silentRequest: msal.AuthenticationParameters = {
authority: this.config.auth.authority!,
correlationId: this.correlationId, // If undefined, MSAL will automatically generate one.
scopes: Array.isArray(scopes) ? scopes : scopes.split(",")
};
let authResponse: msal.AuthResponse | undefined;
try {
logger.info("Attempting to acquire token silently");
authResponse = await this.app.acquireTokenSilent(silentRequest);
} catch (err) {
if (err instanceof msal.AuthError) {
switch (err.errorCode) {
case "consent_required":
case "interaction_required":
case "login_required":
logger.info(`Authentication returned errorCode ${err.errorCode}`);
break;
default:
logger.info(`Failed to acquire token: ${err.message}`);
throw err;
}
}
}
if (authResponse === undefined) {
logger.info(
`Silent authentication failed, falling back to interactive method ${this.loginStyle}`
);
switch (this.loginStyle) {
case "redirect":
// This will go out of the page.
// Once the InteractiveBrowserCredential is initialized again,
// we'll load the MSAL account in the constructor.
this.app.acquireTokenRedirect(silentRequest);
return undefined;
case "popup":
authResponse = await this.app.acquireTokenPopup(silentRequest);
break;
}
}
return {
accessToken: authResponse.accessToken,
expiresOn: authResponse.expiresOn
};
}
}