-
Notifications
You must be signed in to change notification settings - Fork 120
MSAL.js with NAA does not refresh access token after claims challenge CAE #6534
Description
Provide required information needed to triage your issue
Your Environment
- Platform [PC desktop, Mac, iOS, Office on the web]: New Outlook on windows desktop
- Host [Excel, Word, PowerPoint, etc.]: Outlook
- Microsoft Outlook Version: 1.2026.210.300
- Client Version: 20260213001.06
- WebView2 Version: 145.0.3800.65 (Stable)
- Operating System: Windows
- Browser (if using Office on the web):
Expected behavior
When acquireTokenSilent or acquireTokenPopup is called with a claims parameter (containing a decoded CAE claims challenge from a Graph API 401 response) in the Nested App Authentication (NAA) flow via createNestablePublicClientApplication:
- A new token should be issued that satisfies the claims challenge.
- The new token should be cached, replacing the previous stale token.
- Subsequent calls to
acquireTokenSilent(withoutclaims) should return the new, valid token.
Current behavior
In the NAA flow, acquireTokenSilent or acquireTokenPopup with the claims parameter does return a valid token that satisfies the claims challenge - the immediate Graph call succeeds (HTTP 200). However, MSAL does not update its cache with this new token. On subsequent calls:
acquireTokenSilentwithoutclaimsreturns the old cached token (the one that was revoked/insufficient), not the new valid token that was just acquired.- Graph rejects this old token again with HTTP 401 and the same claims challenge.
- This forces the application to re-acquire with
claimson every single API call, defeating the purpose of token caching entirely.
Steps to reproduce
- Create an Office Add-in using NAA (
createNestablePublicClientApplication) withclientCapabilities: ["CP1"]andsupportsNestedAppAuth: true. - Register the Graph app, SPA redirect URI
brk-multihub://..., and authorized Office client app IDs. - Sign in via the add-in and successfully call Microsoft Graph (e.g.,
GET /v1.0/me) -> confirm HTTP 200. - Trigger a CAE event by revoking the user's sessions in Entra ID.
- Call Microsoft Graph again from the add-in -> Graph returns HTTP 401 with
WWW-Authenticate: Bearer ... claims="eyJhY2..."- Sometimes I have to wait for up to 15 mins for CAE to trigger. - Parse and base64-decode the claims value from the header.
- Call
acquireTokenSilent({ scopes: ["User.Read"], account, claims: decodedClaims })-> MSAL returns a new valid token. Call Graph with it -> HTTP 200 (success). - On the next call, use
acquireTokenSilent({ scopes: ["User.Read"], account })withoutclaims-> MSAL returns the old cached token, not the valid token from step 7. - Call Graph with this old token -> HTTP 401 again with the same claims challenge.
- The application is now forced to pass
claimson every single token request to get a valid token, because MSAL never updates its cache.
Link to live example(s)
- Reproduction repository: https://github.com/Osaibi-RivaEngine/NAA_CAE clone and follow the setup steps below.
- Setup: You must create your own Graph app registration and update the client ID in the project before running:
- Open
src/auth/authConfig.tsand replace theCLIENT_IDvalue with your own Client ID. - Open
manifest.jsonand updatewebApplicationInfo.idandwebApplicationInfo.resourcewith your client ID. - Ensure your app registration has a SPA redirect URI
brk-multihub://...and the required authorized Office client app IDs.
- Open
Provide additional details
@azure/msal-browserversion: 4.28.2- The
claimsparameter is a decoded JSON string (from the base64 value in theWWW-Authenticateheader), e.g.:{"access_token":{"nbf":{"essential":true, "value":"1771868574"}}}
Attempted mitigations - none worked
1. forceRefresh: true
We tried to set request.forceRefresh = true when a claims challenge is present, expecting MSAL to bypass the cache:
if (storedClaims) {
request.forceRefresh = true;
}
const result = await pca.acquireTokenSilent(request);acquireTokenSilent calls still return the old stale token from cache.
2. setActiveAccount after acquiring the valid token
After each successful acquireTokenSilent or acquireTokenPopup call with claims, we explicitly call pca.setActiveAccount(result.account) to ensure the new token is associated with the active account:
const result = await pca.acquireTokenSilent(request);
if (result.account) {
pca.setActiveAccount(result.account);
}This has no effect on the caching issue. The next acquireTokenSilent without claims still returns the old cached token.
Context
We are building an Outlook Desktop add-in that calls Microsoft Graph and must comply with organisation's Conditional Access policies (session revocation, location-based access, MFA step-up). We opted in to CAE via clientCapabilities: ["CP1"] and implemented full claims-challenge handling as documented. However because the NAA does not cache the new valid token after a claims challenge, every subsequent API call requires two round-trips - one that fails with a 401 and triggers the claims flow, and a second that passes the claims to get a fresh token. This does not break our application - it remains functional - but it doubles the number of network requests needed for every Graph call after a CAE event, degrading performance and user experience unnecessarily.
This is from the sample app provided in live example:
Useful logs
- Console errors -
acquireTokenSilentwithclaimsreturns a valid new token (confirmed by successful Graph call). However, a subsequentacquireTokenSilentwithoutclaimsreturns a different JWT, the old cached token with earliernbf/iattimestamps. Comparing the two JWTs confirms the cache was not updated.
