AzureAD / microsoft-authentication-library-for-js

Microsoft Authentication Library (MSAL) for JS
http://aka.ms/aadv2
MIT License
3.61k stars 2.64k forks source link

Keep Me Signed In Not Working, x-ms-cpim-sso:{Id} Cookie Not Persisting #7288

Open akopplinufpi opened 1 week ago

akopplinufpi commented 1 week ago

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

^3.4.0

Wrapper Library

MSAL React (@azure/msal-react)

Wrapper Library Version

^2.0.7

Public or Confidential Client?

Public

Description

We are experiencing an issue where the user selects "Keep Me Signed In" (KMSI) when logging in. But the user is unable to get new tokens after 24 hours without interaction (acquiretokensilent VS acquiretokenredirect).

In the Microsoft documentation it is stated that the cookie used to persist the KMSI setting is called x-ms-cpim-sso:{Id}

image

source: https://learn.microsoft.com/en-us/azure/active-directory-b2c/cookie-definitions

I verified that the token is being successfully stored in the browser:

image

The expiration date on the cookie is 90 days from today. However, when I close the browser and re-open, the cookie is no longer there. The fact that the cookie is getting deleted therefore is breaking the KMSI functionality. I have tested this in both Chrome and Edge.

Here are the SPA redirect URI's that we are using the in the app

image

We are using user flow

image

We have keep me signed in selected

image

Error Message

No response

MSAL Logs

main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-react@2.0.7 : Info - useAccount - Updating account main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-react@2.0.7 : Info - useAccount - Updating account main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-browser@3.5.0 : Info - Emitting event: msal:initializeStart main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-browser@3.5.0 : Info - Emitting event: msal:initializeEnd main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-browser@3.5.0 : Info - Emitting event: msal:handleRedirectStart main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-react@2.0.7 : Info - MsalProvider - msal:handleRedirectStart results in setting inProgress from startup to handleRedirect main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-react@2.0.7 : Info - useAccount - Updating account main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-react@2.0.7 : Info - useAccount - Updating account jobs:1 GET https://(my-domain.com)/favicons/site.webmanifest 404 (Not Found) site.webmanifest:1 Manifest: Line: 1, column: 1, Syntax error. main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-browser@3.5.0 : Info - BrowserCacheManager: addTokenKey - idToken added to map main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-browser@3.5.0 : Info - BrowserCacheManager: addTokenKey - refreshToken added to map main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-browser@3.5.0 : Info - Emitting event: msal:loginSuccess main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-browser@3.5.0 : Info - Emitting event: msal:handleRedirectEnd main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-react@2.0.7 : Info - MsalProvider - msal:handleRedirectEnd results in setting inProgress from handleRedirect to none main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-react@2.0.7 : Info - useAccount - Updating account main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-react@2.0.7 : Info - useAccount - Updating account main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-react@2.0.7 : Info - useAccount - Updating account main.be3b1d23.js:2 GET https://(my-domain.com)/api/jobs/jobdetails 401 (Unauthorized) (anonymous) @ main.be3b1d23.js:2 (anonymous) @ main.be3b1d23.js:2 xhr @ main.be3b1d23.js:2 HI @ main.be3b1d23.js:2 Promise.then (async) request @ main.be3b1d23.js:2 QT.forEach.YI. @ main.be3b1d23.js:2 (anonymous) @ main.be3b1d23.js:2 makeGetRequest @ main.be3b1d23.js:2 getAPIResponseAsync @ main.be3b1d23.js:2 loadJobData @ main.be3b1d23.js:2 loadJobDataAsync @ main.be3b1d23.js:2 (anonymous) @ main.be3b1d23.js:2 ol @ main.be3b1d23.js:2 _c @ main.be3b1d23.js:2 cc @ main.be3b1d23.js:2 Wo @ main.be3b1d23.js:2 (anonymous) @ main.be3b1d23.js:2 Cc @ main.be3b1d23.js:2 ic @ main.be3b1d23.js:2 C @ main.be3b1d23.js:2 I @ main.be3b1d23.js:2 main.be3b1d23.js:2 Unauthorized, trying to get token and retrying request. Route: jobs/jobDetails main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-browser@3.5.0 : Info - Emitting event: msal:acquireTokenStart main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getAccessToken - No token found main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getRefreshToken - returning refresh token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [3e0333b9-cdf7-4819-992f-b09dafa3560f] : @azure/msal-common@14.4.0 : Info - Token refresh is required due to cache outcome: 2 main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-browser@3.5.0 : Info - Emitting event: msal:acquireTokenFromNetworkStart main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getRefreshToken - returning refresh token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 GET https://(my-domain.com)/api/user/userrole 401 (Unauthorized) (anonymous) @ main.be3b1d23.js:2 (anonymous) @ main.be3b1d23.js:2 xhr @ main.be3b1d23.js:2 HI @ main.be3b1d23.js:2 Promise.then (async) request @ main.be3b1d23.js:2 QT.forEach.YI. @ main.be3b1d23.js:2 (anonymous) @ main.be3b1d23.js:2 makeGetRequest @ main.be3b1d23.js:2 getAPIResponseAsync @ main.be3b1d23.js:2 (anonymous) @ main.be3b1d23.js:2 (anonymous) @ main.be3b1d23.js:2 ol @ main.be3b1d23.js:2 _c @ main.be3b1d23.js:2 cc @ main.be3b1d23.js:2 Wo @ main.be3b1d23.js:2 (anonymous) @ main.be3b1d23.js:2 Cc @ main.be3b1d23.js:2 ic @ main.be3b1d23.js:2 C @ main.be3b1d23.js:2 I @ main.be3b1d23.js:2 main.be3b1d23.js:2 Unauthorized, trying to get token and retrying request. Route: User/UserRole main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-browser@3.5.0 : Info - BrowserCacheManager: addTokenKey - accessToken added to map main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-browser@3.5.0 : Info - Emitting event: msal:acquireTokenSuccess main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:52 GMT] : [] : @azure/msal-react@2.0.7 : Info - useAccount - Updating account main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:55 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:55 GMT] : [] : @azure/msal-common@14.4.0 : Info - CacheManager:getIdToken - Returning id token main.be3b1d23.js:2 [Tue, 03 Sep 2024 14:20:55 GMT] : [] : @azure/msal-react@2.0.7 : Info - useAccount - Updating account

Network Trace (Preferrably Fiddler)

MSAL Configuration

import { LogLevel } 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;
const isEdge = msedge > 0;
const isFirefox = firefox > 0; // Only needed if you need to support the redirect flow in Firefox incognito

/**
 * 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 b2cPolicies = {
    names: {
        signUpSignIn: "B2C_1_{my-b2c-tenant}.SignInSignUp",
        editProfile: "B2C_1_{my-b2c-tenant}.EditProfile"
    },
    authorities: {
        signUpSignIn: {
            authority:
                "https://{my-b2c-tenant}.b2clogin.com/{my-b2c-tenant}.onmicrosoft.com/B2C_1_{my-b2c-tenant}.SignInSignUp"
        },
        editProfile: {
            authority:
                "https://{my-b2c-tenant}.b2clogin.com/{my-b2c-tenant}.onmicrosoft.com/B2C_1_{my-b2c-tenant}.ProfileEditing"
        }
    },
    authorityDomain: "{my-b2c-tenant}.b2clogin.com"
};

// Config object to be passed to Msal on creation
export const msalConfig = {
    auth: {
        clientId: "13c05f42-a185-47f3-a9b4-15cab19dc7b3",
        authority: b2cPolicies.authorities.signUpSignIn.authority,
        knownAuthorities: [b2cPolicies.authorityDomain],
        // redirectUri: window.location.origin + "/jobs",
        postLogoutRedirectUri: "/",
        scopes: [
            "https://{my-b2c-tenant}.onmicrosoft.com/api/TrussTrax.Read",
            "https://{my-b2c-tenant}.onmicrosoft.com/api/TrussTrax.Write"
        ]
    },
    cache: {
        cacheLocation: "localStorage",
        storeAuthStateInCookie: isIE || isEdge || isFirefox
    },
    system: {
        loggerOptions: {
            logLevel: LogLevel.Info,
            loggerCallback: (level, message, containsPii) => {
                if (containsPii) {
                    return;
                }
                switch (level) {
                    case LogLevel.Error:
                        console.error(message);
                        return;
                    case LogLevel.Info:
                        console.info(message);
                        return;
                    case LogLevel.Verbose:
                        console.debug(message);
                        return;
                    case LogLevel.Warning:
                        console.warn(message);
                        return;
                    default:
                        return;
                }
            }
        }
    }
};

Relevant Code Snippets

in app.jsx, we wrap our /jobs route in a component we call .


<Route
    path="/jobs"
    element={
        <RequireAuth>
        <PageTracking route="/jobs">
            <Jobs
            isLoading={isLoading}
            jobsStatuses={jobStatuses}
        />
            </PageTracking>
    </RequireAuth>
    }
/>

here is the relevant code from RequireAuth

return (
    <>
        <MsalAuthenticationTemplate
            interactionType={InteractionType.Redirect}
            authenticationRequest={config.auth.scopes}
            errorComponent={ErrorComponent}
            loadingComponent={LoadingComponent}>
            {role === null ? <CenterSpinner /> : role === "None" ? <Welcome /> : children}
        </MsalAuthenticationTemplate>
    </>
);

Here is the code from our API class that is responsible for getting a valid token and attaching it to network requests. After 24 hours, when a network request is made, the user is redirected to /notauthorized

public getTokenAsync = async (redirectOnUnauthorized: boolean = true) => {
        const activeAccount = this.msalInstance.getActiveAccount();
        const accounts = this.msalInstance.getAllAccounts();

        const request = {
            scopes: this.msalInstance.controller.config.auth.scopes,
            account: activeAccount || accounts[0]
        };

        try {
            // tokens expire every 20 or 30 mins. politely ask MSAL for a new token
            const authResult = await this.msalInstance.acquireTokenSilent(request);
            return authResult.accessToken;
        } catch (error) {
            // if we cant get a token, the user needs to re-authenticate

            if (redirectOnUnauthorized) {
                if (window.location.pathname !== "/notauthorized") {
                    window.location.href = "/notauthorized";
                }
            } else {
                throw new Error("User not authorized");
            }

            // TODO this would be nice to get this working
            // const result = await this.msalInstance.acquireTokenRedirect(request);
            // return result.accessToken;
        }
    };

If the users role is null, we we render a spinner, while the user is being redirected to B2C to sign in. You can see the scopes that are used to sign in from the MSAL Configuration above. Once the user comes back from being redirected to B2C, the user gets a token, which is then used to make an API call to get the role. Once the user has their role, they are let in to the app, which is rendered by {children}

Reproduction Steps

If you would like to reproduce this issue in our dev environment, please contact me and I will be able to get you access.

Expected Behavior

The expected behavior is that the cookie x-ms-cpim-sso:{Id} should not get deleted when the browser closes. it should stay in the cookies in the browser and allow the user to stay signed in for up to 90 days. What we are experiencing is that the user is again unable to retrieve a token after 24 hours without interaction.

Identity Provider

Azure B2C Custom Policy

Browsers Affected (Select all that apply)

Chrome, Edge

Regression

No response

Source

External (Customer)

akopplinufpi commented 1 week ago

Update: Ive found that if I shift+F5 refresh, then the cookie re-appears in the browser. However this still does not explain why users are booted out after 24 hours

sameerag commented 1 week ago

@akopplinufpi A SPA RT is only valid for 24 hrs by design in the default case. MSAL JS however does not control the browser cookies or the KMSI cookie presence enabling the server to issue a valid RT silently. Did you check with B2C service if they have an explanation? Support link here.

Please let me know if there is no response and I can try engage the B2C team here.

akopplinufpi commented 6 days ago

Hi @sameerag thank you for taking the time to respond! I went to create the support request, and it looks like the "Technical" issue type is not available. I went ahead and created a request anyways... Can you ping the B2C team to see if they got the request? support request number: 2409090040008015

image