@azure/msal-react 1.1.2 → 1.3.1

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.
@@ -1,5 +1,5 @@
1
- import React__default, { createContext, useEffect, useMemo, useState, useRef, useContext, useCallback } from 'react';
2
- import { stubbedPublicClientApplication, InteractionStatus, Logger, WrapperSKU, EventType, EventMessageUtils, AccountEntity, InteractionType } from '@azure/msal-browser';
1
+ import React__default, { createContext, useEffect, useMemo, useReducer, useContext, useState, useRef, useCallback } from 'react';
2
+ import { stubbedPublicClientApplication, InteractionStatus, Logger, WrapperSKU, EventMessageUtils, AccountEntity, AuthError, InteractionType, InteractionRequiredAuthError, EventType, OIDC_DEFAULT_SCOPES } from '@azure/msal-browser';
3
3
 
4
4
  /*
5
5
  * Copyright (c) Microsoft Corporation. All rights reserved.
@@ -53,15 +53,101 @@ function accountArraysAreEqual(arrayA, arrayB) {
53
53
  return elementA.homeAccountId === elementB.homeAccountId && elementA.localAccountId === elementB.localAccountId && elementA.username === elementB.username;
54
54
  });
55
55
  }
56
+ function getAccountByIdentifiers(allAccounts, accountIdentifiers) {
57
+ if (allAccounts.length > 0 && (accountIdentifiers.homeAccountId || accountIdentifiers.localAccountId || accountIdentifiers.username)) {
58
+ const matchedAccounts = allAccounts.filter(accountObj => {
59
+ if (accountIdentifiers.username && accountIdentifiers.username.toLowerCase() !== accountObj.username.toLowerCase()) {
60
+ return false;
61
+ }
62
+
63
+ if (accountIdentifiers.homeAccountId && accountIdentifiers.homeAccountId.toLowerCase() !== accountObj.homeAccountId.toLowerCase()) {
64
+ return false;
65
+ }
66
+
67
+ if (accountIdentifiers.localAccountId && accountIdentifiers.localAccountId.toLowerCase() !== accountObj.localAccountId.toLowerCase()) {
68
+ return false;
69
+ }
70
+
71
+ return true;
72
+ });
73
+ return matchedAccounts[0] || null;
74
+ } else {
75
+ return null;
76
+ }
77
+ }
56
78
 
57
79
  /* eslint-disable header/header */
58
80
  const name = "@azure/msal-react";
59
- const version = "1.1.2";
81
+ const version = "1.3.1";
60
82
 
61
83
  /*
62
84
  * Copyright (c) Microsoft Corporation. All rights reserved.
63
85
  * Licensed under the MIT License.
64
86
  */
87
+ var MsalProviderActionType;
88
+
89
+ (function (MsalProviderActionType) {
90
+ MsalProviderActionType["UNBLOCK_INPROGRESS"] = "UNBLOCK_INPROGRESS";
91
+ MsalProviderActionType["EVENT"] = "EVENT";
92
+ })(MsalProviderActionType || (MsalProviderActionType = {}));
93
+ /**
94
+ * Returns the next inProgress and accounts state based on event message
95
+ * @param previousState
96
+ * @param action
97
+ */
98
+
99
+
100
+ const reducer = (previousState, action) => {
101
+ const {
102
+ type,
103
+ payload
104
+ } = action;
105
+ let newAccounts = previousState.accounts;
106
+ let newInProgress = previousState.inProgress;
107
+
108
+ switch (type) {
109
+ case MsalProviderActionType.UNBLOCK_INPROGRESS:
110
+ if (previousState.inProgress === InteractionStatus.Startup) {
111
+ newInProgress = InteractionStatus.None;
112
+ payload.logger.info("MsalProvider - handleRedirectPromise resolved, setting inProgress to 'none'");
113
+ }
114
+
115
+ break;
116
+
117
+ case MsalProviderActionType.EVENT:
118
+ const message = payload.message;
119
+ const status = EventMessageUtils.getInteractionStatusFromEvent(message, previousState.inProgress);
120
+
121
+ if (status) {
122
+ payload.logger.info(`MsalProvider - ${message.eventType} results in setting inProgress from ${previousState.inProgress} to ${status}`);
123
+ newInProgress = status;
124
+ }
125
+
126
+ break;
127
+
128
+ default:
129
+ throw new Error(`Unknown action type: ${type}`);
130
+ }
131
+
132
+ const currentAccounts = payload.instance.getAllAccounts();
133
+
134
+ if (!accountArraysAreEqual(currentAccounts, previousState.accounts)) {
135
+ payload.logger.info("MsalProvider - updating account state");
136
+ newAccounts = currentAccounts;
137
+ } else {
138
+ payload.logger.verbose("MsalProvider - no account changes");
139
+ }
140
+
141
+ return { ...previousState,
142
+ inProgress: newInProgress,
143
+ accounts: newAccounts
144
+ };
145
+ };
146
+ /**
147
+ * MSAL context provider component. This must be rendered above any other components that use MSAL.
148
+ */
149
+
150
+
65
151
  function MsalProvider({
66
152
  instance,
67
153
  children
@@ -72,56 +158,24 @@ function MsalProvider({
72
158
 
73
159
  const logger = useMemo(() => {
74
160
  return instance.getLogger().clone(name, version);
75
- }, [instance]); // State hook to store accounts
76
-
77
- const [accounts, setAccounts] = useState([]); // State hook to store in progress value
78
-
79
- const [inProgress, setInProgress] = useState(InteractionStatus.Startup); // Mutable object used in the event callback
80
-
81
- const inProgressRef = useRef(inProgress);
82
- useEffect(() => {
83
- const callbackId = instance.addEventCallback(message => {
84
- switch (message.eventType) {
85
- case EventType.ACCOUNT_ADDED:
86
- case EventType.ACCOUNT_REMOVED:
87
- case EventType.LOGIN_SUCCESS:
88
- case EventType.SSO_SILENT_SUCCESS:
89
- case EventType.HANDLE_REDIRECT_END:
90
- case EventType.LOGIN_FAILURE:
91
- case EventType.SSO_SILENT_FAILURE:
92
- case EventType.LOGOUT_END:
93
- case EventType.ACQUIRE_TOKEN_SUCCESS:
94
- case EventType.ACQUIRE_TOKEN_FAILURE:
95
- const currentAccounts = instance.getAllAccounts();
96
-
97
- if (!accountArraysAreEqual(currentAccounts, accounts)) {
98
- logger.info("MsalProvider - updating account state");
99
- setAccounts(currentAccounts);
100
- } else {
101
- logger.info("MsalProvider - no account changes");
102
- }
103
-
104
- break;
105
- }
106
- });
107
- logger.verbose(`MsalProvider - Registered event callback with id: ${callbackId}`);
108
- return () => {
109
- // Remove callback when component unmounts or accounts change
110
- if (callbackId) {
111
- logger.verbose(`MsalProvider - Removing event callback ${callbackId}`);
112
- instance.removeEventCallback(callbackId);
113
- }
161
+ }, [instance]);
162
+ const [state, updateState] = useReducer(reducer, undefined, () => {
163
+ // Lazy initialization of the initial state
164
+ return {
165
+ inProgress: InteractionStatus.Startup,
166
+ accounts: instance.getAllAccounts()
114
167
  };
115
- }, [instance, accounts, logger]);
168
+ });
116
169
  useEffect(() => {
117
170
  const callbackId = instance.addEventCallback(message => {
118
- const status = EventMessageUtils.getInteractionStatusFromEvent(message, inProgressRef.current);
119
-
120
- if (status !== null) {
121
- logger.info(`MsalProvider - ${message.eventType} results in setting inProgress from ${inProgressRef.current} to ${status}`);
122
- inProgressRef.current = status;
123
- setInProgress(status);
124
- }
171
+ updateState({
172
+ payload: {
173
+ instance,
174
+ logger,
175
+ message
176
+ },
177
+ type: MsalProviderActionType.EVENT
178
+ });
125
179
  });
126
180
  logger.verbose(`MsalProvider - Registered event callback with id: ${callbackId}`);
127
181
  instance.handleRedirectPromise().catch(() => {
@@ -132,12 +186,16 @@ function MsalProvider({
132
186
  * If handleRedirectPromise returns a cached promise the necessary events may not be fired
133
187
  * This is a fallback to prevent inProgress from getting stuck in 'startup'
134
188
  */
135
- if (inProgressRef.current === InteractionStatus.Startup) {
136
- inProgressRef.current = InteractionStatus.None;
137
- setInProgress(InteractionStatus.None);
138
- }
189
+ updateState({
190
+ payload: {
191
+ instance,
192
+ logger
193
+ },
194
+ type: MsalProviderActionType.UNBLOCK_INPROGRESS
195
+ });
139
196
  });
140
197
  return () => {
198
+ // Remove callback when component unmounts or accounts change
141
199
  if (callbackId) {
142
200
  logger.verbose(`MsalProvider - Removing event callback ${callbackId}`);
143
201
  instance.removeEventCallback(callbackId);
@@ -146,8 +204,8 @@ function MsalProvider({
146
204
  }, [instance, logger]);
147
205
  const contextValue = {
148
206
  instance,
149
- inProgress,
150
- accounts,
207
+ inProgress: state.inProgress,
208
+ accounts: state.accounts,
151
209
  logger
152
210
  };
153
211
  return React__default.createElement(MsalContext.Provider, {
@@ -170,61 +228,9 @@ const useMsal = () => useContext(MsalContext);
170
228
  * Licensed under the MIT License.
171
229
  */
172
230
 
173
- function getAccount(instance, accountIdentifiers) {
174
- const allAccounts = instance.getAllAccounts();
175
-
176
- if (allAccounts.length > 0 && (accountIdentifiers.homeAccountId || accountIdentifiers.localAccountId || accountIdentifiers.username)) {
177
- const matchedAccounts = allAccounts.filter(accountObj => {
178
- if (accountIdentifiers.username && accountIdentifiers.username.toLowerCase() !== accountObj.username.toLowerCase()) {
179
- return false;
180
- }
181
-
182
- if (accountIdentifiers.homeAccountId && accountIdentifiers.homeAccountId.toLowerCase() !== accountObj.homeAccountId.toLowerCase()) {
183
- return false;
184
- }
185
-
186
- if (accountIdentifiers.localAccountId && accountIdentifiers.localAccountId.toLowerCase() !== accountObj.localAccountId.toLowerCase()) {
187
- return false;
188
- }
189
-
190
- return true;
191
- });
192
- return matchedAccounts[0] || null;
193
- } else {
194
- return null;
195
- }
196
- }
197
- /**
198
- * Given 1 or more accountIdentifiers, returns the Account object if the user is signed-in
199
- * @param accountIdentifiers
200
- */
201
-
202
-
203
- function useAccount(accountIdentifiers) {
204
- const {
205
- instance,
206
- inProgress
207
- } = useMsal();
208
- const initialStateValue = inProgress === InteractionStatus.Startup ? null : getAccount(instance, accountIdentifiers);
209
- const [account, setAccount] = useState(initialStateValue);
210
- useEffect(() => {
211
- const currentAccount = getAccount(instance, accountIdentifiers);
212
-
213
- if (!AccountEntity.accountInfoIsEqual(account, currentAccount, true)) {
214
- setAccount(currentAccount);
215
- }
216
- }, [inProgress, accountIdentifiers, instance, account]);
217
- return account;
218
- }
219
-
220
- /*
221
- * Copyright (c) Microsoft Corporation. All rights reserved.
222
- * Licensed under the MIT License.
223
- */
224
-
225
- function isAuthenticated(allAccounts, account, matchAccount) {
231
+ function isAuthenticated(allAccounts, matchAccount) {
226
232
  if (matchAccount && (matchAccount.username || matchAccount.homeAccountId || matchAccount.localAccountId)) {
227
- return !!account;
233
+ return !!getAccountByIdentifiers(allAccounts, matchAccount);
228
234
  }
229
235
 
230
236
  return allAccounts.length > 0;
@@ -237,15 +243,12 @@ function isAuthenticated(allAccounts, account, matchAccount) {
237
243
 
238
244
  function useIsAuthenticated(matchAccount) {
239
245
  const {
240
- accounts: allAccounts,
241
- inProgress
246
+ accounts: allAccounts
242
247
  } = useMsal();
243
- const account = useAccount(matchAccount || {});
244
- const initialStateValue = inProgress === InteractionStatus.Startup ? false : isAuthenticated(allAccounts, account, matchAccount);
245
- const [hasAuthenticated, setHasAuthenticated] = useState(initialStateValue);
248
+ const [hasAuthenticated, setHasAuthenticated] = useState(() => isAuthenticated(allAccounts, matchAccount));
246
249
  useEffect(() => {
247
- setHasAuthenticated(isAuthenticated(allAccounts, account, matchAccount));
248
- }, [allAccounts, account, matchAccount]);
250
+ setHasAuthenticated(isAuthenticated(allAccounts, matchAccount));
251
+ }, [allAccounts, matchAccount]);
249
252
  return hasAuthenticated;
250
253
  }
251
254
 
@@ -313,13 +316,86 @@ function UnauthenticatedTemplate({
313
316
  return null;
314
317
  }
315
318
 
319
+ /*
320
+ * Copyright (c) Microsoft Corporation. All rights reserved.
321
+ * Licensed under the MIT License.
322
+ */
323
+
324
+ function getAccount(instance, accountIdentifiers) {
325
+ if (!accountIdentifiers || !accountIdentifiers.homeAccountId && !accountIdentifiers.localAccountId && !accountIdentifiers.username) {
326
+ // If no account identifiers are provided, return active account
327
+ return instance.getActiveAccount();
328
+ }
329
+
330
+ return getAccountByIdentifiers(instance.getAllAccounts(), accountIdentifiers);
331
+ }
332
+ /**
333
+ * Given 1 or more accountIdentifiers, returns the Account object if the user is signed-in
334
+ * @param accountIdentifiers
335
+ */
336
+
337
+
338
+ function useAccount(accountIdentifiers) {
339
+ const {
340
+ instance,
341
+ inProgress,
342
+ logger
343
+ } = useMsal();
344
+ const [account, setAccount] = useState(() => getAccount(instance, accountIdentifiers));
345
+ useEffect(() => {
346
+ setAccount(currentAccount => {
347
+ const nextAccount = getAccount(instance, accountIdentifiers);
348
+
349
+ if (!AccountEntity.accountInfoIsEqual(currentAccount, nextAccount, true)) {
350
+ logger.info("useAccount - Updating account");
351
+ return nextAccount;
352
+ }
353
+
354
+ return currentAccount;
355
+ });
356
+ }, [inProgress, accountIdentifiers, instance, logger]);
357
+ return account;
358
+ }
359
+
360
+ /*
361
+ * Copyright (c) Microsoft Corporation. All rights reserved.
362
+ * Licensed under the MIT License.
363
+ */
364
+ const ReactAuthErrorMessage = {
365
+ invalidInteractionType: {
366
+ code: "invalid_interaction_type",
367
+ desc: "The provided interaction type is invalid."
368
+ },
369
+ unableToFallbackToInteraction: {
370
+ code: "unable_to_fallback_to_interaction",
371
+ desc: "Interaction is required but another interaction is already in progress. Please try again when the current interaction is complete."
372
+ }
373
+ };
374
+ class ReactAuthError extends AuthError {
375
+ constructor(errorCode, errorMessage) {
376
+ super(errorCode, errorMessage);
377
+ Object.setPrototypeOf(this, ReactAuthError.prototype);
378
+ this.name = "ReactAuthError";
379
+ }
380
+
381
+ static createInvalidInteractionTypeError() {
382
+ return new ReactAuthError(ReactAuthErrorMessage.invalidInteractionType.code, ReactAuthErrorMessage.invalidInteractionType.desc);
383
+ }
384
+
385
+ static createUnableToFallbackToInteractionError() {
386
+ return new ReactAuthError(ReactAuthErrorMessage.unableToFallbackToInteraction.code, ReactAuthErrorMessage.unableToFallbackToInteraction.desc);
387
+ }
388
+
389
+ }
390
+
316
391
  /*
317
392
  * Copyright (c) Microsoft Corporation. All rights reserved.
318
393
  * Licensed under the MIT License.
319
394
  */
320
395
  /**
321
- * Invokes a login call if a user is not currently signed-in. Failed logins can be retried using the login callback returned.
322
- * Optionally provide a request object to be used in the login call.
396
+ * If a user is not currently signed in this hook invokes a login. Failed logins can be retried using the login callback returned.
397
+ * If a user is currently signed in this hook attempts to acquire a token. Subsequent token requests can use the acquireToken callback returned.
398
+ * Optionally provide a request object to be used in the login/acquireToken call.
323
399
  * Optionally provide a specific user that should be logged in.
324
400
  * @param interactionType
325
401
  * @param authenticationRequest
@@ -333,8 +409,28 @@ function useMsalAuthentication(interactionType, authenticationRequest, accountId
333
409
  logger
334
410
  } = useMsal();
335
411
  const isAuthenticated = useIsAuthenticated(accountIdentifiers);
336
- const [[result, error], setResponse] = useState([null, null]);
337
- const [hasBeenCalled, setHasBeenCalled] = useState(false);
412
+ const account = useAccount(accountIdentifiers);
413
+ const [[result, error], setResponse] = useState([null, null]); // Boolean used to check if interaction is in progress in acquireTokenSilent fallback. Use Ref instead of state to prevent acquireToken function from being regenerated on each change to interactionInProgress value
414
+
415
+ const interactionInProgress = useRef(inProgress !== InteractionStatus.None);
416
+ useEffect(() => {
417
+ interactionInProgress.current = inProgress !== InteractionStatus.None;
418
+ }, [inProgress]); // Flag used to control when the hook calls login/acquireToken
419
+
420
+ const shouldAcquireToken = useRef(true);
421
+ useEffect(() => {
422
+ if (!!error) {
423
+ // Errors should be handled by consuming component
424
+ shouldAcquireToken.current = false;
425
+ return;
426
+ }
427
+
428
+ if (!!result) {
429
+ // Token has already been acquired, consuming component/application is responsible for renewing
430
+ shouldAcquireToken.current = false;
431
+ return;
432
+ }
433
+ }, [error, result]);
338
434
  const login = useCallback(async (callbackInteractionType, callbackRequest) => {
339
435
  const loginType = callbackInteractionType || interactionType;
340
436
  const loginRequest = callbackRequest || authenticationRequest;
@@ -354,9 +450,59 @@ function useMsalAuthentication(interactionType, authenticationRequest, accountId
354
450
  return instance.ssoSilent(loginRequest);
355
451
 
356
452
  default:
357
- throw "Invalid interaction type provided.";
453
+ throw ReactAuthError.createInvalidInteractionTypeError();
358
454
  }
359
455
  }, [instance, interactionType, authenticationRequest, logger]);
456
+ const acquireToken = useCallback(async (callbackInteractionType, callbackRequest) => {
457
+ const fallbackInteractionType = callbackInteractionType || interactionType;
458
+ let tokenRequest;
459
+
460
+ if (callbackRequest) {
461
+ logger.trace("useMsalAuthentication - acquireToken - Using request provided in the callback");
462
+ tokenRequest = { ...callbackRequest
463
+ };
464
+ } else if (authenticationRequest) {
465
+ logger.trace("useMsalAuthentication - acquireToken - Using request provided in the hook");
466
+ tokenRequest = { ...authenticationRequest,
467
+ scopes: authenticationRequest.scopes || OIDC_DEFAULT_SCOPES
468
+ };
469
+ } else {
470
+ logger.trace("useMsalAuthentication - acquireToken - No request object provided, using default request.");
471
+ tokenRequest = {
472
+ scopes: OIDC_DEFAULT_SCOPES
473
+ };
474
+ }
475
+
476
+ if (!tokenRequest.account && account) {
477
+ logger.trace("useMsalAuthentication - acquireToken - Attaching account to request");
478
+ tokenRequest.account = account;
479
+ }
480
+
481
+ const getToken = async () => {
482
+ logger.verbose("useMsalAuthentication - Calling acquireTokenSilent");
483
+ return instance.acquireTokenSilent(tokenRequest).catch(async e => {
484
+ if (e instanceof InteractionRequiredAuthError) {
485
+ if (!interactionInProgress.current) {
486
+ logger.error("useMsalAuthentication - Interaction required, falling back to interaction");
487
+ return login(fallbackInteractionType, tokenRequest);
488
+ } else {
489
+ logger.error("useMsalAuthentication - Interaction required but is already in progress. Please try again, if needed, after interaction completes.");
490
+ throw ReactAuthError.createUnableToFallbackToInteractionError();
491
+ }
492
+ }
493
+
494
+ throw e;
495
+ });
496
+ };
497
+
498
+ return getToken().then(response => {
499
+ setResponse([response, null]);
500
+ return response;
501
+ }).catch(e => {
502
+ setResponse([null, e]);
503
+ throw e;
504
+ });
505
+ }, [instance, interactionType, authenticationRequest, logger, account, login]);
360
506
  useEffect(() => {
361
507
  const callbackId = instance.addEventCallback(message => {
362
508
  switch (message.eventType) {
@@ -386,18 +532,27 @@ function useMsalAuthentication(interactionType, authenticationRequest, accountId
386
532
  };
387
533
  }, [instance, logger]);
388
534
  useEffect(() => {
389
- if (!hasBeenCalled && !error && !isAuthenticated && inProgress === InteractionStatus.None) {
390
- logger.info("useMsalAuthentication - No user is authenticated, attempting to login"); // Ensure login is only called one time from within this hook, any subsequent login attempts should use the callback returned
391
-
392
- setHasBeenCalled(true);
393
- login().catch(() => {
394
- // Errors are handled by the event handler above
395
- return;
396
- });
535
+ if (shouldAcquireToken.current && inProgress === InteractionStatus.None) {
536
+ shouldAcquireToken.current = false;
537
+
538
+ if (!isAuthenticated) {
539
+ logger.info("useMsalAuthentication - No user is authenticated, attempting to login");
540
+ login().catch(() => {
541
+ // Errors are saved in state above
542
+ return;
543
+ });
544
+ } else if (account) {
545
+ logger.info("useMsalAuthentication - User is authenticated, attempting to acquire token");
546
+ acquireToken().catch(() => {
547
+ // Errors are saved in state above
548
+ return;
549
+ });
550
+ }
397
551
  }
398
- }, [isAuthenticated, inProgress, error, hasBeenCalled, login, logger]);
552
+ }, [isAuthenticated, account, inProgress, login, acquireToken, logger]);
399
553
  return {
400
554
  login,
555
+ acquireToken,
401
556
  result,
402
557
  error
403
558
  };