Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some questions about using msal@2.x with SPA and B2C - acquireTokenSilent cache and refresh_token #1932

Closed
4 tasks
Ikaer opened this issue Jul 11, 2020 · 8 comments · Fixed by #1962 or #1946
Closed
4 tasks
Assignees
Labels
b2c Related to Azure B2C library-specific issues msal-browser Related to msal-browser package msal-common Related to msal-common package question Customer is asking for a clarification, use case or information.

Comments

@Ikaer
Copy link

Ikaer commented Jul 11, 2020

Please follow the issue template below. Failure to do so will result in a delay in answering your question.

Library

  • msal@1.x.x or @azure/msal@1.x.x
  • [x ] @azure/msal-browser@2.x.x
  • @azure/msal-angular@0.x.x
  • @azure/msal-angular@1.x.x
  • @azure/msal-angularjs@1.x.x

Description

Please provide your question here, including as much relevant details as possible.

Hi,

I'm trying to use msal-browser@2.0.0 in spa mode (one asp.net app, one asp.net web api and an AD B2C in the middle for auth).
I've managed to get things working, sort of, but there is always something that is not completely ok.

For example, msal always call b2c when I'm using acquireTokenSilent even if I have an access token in cache. For what I've picked in other treads it can happened if I ask multiple resource.
So I've checked what kind of resources are asked,

when I'm calling initially loginRedirect, i'm asking for following scopes:
image
image

Then on first call to acquireTokenSilent before making an API call, I asked those one:
image
image
I didn't asked for openid and profile in my configuration, after checking msal code, those extra scope openid and profile are added automatically by the framework. Maybe its normal.

If I check my sessionStorage, I have the following values for the access token:

key:  xxx-b2c_1_susi.xxx-xxx.b2clogin.com-accesstoken-xxx--https://xx.onmicrosoft.com/xx/read https://xx.onmicrosoft.com/xx/write

value:
cachedAt: "1594479923"
clientId: "xxx"
credentialType: "AccessToken"
environment: "xx.b2clogin.com"
expiresOn: "1594483523"
extendedExpiresOn: "NaN"
homeAccountId: "xxxxx"
secret: "xxxx"
target: "https://xx.onmicrosoft.com/xx/read https://xx.onmicrosoft.com/xx/write"

So is it why the cache does not kick in ? scopes are different from the one asked ? if it is the case, how can I prevent msal to add those extra scopes ?

Second problem: when this second call is made with the refresh token provided by the first one, the response does not contains a new refresh token because 'offline_access' has not been precised in scopes. So a the cache stores the following key:

xx-b2c_1_susi.xx-xx.b2clogin.com-refreshtoken-xx
clientId: "xxx"
credentialType: "RefreshToken"
environment: "xxx.b2clogin.com"
homeAccountId: "xxx-b2c_1_susi.xxx"

There is no secret in it,

so the third call (before calling my API) will try to call the token endpoint with refresh_token:undefined, and returns an error AADB2C90090 The provided JWE is not a valid 5 segment token

client_id: xx
scope: https://xx.onmicrosoft.com/xx/read https://xx.onmicrosoft.com/xx/write openid profile
grant_type: refresh_token
client_info: 1
client-request-id: xxx
refresh_token: undefined

Then msal makes a fourth call asking for a new authorization_code and the circle is complete:
Each time I'm trying to call the API, I have one fail call to B2C because of the refresh_token undefined and a good one to get a new authorization code.
image

At the end of the day, it's working ^^, but I'm sure there is a room for improvements.

those are extract of the code, maybe someone can help me find out what I'm doing wrong ? (sorry for the messy code)

// Create the main myMSALObj instance
// configuration parameters are located at authConfig.js
import { PublicClientApplication, AuthenticationResult } from '@azure/msal-browser';
import { msalconfig2 } from '../config/config'


class Auth {
    private _token: AuthenticationResult;
    set token(value: AuthenticationResult) {

        this._token = value;
    }
    get token() {
        return this._token;
    }

    //private tokenRequest: AuthorizationUrlRequest;

    async login() {

        try {
            
                await this.myMSALObj.loginRedirect({
                    redirectUri: msalconfig2.config.auth.redirectUri as string,
                    scopes: msalconfig2.loginScopes,
                    extraScopesToConsent: msalconfig2.apiScopes
                })
                console.log("id_token acquired at: " + new Date().toString());
                console.log(this.token);
            

        }
        catch (error) {
            console.log(error);

            // Error handling
            if (error.errorMessage) {
                // Check for forgot password error
                // Learn more about AAD error codes at https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes
                if (error.errorMessage.indexOf("AADB2C90118") > -1) {
                    // this.token = await this.myMSALObj.loginPopup(msalconfig2.policies.authorities.forgotPassword)
                    console.log(this.token);
                    window.alert("Password has been reset successfully. \nPlease sign-in with your new password.");
                }
            }
        }

    }

    // Sign-out the user
    logout() {
        // Removes all sessions, need to call AAD endpoint to do full logout
        this.myMSALObj.logout();
    }

    myMSALObj: PublicClientApplication;

    constructor() {

        this.myMSALObj = new PublicClientApplication(msalconfig2.config);


        var promise = this.myMSALObj.handleRedirectPromise()
        promise.then(resp => {
            this.token = resp;
            if (this.callback) {
                this.callback() // update ui
            }
        });
        promise.catch(err => {
            console.error(err);
        });

    }
    private callback: (user: { name: string; id: string; }) => void;
    attachLoginCallback(callback: (user: { name: string; id: string; }) => void) {
        this.callback = callback;
    }

    private async getTokenPopup() {

        var failSilent = false;
        try {

            failSilent = await this.tryAcquireTokenSilent();
        }
        catch (error) {
            console.log("Silent token acquisition fails. Acquiring token using popup");
            console.log(error);
            failSilent = true;
        }
        if (failSilent) {
            try {

                this.myMSALObj.loginRedirect({
                    redirectUri: msalconfig2.config.auth.redirectUri as string,
                    scopes: msalconfig2.loginScopes,
                    extraScopesToConsent: msalconfig2.apiScopes
                });
                console.log("access_token acquired at: " + new Date().toString());
            }
            catch (error) {
                console.log(error)
            }

        }
        return this.token;
    }

    async addAuthToAPICall(headers: Headers) {
        await this.getTokenPopup();
        const bearer = `Bearer ${this.token.accessToken}`;
        headers.append("Authorization", bearer);
    }


    async tryAcquireTokenSilent() {
        var failSilent = false;
        try {
            var test = this.myMSALObj.getAllAccounts();

            if (test && test.length > 0) {
                var account = test[0];
                this.token = await this.myMSALObj.acquireTokenSilent({
                    account: account,
                    redirectUri: this.myMSALObj.getRedirectUri(),
                    scopes: msalconfig2.apiScopes
                })

                if (!this.token) {
                    failSilent = true;
                }
            }
        }
        catch (e) {
            failSilent = true;
        }
        return failSilent;
    }

    async loadUserIfPossible() {
        return this.tryAcquireTokenSilent();
    }

}

export var auth = new Auth();

basically when application is loading, I'm calling loadUserIfPossible() to retrieve user info if possible.
If not, user click signin button which calls login() method.
After that each api call is going trough the addAuthToAPICall() before calling the api.

import * as Msal from '@azure/msal-browser'

export const isIE = window.navigator.userAgent.indexOf('MSIE ') > -1 || window.navigator.userAgent.indexOf('Trident/') > -1;

export const b2cPolicies = {
    names: {
        signUpSignIn: "b2c_1_susi",
        resetPassword: "b2c_1_reset"
    },
    authorities: {
        signUpSignIn: {
            authority: "https://xx.b2clogin.com/xx.onmicrosoft.com/b2c_1_susi"
        },
        resetPassword: {
            authority: "https://xx.b2clogin.com/xx.onmicrosoft.com/b2c_1_reset"
        }
    }
}

export const apiConfig: { b2cScopes: string[], webApi: string } = {
    b2cScopes: ["https://xx.onmicrosoft.com/xx/read", "https://xx.onmicrosoft.com/xx/write"],
    webApi: 'https://xx.azurewebsites.net'
};


export const msalConfig: Msal.Configuration = {
    auth: {
        clientId: "xx",
        authority: b2cPolicies.authorities.signUpSignIn.authority,
        redirectUri: "https://localhost:44351/",
        navigateToLoginRequestUrl: true,
        knownAuthorities: [
            "xx.b2clogin.com"
        ]
    },
    cache: {
        cacheLocation: "sessionStorage",
        storeAuthStateInCookie: isIE, 
    },
    system: {
        loggerOptions: {
            loggerCallback: (arg1, arg2, arg3) => {
                console.log(arg2);
            },
            logLevel: Msal.LogLevel.Verbose,
            piiLoggingEnabled: true
        }
    }
}

export const loginRequest: { scopes: string[] } = {
    scopes: ['openid', 'profile', 'offline_access']
};


export const tokenRequest: { scopes: string[] } = {
    scopes: apiConfig.b2cScopes // i.e. [https://fabrikamb2c.onmicrosoft.com/helloapi/demo.read]
};




export const msalconfig2 = {
    policies: b2cPolicies,
    config: msalConfig,
    api: apiConfig,
    apiScopes: apiConfig.b2cScopes,
    loginScopes: loginRequest.scopes
}

Also, does anyone know a sample with this scenario ? msal-browser@2.x, spa, B2C and an API which is not graph ? on the sample page https://docs.microsoft.com/en-us/azure/active-directory/develop/sample-v2-code
There is this one
image
which is closed but uses msal@1.x

Examples:

"How do I use MSAL with Vue.js"
"How do I SSO between tabs?"
"How do I use MSAL to protect my custom Web API?"
"How can my app support multiple AAD tenants?"
"When will my scenario be supported?"
"When will this framework be supported"

@Ikaer Ikaer added the question Customer is asking for a clarification, use case or information. label Jul 11, 2020
@jo-arroyo jo-arroyo added msal-browser Related to msal-browser package b2c Related to Azure B2C library-specific issues msal-common Related to msal-common package labels Jul 13, 2020
@jo-arroyo
Copy link
Collaborator

@Ikaer Thanks for bringing this to our attention. We will look into it and follow up.

@Ikaer
Copy link
Author

Ikaer commented Jul 15, 2020

@PraveenVerma17 same problem for me if I remove offline_acces.
I've initially added this scope because the request that fail each time is a grant_type "refresh_token". In order to have a refresh_token from the initial login response, I must add the scope offline_access.

without offline_access on login:
image
with offline_access on login:
image

But even with that, the cache issue prevents msal to send the refresh_token to token endpoint.

I maybe wrong, but I think msal@2.x which uses the authentication code flow by default need this refresh_token
(https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow)

@tnorling
Copy link
Collaborator

@Ikaer To answer your questions:

  1. openid and profile were being added to all requests, but the PRs linked removed these from the refresh flow. The cache lookups should be happening on what you pass to the library.
  2. For now if you are using a B2C scenario you must include offline_access when you request tokens in order to get a refresh token. This is something we will be looking to change in the future but need to work with the service team to get there.

The fix mentioned above will be available in the upcoming GA release of the library. Once that has been released please give it a try and open a new issue if you are still seeing problems.

@Ikaer
Copy link
Author

Ikaer commented Jul 18, 2020

@tnorling thanks for your answers and PRs.

For now if you are using a B2C scenario you must include offline_access when you request tokens in order to get a refresh token. This is something we will be looking to change in the future but need to work with the service team to get there.

Should I add offline_access to the scopes of my api when I'm requesting a token to access it so ?, does the cache will work even its not the same resource as my api scopes ?

Can you confirm me that the normal flow should be:

  1. login to Azure AD B2C with scopes ['openid', 'profile', 'offline_access']
  2. ask silently for an access token to the api with scopes ["https://xx.onmicrosoft.com/xx/read", "https://xx.onmicrosoft.com/xx/write", 'offline_access']. Does the cache will works with this kind of scopes ?

@tnorling
Copy link
Collaborator

@Ikaer You can actually directly request your API scopes in the login call. Unfortunately I don't believe, offline_access will be included in the scopes with the cached access_token so as a workaround for now you should include offline_access in the extraScopesToConsent parameter to make sure the cache lookups succeed and you are also able to get a refresh token when a renewal is needed.

request = {
    scopes: ["https://xx.onmicrosoft.com/xx/read", "https://xx.onmicrosoft.com/xx/write"],
    extraScopesToConsent: ["offline_access"]
}

Apologies for the inconvenience while we work through the kinks in the new library. Let me know if this workaround doesn't solve your problem and I'll take a closer look.

@tnorling
Copy link
Collaborator

@Ikaer Following up as my previous suggestion to use extraScopesToConsent won't work as that is not part of the SilentFlowRequest object. For now please omit offline_access when you would like to retrieve the token from the cache and include it when you need to renew. I understand this is not ideal, but until the service is able to roll out a change for this, it's the only way to take advantage of both the cache and proper token renewal. I've opened a separate ticket to track this specific issue #1999

@Ikaer
Copy link
Author

Ikaer commented Jul 21, 2020

@tnorling thanks for your advices and explanations, I will try that.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 29, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
b2c Related to Azure B2C library-specific issues msal-browser Related to msal-browser package msal-common Related to msal-common package question Customer is asking for a clarification, use case or information.
Projects
None yet
5 participants
@tnorling @pkanher617 @Ikaer @jo-arroyo and others