question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Warning - Account used to refresh tokens not in persistence, refreshed tokens will not be stored in the cache

See original GitHub issue

Core Library

MSAL.js v2 (@azure/msal-browser)

Core Library Version

2.26.0

Wrapper Library

MSAL React (@azure/msal-react)

Wrapper Library Version

1.4.2

Description

I am writing an SPA with back-end REST API. After updating my npm packages, I noticed that my browser’s console log is full of warnings when running my app saying

@azure/msal-common@7.0.0 : Warning - Account used to refresh tokens not in persistence, refreshed tokens will not be stored in the cache

I’ve also noticed that requests to the endpoint

https://mycompanyauth.b2clogin.com/mycompanyauth.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/token

take between 3 and 11 seconds which seems rather long to me.

What’s wrong here?

Error Message

@azure/msal-common@7.0.0 : Warning - Account used to refresh tokens not in persistence, refreshed tokens will not be stored in the cache

Msal Logs

No response

MSAL Configuration

const ua = window.navigator.userAgent
const msie = ua.indexOf("MSIE ")
const msie11 = ua.indexOf("Trident/")
const msedge = ua.indexOf("Edge/")
const firefox = ua.indexOf("Firefox")
const isIE = msie > 0 || msie11 > 0 // eslint-disable-line @typescript-eslint/no-unused-vars
const isEdge = msedge > 0
 // Only needed if you need to support the redirect flow in Firefox incognito:
const isFirefox = firefox > 0  // eslint-disable-line @typescript-eslint/no-unused-vars


export const msalConfig: Configuration = {
    auth: {
        clientId: process.env.REACT_APP_CLIENT_ID as string,
        authority: b2cConfiguration.policies.signUpSignIn.authority,
        knownAuthorities: [b2cConfiguration.authorityDomain],
        redirectUri: process.env.REACT_APP_REDIRECT_URI as string,
        navigateToLoginRequestUrl: true,
        postLogoutRedirectUri: process.env.REACT_APP_POST_LOGOUT_REDIRECT_URI as string
    },
    cache: {
        cacheLocation: "sessionStorage", // This configures where your cache will be stored
        storeAuthStateInCookie: isEdge // Set this to "true" if you are having issues on Edge
    },
    system: {
        loggerOptions: {
            loggerCallback: (level, message, containsPii) => {
                if (containsPii) {
                    return
                }
                switch (level) {
                    case LogLevel.Error:
                        console.error(message)
                        return
                    case LogLevel.Info:
                        console.debug(message)
                        return
                    case LogLevel.Verbose:
                        console.debug(message)
                        return
                    case LogLevel.Warning:
                        console.warn(message)
                        return
                }
            }
        }
    }
}

Relevant Code Snippets

File azure-auth-config.ts:

import { Configuration, LogLevel, RedirectRequest } from "@azure/msal-browser"

// Browser check variables
// If you support IE, our recommendation is that you sign-in using Redirect APIs
// If you as a developer are testing using Edge InPrivate mode, please add "isEdge" to the if check
const ua = window.navigator.userAgent
const msie = ua.indexOf("MSIE ")
const msie11 = ua.indexOf("Trident/")
const msedge = ua.indexOf("Edge/")
const firefox = ua.indexOf("Firefox")
const isIE = msie > 0 || msie11 > 0 // eslint-disable-line @typescript-eslint/no-unused-vars
const isEdge = msedge > 0
 // Only needed if you need to support the redirect flow in Firefox incognito:
const isFirefox = firefox > 0  // eslint-disable-line @typescript-eslint/no-unused-vars

export type B2CConfiguration = Readonly<{
    authorityDomain: string,
    policies: Readonly<{
        signUpSignIn: Readonly<{
            name: string
            authority: string
        }>
        resetPassword: Readonly<{
            name: string
            authority: string
        }>
        editProfile: Readonly<{
            name: string
            authority: string
        }>
    }>
}>

/**
 * Enter here the user flows and custom policies for your B2C application
 * To learn more about user flows, visit: https://docs.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview
 * To learn more about custom policies, visit: https://docs.microsoft.com/en-us/azure/active-directory-b2c/custom-policy-overview
 */
 export const b2cConfiguration: B2CConfiguration = {
    authorityDomain: process.env.REACT_APP_AUTHORITY_DOMAIN as string,
    policies: {
        signUpSignIn: {
            name: process.env.REACT_APP_POLICY_NAME_SIGNUPSIGNIN as string,
            authority: process.env.REACT_APP_AUTHORITY_SIGNUPSIGNIN as string
        },
        resetPassword: {
            name: process.env.REACT_APP_POLICY_NAME_RESETPASSWORD as string,
            authority: process.env.REACT_APP_AUTHORITY_RESETPASSWORD as string
        },
        editProfile: {
            name: process.env.REACT_APP_POLICY_NAME_EDITPROFILE as string,
            authority: process.env.REACT_APP_AUTHORITY_EDITPROFILE as string
        },
    }
}

/**
 * Configuration object to be passed to MSAL instance on creation. 
 * For a full list of MSAL.js configuration parameters, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md 
 */
export const msalConfig: Configuration = {
    auth: {
        clientId: process.env.REACT_APP_CLIENT_ID as string,
        authority: b2cConfiguration.policies.signUpSignIn.authority,
        knownAuthorities: [b2cConfiguration.authorityDomain],
        redirectUri: process.env.REACT_APP_REDIRECT_URI as string,
        navigateToLoginRequestUrl: true,
        postLogoutRedirectUri: process.env.REACT_APP_POST_LOGOUT_REDIRECT_URI as string
    },
    cache: {
        cacheLocation: "sessionStorage", // This configures where your cache will be stored
        storeAuthStateInCookie: isEdge // Set this to "true" if you are having issues on Edge
    },
    system: {
        loggerOptions: {
            loggerCallback: (level, message, containsPii) => {
                if (containsPii) {
                    return
                }
                switch (level) {
                    case LogLevel.Error:
                        console.error(message)
                        return
                    case LogLevel.Info:
                        console.debug(message)
                        return
                    case LogLevel.Verbose:
                        console.debug(message)
                        return
                    case LogLevel.Warning:
                        console.warn(message)
                        return
                }
            }
        }
    }
}

/**
 * Scopes you add here will be prompted for user consent during sign-in.
 * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
 * For more information about OIDC scopes, visit: 
 * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
 */
export const loginRequest: RedirectRequest = {
    scopes: [
    ],
    authority: b2cConfiguration.policies.signUpSignIn.authority
}

export const resetPasswordRequest: RedirectRequest = {
    scopes: [
    ],
    authority: b2cConfiguration.policies.resetPassword.authority
}

export const editProfileRequest: RedirectRequest = {
    scopes: [
    ],
    authority: b2cConfiguration.policies.editProfile.authority
}

export const backEndConfig = {
    // see https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md
    accessTokenScopes: [
        `${process.env.REACT_APP_BACK_END_APP_ID_URI}/Api.AccessAsUser`
    ]
}

File azure-auth-provider.ts:

import { AccountInfo, AuthError, EventMessage, EventType, InteractionRequiredAuthError, IPublicClientApplication, PublicClientApplication } from "@azure/msal-browser"
import { b2cConfiguration, msalConfig, resetPasswordRequest } from "./azure-auth-config"

export const msalSingleton: IPublicClientApplication = new PublicClientApplication(msalConfig)

// Account selection logic is app dependent. Adjust as needed for different use cases.
const accounts = msalSingleton.getAllAccounts()
if (accounts.length > 0) {
    msalSingleton.setActiveAccount(accounts[0])
}

// Compare https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/2084#issuecomment-675013771
type IdTokenClaims = { 
    tfp: string
}

msalSingleton.addEventCallback((event: EventMessage) => {
    if (event.eventType === EventType.LOGIN_FAILURE) {
        /**
         * Gets triggered when user clicks on "forgot password" button in Microsoft Login Page.
         * Microsoft only redirects to application, but does NOT execute any userflow. This has
         * to be done by the application.
         * See https://github.com/Azure-Samples/active-directory-b2c-javascript-msal-singlepageapp/issues/9
         * and https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/1245
         */ 
        if (event.error && event.error instanceof AuthError && event.error.errorMessage.indexOf("AADB2C90118") > -1) {
            console.debug("Invoking B2C password reset policy...")
            msalSingleton.loginRedirect(resetPasswordRequest)
        }
    }

    if (event.eventType === EventType.LOGIN_SUCCESS || event.eventType === EventType.ACQUIRE_TOKEN_SUCCESS){
        if(event.payload){
            /**
             * We need to reject id tokens that were not issued with the default sign-in policy.
             * The "tfp" claim in the token tells us what policy is used.
             * To learn more about B2C tokens, visit https://docs.microsoft.com/en-us/azure/active-directory-b2c/tokens-overview
             */
            if("idTokenClaims" in event.payload && event.payload.idTokenClaims){
                const idTokenClaims = event.payload.idTokenClaims as IdTokenClaims
                if (idTokenClaims.tfp === b2cConfiguration.policies.resetPassword.name) {
                    msalSingleton.logoutRedirect();
                } else if (idTokenClaims.tfp === b2cConfiguration.policies.editProfile.name) {
                    msalSingleton.logoutRedirect()
                }
            }

            if("account" in event.payload && event.payload.account){
                const account: AccountInfo = event.payload.account
                console.debug("Login successful. Setting account information...")
                msalSingleton.setActiveAccount(account)
                console.debug("Account information set.")
            }
        }
    }
})

export async function acquireSilentAccessTokenFromAzure(msalInstance: IPublicClientApplication, scopes: string[]): Promise<string>{
    const account = msalInstance.getActiveAccount()
    if (!account) {
        throw new Error("No active account! Verify a user has been signed in and msalInstance.setActiveAccount has been called.")
    }
    const request = {
        scopes,
        account: account
    }
    try{
        const accessToken = (await msalInstance.acquireTokenSilent(request)).accessToken
        console.debug("Access Token acquired:")
        console.debug(accessToken)
        return accessToken
    } catch (error){
        if (error instanceof InteractionRequiredAuthError) {
            // fallback to interaction when silent call fails:
            await msalInstance.acquireTokenRedirect(request)
            throw new Error("The acquireTokenRedirect method did not trigger a redirect. This should never happen!")
        } else {
            console.error(error)
            throw error
        }
    }

}

File hooks.ts:

export function useAcquireSilentAccessTokenFromAzure(scopes: string[]): () => Promise<string> {
    const { instance: msalInstance } = useMsal()
    const acquire = useCallback(() => acquireSilentAccessTokenFromAzure(msalInstance, scopes), [msalInstance, scopes])
    return acquire
}

export function useAcquireBackEndAccessToken(): () => Promise<string>{
    return useAcquireSilentAccessTokenFromAzure(backEndConfig.accessTokenScopes)
}

Now in a React component get the hook

const acquireBackEndAccessToken = useAcquireBackEndAccessToken()

and request a token:

const token = await acquireBackEndAccessToken()

Check the console for warnings.

Reproduction Steps

Happens each time an access token is requested.

Expected Behavior

Empty console log and snappy token requests with response times under 1 second.

Identity Provider

Azure B2C Custom Policy

Browsers Affected (Select all that apply)

Chrome

Regression

No response

Source

External (Customer)

Issue Analytics

  • State:closed
  • Created a year ago
  • Comments:36 (7 by maintainers)

github_iconTop GitHub Comments

1reaction
florianeidnercommented, Sep 2, 2022

As @svdHero and @derisen already figured out, the issue was related to our custom B2C policies. In the end, we were using <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}"> in the SignUpOrSignin.xml policy and didn’t assign the Output Claim “tenantId” in the Technical Profil for Session Management (“SM-RefreshTokenReadAndSetup”).

Removing the default value from the policy and adding the output claim <OutputClaim ClaimTypeReferenceId="tenantId"/> to the SM-RefreshTokenReadAndSetup Technical Profile solved the issue for us. Thanks for pointing in the right direction!

1reaction
svdHerocommented, Aug 4, 2022

By hint of @derisen (via email) I’ve just found out that the problem only occurs with internal user accounts, but not with local B2C user accounts. The internal user accounts are from our company AAD that is configured in B2C as an OpenID Connect identity provider.

I will ask our IT department to add a limited test user account for @derisen to our company AAD for further investigations.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Missing ability to refresh token in msal-node when acquired by ...
acquireTokenByRefreshToken will refresh the tokens and populate the cache for you. From then on, you use acquireTokenSilent. You are right that ...
Read more >
Persist Token Cache or Better Solution - Microsoft Q&A
Here's the problem: every time I want to push any updated to prod the cache of tokens are lost due to being held...
Read more >
What Are Refresh Tokens and How to Use Them Securely
This post will explore the concept of refresh tokens as defined by OAuth 2.0. We will learn how they compare to other token...
Read more >
Where to store the refresh token on the Client? - Stack Overflow
Access token and refresh token shouldn't be stored in the local/session storage, because they are not a place for any sensitive data. Hence...
Read more >
Persistent login in React using refresh token rotation
Refresh tokens have a long lifetime. If they are valid and not expired, clients can obtain new access tokens. This long lifetime may...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found