AzureAD / microsoft-authentication-library-for-js

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

AccessToken empty after upgrading to 2.2.0 for ADB2C #2315

Closed dfibuch closed 3 years ago

dfibuch commented 4 years ago

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

Library

Important: Please fill in your exact version number above, e.g. msal@1.1.3.

Framework

Description

After updating to v2.2.0 from v2.1.0 and trying to login using my ADB2C account, the accessToken is not present for either loginRedirect or acquireTokenSilent and I get stuck in a loop of always trying to login.

Using the same code, I have no issues with my ADB2C (Azure AD) app.

Side note: This error is still happening, even though it's been said it should be fixed BrowserAuthError: interaction_in_progress: Interaction is currently in progress. Please ensure that this interaction has been completed before calling an interactive API.

Error Message

image

Security

Regression

MSAL Configuration

image

loginRequest = {
            scopes: ["openid", "profile", "offline_access"]
}

Reproduction steps

Call loginRedirect with your AuthorizationUrlRequest that has scopes on it, sign in with ADB2C account and in your handleRedirectPromise the accessToken is empty.

Expected behavior

Token should be returned as before

Browsers/Environment

tnorling commented 4 years ago

@dfibuch You're not getting an access_token because no access_token was requested, if you need an access_token you should request a scope for a resource you're trying to access (openid and profile will usually only return an idToken).

As for the interaction_in_progress error and login loop, please make sure handleRedirectPromise has resolved before calling loginRedirect or one of the getAccount APIs. If you're certain handleRedirectPromise has finished before calling another msal function and you're still seeing this, please share some code snippets of how you're using msal and/or msal logs so we can help you debug. Thanks!

dfibuch commented 4 years ago

Cheers for the detailed reply @tnorling ! Can you provide me some more details about how I go about requesting the access_token? I take it something around this has changed in this latest version because I didn't have to request it before.

As to the error, I can say for certain that my login code gets called again before the promise has resolved as I can see this happening in console. This is because I automatically call login when the user hits the homepage, then they go to the login screen, get redirected back to the homepage of the app (which calls login again), then the promise resolves and user is logged in. So I think I need to change where I redirect the user after the initial login so that my login code doesn't get called, then when the promise resolves take them to the home page.

tnorling commented 4 years ago

@dfibuch You should get an access_token if you request a scope associated with a resource. openid and profile release information about the user which is usually contained in an id_token. Can you confirm that you were getting an access_token using these same scopes before and can you confirm which version that happened in? I would not expect a code change to change what the server responds with and the behavior you describe does not surprise me so I would be curious to know why you were getting the access_token before.

dfibuch commented 4 years ago

I was (and still am on our UAT system) performing the same request with the same scopes in msal-browser v2.1.0 and I get my access token, though it seems only on acquireTokenSilent (I get the token in both situations with Azure AD, but this is ADB2C)

image

And this is my silentRequest (same one I am making before and after update to 2.2.0):

image

tnorling commented 4 years ago

@dfibuch In this example you are passing a resource scope which I would expect to return an access_token. The example you provided in the original post only contained openid, profile and offline_access which I would expect to only return an id_token. Is the original post incorrect or do you have 2 separate requests?

dfibuch commented 4 years ago

Sorry, I can see the confusion. In the original post I was showing my loginRedirect return which I was expecting to give me the access_token but you correctly pointed out that this doesn't (at least not for ADB2C sign in), and I can confirm this is the same behaviour for 2.1.0.

In my 2nd example you can see that my acquireTokenSilent is returning an access_token (2.1.0) but this was not the case for 2.2.0 doing the same request. If you'd like I can swap to my 2.2.0 branch and run the test again to get screenshots.

Hope this clears it up. And thank you for your continued assistance and patience.

tnorling commented 4 years ago

@dfibuch I think some network logs would be useful here. Could you capture your network traffic when using both 2.1.0 and 2.2.0 and I can take a look to see what might be different? You can send it to the email on my profile.

wasabii commented 4 years ago

I am having the same issue. However, I'm also having it with 2.1.0. acquireTokenSilent with { account: it, scopes: [ 'scopeurl'] } is returning a response with an idToken, but no accessToken. This is happening after the callback from loginRedirect.

Where request is { account: accountobject, scopes: [ 'scopeurl' ] }

            let token = await uaa.acquireTokenSilent(request);
            console.log(token);

Outputs:

accessToken: ""
account: {homeAccountId: "adasdasdasd", environment: "asdasdasd", tenantId: "", username: "sdfsdfsdf", name: "asdasdasd"}
expiresOn: null
extExpiresOn: null
familyId: null
fromCache: false
idToken: "eyJ0eXAiO...."
scopes: []

I should note, acquireTokenPopup actually does seem to work. Returns an object with accessToken and scopes populated.

tnorling commented 4 years ago

@wasabii Can you do some debugging on your end and determine if acquireTokenSilent is returning from cache or making a network call?

If it's making a network call can you determine if: A. The requested scopes are included on the request B. The access token is in the response

If you don't know how to inspect the request/response feel free to capture the network trace and email it to me. Thanks!

wasabii commented 4 years ago

There is a form POST to /oauth2/v2.0/token. The form data contains scope: https://blahblahblah.onmicrosoft.com/blah-dev1-api/Api openid profile for grant_type refresh_token.

The result only has idToken present in it.

The additional openid and profile scopes are probably the issue. These are being added automatically.

dfibuch commented 4 years ago

Sorry @tnorling , I haven't forgotten about this, just been prioritised on other things for time being but I will get back to it if you guys haven't figured it out already.

tnorling commented 4 years ago

@wasabii @dfibuch I'm not able to repro this on my end. Can you double check that your app registration has the redirectUri configured as type "spa", implicit grant settings are disabled and that the scopes are exposed? Can you also share what policy you are using and the identity provider you are trying to login with? Unfortunately without being able to reproduce this and without any logs it's difficult for me to say what the issue may be.

Given that your request contains the required scopes but the response does not contain what we would expect, this may be a server problem. You can file a support ticket on the service by following these instructions

The additional openid and profile scopes are probably the issue. These are being added automatically.

openid and profile are added by the library by default to all requests as the server expects these scopes to be present. This is by design.

wasabii commented 4 years ago

It is registered as a SPA.

Simply changing it to acquireTokenPopup works fine, by the way. I can change it, it works. Change it back, it no longer works. One thing.

tnorling commented 4 years ago

@wasabii That tells me it's related to refresh token redemption, are you passing the offline_access scope? But again it's working as expected with my sample app so it could be an issue with either the policy or the IDP you're using. I would suggest you open a ticket with the service so they can take a look on their end. If you can provide a reproduceable sample I can also bring it up internally.

dfibuch commented 4 years ago

@tnorling My app-registration image

API Permissions image

Using acquireTokenPopup like @wasabii suggested, with the same SilentRequest as always and a single scope for my execute api I get the accessToken

image

When switching to acquireTokenSilent I no longer get my accessToken and also I've noticed the scopes that it returns is now empty and above it has the scope I originally requested

image

Weirdly enough, when I first used the popup method and then changed the code to use silent (without clearing any cookies/session) the silent method returned the accessToken so its almost like whatever code runs before it doesn't set something correctly to be used in the silent method?

EDIT: Just to point out that this whole thing still works in v2.1.0 and gives me my accessToken and scopes when acquireTokenSilent is called with the same silentRequest.

tnorling commented 4 years ago

Weirdly enough, when I first used the popup method and then changed the code to use silent (without clearing any cookies/session) the silent method returned the accessToken so its almost like whatever code runs before it doesn't set something correctly to be used in the silent method?

acquireTokenSilent will first attempt to find the token in the cache, so this is likely why you got the accessToken after calling acquireTokenPopup first. It sounds like the issue happens when exchanging the refreshToken for new tokens, which happens if there is no cached accessToken or if the cached accessToken is expired.

dfibuch commented 4 years ago

Is that an issue with how we've configured something our end or internals of the package?

tnorling commented 4 years ago

@dfibuch It looks to me to be an issue with either the B2C policy you're using or the downstream IDP you're logging in with. I may be able to confirm with network logs and if you can confirm for me your scenario or provide a reproduction (preferably using one of our samples) that may help me investigate. I've tried running our own b2c sample against the latest version and have not been able to reproduce this behavior so it's hard to say with certainty at this point what the root cause is.

dfibuch commented 4 years ago

@tnorling I'll try to get some time to look at this again tomorrow (seems unfortunate that due to our timezones I finish when you start 😂). Unless @wasabii is able to provide some of this info as well.

wasabii commented 4 years ago

I did find that I had those implicit settings enabled. Disabling them did fix it in 2.1. So, I'm trying to get 2.2 working to properly align with the original bug.

However now I'm struggling with a different issue where popup is actually presenting a login screen in a popup. When I figure that guy out, I'll let y'all know where it stands.

wasabii commented 4 years ago

Okay. Cleared that up. So, on 2.2, I'm getting an empty accessToken. Call to acquireTokenSilent happens pretty closely after the return to the site after login. But yeah, empty accessToken in 2.2.

barrymarc commented 4 years ago

I'm also getting an empty accessToken after upgrading to version 2.2. I tried switching the msal.interceptor.ts line 32 to 'return this.authService.acquireTokenPopup' and this returns an accessToken. I'm currently using the Angular10-Browser-Sample code, but changed the config to use my B2C server and access my API. I changed the B2C client application registration to match the setting listed in https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#configure-platform-settings and https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#redirect-uri-setup-required-for-single-page-apps.

Let me know what I can share to help debug this?

tnorling commented 4 years ago

A couple useful things that might help us out:

  1. What B2C policy are you using? One of the built in flows or a custom policy?
  2. What Identity Provider are you using? Is it B2C local login or a Social Login (Google, Facebook, etc)?
  3. Network logs can be emailed to the e-mail address listed on my profile
  4. A reproduction of the issue - either code I can take a look at (preferably using one of our samples) or a live demo

Thanks!

wasabii commented 4 years ago
tnorling commented 4 years ago

Ok I was finally able to repro this behavior on my end:

  1. Login request does not contain scopes, MSAL makes an auth Code request with defaults openid, profile, receive idToken and refreshToken in the response.
  2. Call acquireTokenSilent with your resource scope, MSAL attempts to exchange refreshToken from step 1 for an accessToken with the requested scope + openid and profile. No accessToken returned in the response.

A workaround for now would be to include your scope in the initial loginRedirect or loginPopup call or to call acquireTokenRedirect or acquireTokenPopup before calling acquireTokenSilent. I'll work with the server team to understand why this is happening.

wasabii commented 4 years ago

Ahh. I explicitly do not include my scopes there, as I need scopes from two different resources. And including both breaks it. Not supported or something.

tnorling commented 4 years ago

Yes, you do need to make 2 separate requests for 2 separate resources. Are you still getting this behavior if you include the 2nd resource set in extraScopesToConsent?

For example:

msal.loginPopup({
scopes: ["scope_resource1"],
extraScopesToConsent:["scope_resource2"]
});

msal.acquireTokenSilent({scopes: ["scope_resource1"]})
msal.acquireTokenSilent({scopes: ["scope_resource2"]})
barrymarc commented 4 years ago

@tnorling I sent you an email, but here are the answers to the troubleshooting tips you listed above.

  1. Tried with our custom policy and default B2C_1A_signup_signin.
  2. B2C local account.
  3. Attached to the email.
  4. Repo listed in the email.
LarsKemmann commented 4 years ago

@tnorling Do you have any updates from the server team, and any idea of timing for a fix? We can go forward with the suggested workaround for now during development (we've confirmed that it works), but that will not work in production (redirects tear down in-progress user work, popups don't work with Safari users). Thanks!

wasabii commented 4 years ago

@tnorling I gave it a go. Not supported:

AADB2C90146:+The+scope+'https://xxx/analyticssimulator-dev1-web/Web+https://xxx/analyticssimulator-dev1-api/Api+openid+profile'+provided+in+request+specifies+more+than+one+resource+for+an+access+token,+which+is+not+supported.

tnorling commented 4 years ago

Update: When exchanging a refresh token for a new access token the server will only respond with an access token for the scopes that were requested when obtaining that refresh token. This was by design but we are going to work with the server team to see if this behavior can be changed. Full disclosure: it will probably take some time to decide on and implement a fix.

The reason it appeared to work in a previous version was because acquireTokenSilent falls back to ssoSilent when the refresh fails and ssoSilent goes through the full flow of getting and exchanging an Auth Code for new tokens (this is what was succeeding, not the exchange of the refresh token). When we updated the refresh flow to include scopes openid and profile, the refresh succeeded with a refreshed id_token and silently rejected the access token scope since it was not included in the initial request.

So now that we understand the behavior, this is the workaround until we have a more concrete solution for B2C: Call loginRedirect or loginPopup with your first set of scopes. acquireTokenSilent should succeed until you need a different set of scopes. When you need a 2nd set of scopes you can call ssoSilent in environments were 3rd party cookies are not blocked (i.e. not Safari or Chrome Incognito browsers) or acquireTokenRedirect/acquireTokenPopup if calling ssoSilent is not possible. Afterwards acquireTokenSilent should succeed for the next hour for both sets of scopes, as they will be cached. Unfortunately, once a token expires you will likely experience this problem again and you'll need to call one of the Auth Code APIs again.

I know this isn't an ideal solution but I hope it will unblock you for the time being.

dfibuch commented 4 years ago

Cheers for the update @tnorling , I'm glad I wasn't actually going mad when I couldn't get it to work. Is there any harm staying on v2.1.0 of the library until a fix is implemented? Are there any critical changes that I should be using in v2.2.0/2.3.0?

tnorling commented 4 years ago

@dfibuch No critical updates but if you do run into any other issues upgrading to the latest version is always a great first step. Also I would like to remind everyone that B2C integration with MSAL.js version 2 is not production ready and is only recommended for development environments at this time.

SBajonczak commented 4 years ago

Update: When exchanging a refresh token for a new access token the server will only respond with an access token for the scopes that were requested when obtaining that refresh token. This was by design but we are going to work with the server team to see if this behavior can be changed. Full disclosure: it will probably take some time to decide on and implement a fix.

The reason it appeared to work in a previous version was because acquireTokenSilent falls back to ssoSilent when the refresh fails and ssoSilent goes through the full flow of getting and exchanging an Auth Code for new tokens (this is what was succeeding, not the exchange of the refresh token). When we updated the refresh flow to include scopes openid and profile, the refresh succeeded with a refreshed id_token and silently rejected the access token scope since it was not included in the initial request.

So now that we understand the behavior, this is the workaround until we have a more concrete solution for B2C: Call loginRedirect or loginPopup with your first set of scopes. acquireTokenSilent should succeed until you need a different set of scopes. When you need a 2nd set of scopes you can call ssoSilent in environments were 3rd party cookies are not blocked (i.e. not Safari or Chrome Incognito browsers) or acquireTokenRedirect/acquireTokenPopup if calling ssoSilent is not possible. Afterwards acquireTokenSilent should succeed for the next hour for both sets of scopes, as they will be cached. Unfortunately, once a token expires you will likely experience this problem again and you'll need to call one of the Auth Code APIs again.

I know this isn't an ideal solution but I hope it will unblock you for the time being.

Hey many thx for the description of the Problem and the given workaround.

So I understand that I am responsible for now to refresh the token after it is expired at the moment?

tnorling commented 4 years ago

@SBajonczak Yes, I would suggest doing something like this for now:

// Initial acquisition of scopes 1 and 2
await msal.loginPopup({scopes: ["scope1"]});
const account = msal.getAllAccounts()[0];
await msal.ssoSilent({
    scopes: ["scope2"],
    loginHint: account.username
});

// Subsequent token acquisition with fallback
msal.acquireTokenSilent({
    scopes: ["scope1"],
    account: account
}).then((response) => {
    if (!response.accessToken) {
        return msal.ssoSilent({
            scopes: ["scope1"],
            loginHint: account.username
        });
    } else {
        return response;
    }
});

Replace ssoSilent with acquireTokenRedirect or acquireTokenPopup if you need to support browsers that block 3rd party cookies (i.e. Safari)

dfibuch commented 4 years ago

@tnorling I've just tried your workaround above and confirm it works, but you already knew that :) I've updated to v2.5.1 to get all the latest bug fixes.

Now I just need this to work with acquireTokenRedirect.

SBajonczak commented 4 years ago

Now I just need this to work with acquireTokenRedirect.

That will be awesome 😊👍

dfibuch commented 4 years ago

Now I just need this to work with acquireTokenRedirect.

That will be awesome 😊👍

I will try it today and update here. My initial problem was I couldn't get acquireTokenRedirect to work at all in my app, but I've worked it out now and got it to work, so hopefully this will be trivial.

dfibuch commented 4 years ago

Now I just need this to work with acquireTokenRedirect.

That will be awesome 😊👍

So it turns out it's actually pretty easy to do. I did have to change my code slightly to either return the token from acquireTokenSilent or nothing, then check what I got and call acquireTokenRedirect if I didn't get a token,

        const silentRequest: SilentRequest = {
            account: storedAccount,
            scopes: [myApiResourceScope],

        };

        const token = await this.msalApp.acquireTokenSilent(silentRequest)
            .then(response => {
                if (response.accessToken) {
                    return response;
                }
            })
            // Handling for silent call failing
            .catch(error => {
                const authError = error as AuthError;
                if (error instanceof InteractionRequiredAuthError
                    || authError.errorMessage.includes("AADB2C90077")) {
                    // fallback to interaction when silent call fails
                    return this.msalApp.acquireTokenRedirect(silentRequest);
                }
            });

        if (!token) {
            return this.msalApp.acquireTokenRedirect(silentRequest);
        }

        return token;

Not sure if I am doing it right, but using a SilentRequest for both acquireTokenSilent and acquireTokenRedirect seems to work fine.

Then in my handleRedirectPromise I call my acquireToken code again which first tries to do a acquireTokenSilent (then falls back to the code above) but this time it can get one because I've already called the redirect code that ensures the token is fetched properly.

Jernik commented 3 years ago

I would like to remind everyone that B2C integration with MSAL.js version 2 is not production ready and is only recommended for development environments at this time.

Hi @tnorling where is this documented? The FAQ for msal-browser specifically calls out Azure B2C as a service that it supports with no caveats. This issue (on page 3 of the issues list) and many posts down is the first mention of that I have seen.

Is there a "Support B2C in MSAL.js 2.x" issue somewhere that is tracking blockers like this one for using B2C with MSAL.js 2.0? I've been banging my head against this exact issue for over a week now, and trawling through every issue that seems tangentially related to find this important information does not seem to be the clearest way to communicate this workaround! I just don't want anyone else to get stuck in the same rabbit hole I did, or myself to get stuck in a similar rabbit hole for any other issues requiring a workaround

EDIT: Sorry! After looking at the history, your comment was posted a week before that line was added to the FAQ. My question about B2C issues still stands though, is this the only weird place that needs a workaround, or are there any other gotchas I should be aware of? Additionally, I feel like this issue should be pinned since it seems to be a big roadblock if you don't know the implementation details of the library.

tnorling commented 3 years ago

@Jernik Sorry to hear this has been a source of frustration for you. This particular error and workaround are documented in the FAQ, which is also where other known issues are communicated. If you run into any issues that are not documented there please do open an issue and someone on the team will investigate and update our docs if necessary.

Jernik commented 3 years ago

@tnorling ah that makes sense, in the past you had pinned issues when workarounds like this were needed, so that's where I had looked. My 2 cents on the matter of the api design is that MSAL should trigger the authorization code exchange if it doesn't get an access token back with the scopes that were requested. I don't know if that makes sense from how it's implemented, but from a consumer of the library's perspective, when I call "aquireToken", I want it to give me a token, not "maybe it gives you back a token". Either way, thanks for your work on this library! It's made interacting with Azure identity platform a lot easier and you and your team have been super responsive and helpful!

tnorling commented 3 years ago

@Jernik

from a consumer of the library's perspective, when I call "aquireToken", I want it to give me a token, not "maybe it gives you back a token"

We agree the current behavior is unexpected and the B2C service team is working towards solving this in a way that will give you the behavior you describe. Unfortunately I don't have an estimate as to when that work will be completed as it is a larger undertaking than just a simple bug fix.

My 2 cents on the matter of the api design is that MSAL should trigger the authorization code exchange if it doesn't get an access token back with the scopes that were requested

MSAL doesn't make any assumptions about how you want to do the auth code exchange (popup, redirect or ssoSilent) which is why we leave this up to the application developer to handle.

Jernik commented 3 years ago

MSAL doesn't make any assumptions about how you want to do the auth code exchange (popup, redirect or ssoSilent) which is why we leave this up to the application developer to handle.

That makes sense, is there any plan for an AuthorizationCodeExchangeRequired exception the same way there is an InteractionRequired exception to communicate this to consuming application? Or is checking for the access_code to be on the response the long-term way for it to be controlled?

tnorling commented 3 years ago

No, the long-term plan is to "fix" the current behavior so that you don't get into a situation where the service is silently ignoring your request. Checking for the accessToken is merely a workaround and we don't expect it to be necessary long term. You can spin up an app registration on an AAD (non-B2C) tenant to see how this should work on B2C whenever the work is completed.

tnorling commented 3 years ago

@dfibuch Interactive calls such as redirect and popup should always succeed (provided you've configured everything correctly), if you do find that these are failing please open a new issue as we should understand what's going on there. acquireTokenSilent is the only API affected by the behavior being discussed in this thread.

dfibuch commented 3 years ago

Ye sorry @tnorling I deleted my comment as soon as I realised my mistake. I've been off for a week and came back to "login seems to be playing up again" so started looking into it and got confused. Ignore me 🤦‍♂️

MarekLani commented 3 years ago

hi @tnorling I did use suggested workaround, but it got me into another issue. I am using msal-react in addition, not sure if it makes difference. Nevertheless if I add scope to loginPopup, the login process hangs in InProgress state

tnorling commented 3 years ago

@MarekLani Can you please open a new issue and provide steps to reproduce?

MarekLani commented 3 years ago

@tnorling please disregard my comment, it was related to issue of re-renders invoked in react code.