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

MSAL B2C token refresh flow - multiple APIs #139

Open
Kasenga opened this issue Sep 11, 2020 · 8 comments
Open

MSAL B2C token refresh flow - multiple APIs #139

Kasenga opened this issue Sep 11, 2020 · 8 comments
Assignees

Comments

@Kasenga
Copy link

Kasenga commented Sep 11, 2020

Hello team!
This is a great sample, easy to understand and works well obtaining a token from B2C and calling an API.
There is a known limitation with B2C in the sense that the token refresh flow does not allow obtain tokens for a second, different API.
Doing so yields an id token, refresh token and a null access token!!
To obtain a second access token, AcquireTokenInteractive will need to be called; which may require a users to enter credentials (not an ideal user experience)

Additionally, this means, calling AcquireTokenSilent() and sending a second set of scopes runs … but does not returned an access token, nor does it generate any errors, or throw exceptions.
The error is surfaced later when a call to an API is make, failing with an invalidTokenError.

In my attempt to use MSAL to workaround this, I modified AcquireTokenSilent by adding a scope parameter; I also checked to see if the access token was null; if it was, I then called AcquireTokenInteractive.

    public async Task<UserContext> AcquireTokenSilent(string[] scopes)
    {
        IEnumerable<IAccount> accounts = await _pca.GetAccountsAsync();
        AuthenticationResult authResult = await _pca.AcquireTokenSilent(scopes, GetAccountByPolicy(accounts, B2CConstants.PolicySignUpSignIn))
           .WithB2CAuthority(B2CConstants.AuthoritySignInSignUp)
           .ExecuteAsync();

        if (authResult.AccessToken == null)
        {
            //acquire token interactive ...
            authResult = await _pca.AcquireTokenInteractive(scopes)
            .WithPrompt(Prompt.NoPrompt)
            .WithAuthority(B2CConstants.AuthoritySignInSignUp)
            .ExecuteAsync();
        }
        var newContext = UpdateUserInfo(authResult);

        return newContext;
    }

This “gets the job done” in the sense that a second access token is obtained, but briefly pops up a blank screen.

The ask:
Can this sample be modified to take into account this B2C limitation? Either:

  1. Throw an exception when a null token is returned
  2. Once detected, could a different request be made, such as AcquireTokenInteractive?
  3. Can anything be done to make this seamless, so that there are no prompts?

Thank you

@henrik-me
Copy link

@Kasenga : we are tracking this work as part of our MSAL releases currently scheduled for 4.20. There is currently no ETA.

@bgavrilMS bgavrilMS added the P2 label Nov 5, 2020
@jennyf19 jennyf19 self-assigned this Dec 12, 2020
@GeorgeLeithead
Copy link

Hello team!
This is a great sample, easy to understand and works well obtaining a token from B2C and calling an API.
There is a known limitation with B2C in the sense that the token refresh flow does not allow obtain tokens for a second, different API.
Doing so yields an id token, refresh token and a null access token!!
To obtain a second access token, AcquireTokenInteractive will need to be called; which may require a users to enter credentials (not an ideal user experience)

Additionally, this means, calling AcquireTokenSilent() and sending a second set of scopes runs … but does not returned an access token, nor does it generate any errors, or throw exceptions.
The error is surfaced later when a call to an API is make, failing with an invalidTokenError.

In my attempt to use MSAL to workaround this, I modified AcquireTokenSilent by adding a scope parameter; I also checked to see if the access token was null; if it was, I then called AcquireTokenInteractive.

    public async Task<UserContext> AcquireTokenSilent(string[] scopes)
    {
        IEnumerable<IAccount> accounts = await _pca.GetAccountsAsync();
        AuthenticationResult authResult = await _pca.AcquireTokenSilent(scopes, GetAccountByPolicy(accounts, B2CConstants.PolicySignUpSignIn))
           .WithB2CAuthority(B2CConstants.AuthoritySignInSignUp)
           .ExecuteAsync();

        if (authResult.AccessToken == null)
        {
            //acquire token interactive ...
            authResult = await _pca.AcquireTokenInteractive(scopes)
            .WithPrompt(Prompt.NoPrompt)
            .WithAuthority(B2CConstants.AuthoritySignInSignUp)
            .ExecuteAsync();
        }
        var newContext = UpdateUserInfo(authResult);

        return newContext;
    }

This “gets the job done” in the sense that a second access token is obtained, but briefly pops up a blank screen.

The ask:
Can this sample be modified to take into account this B2C limitation? Either:

  1. Throw an exception when a null token is returned
  2. Once detected, could a different request be made, such as AcquireTokenInteractive?
  3. Can anything be done to make this seamless, so that there are no prompts?

Thank you

What second set of "scopes" did you pass in? (Also, can you confirm the first, as I too am having the NULL AccessToken problem, and need to call another API [also secured by the same B2C]).

@Kasenga
Copy link
Author

Kasenga commented Dec 31, 2020

.
.
.

"What second set of "scopes" did you pass in? (Also, can you confirm the first, as I too am having the NULL AccessToken problem, and need to call another API [also secured by the same B2C])."

Hey George!
I was using MsGraph and a custom API.
The first access token for MsGraph worked just fine - I was seeing an issue when attempting to obtain a second access token for my custom API.
Turns out this results in a null token if you call acquireTokenSilent.
To get the second token, you will need to call acquireTokenInteractive.

@henrik-me
Copy link

@jennyf19 @jmprieur : I think we should explore with the B2C team if we can catch the error and throw an interaction required exception, ensuring customers can use our regular call patterns. From my understanding this happens when the scope to the refresh token flow is not the same as originally used. Thoughts?

@jmprieur
Copy link
Contributor

@henrik-me : Would we want to change MSAL.NET so that a UIRequiredException is thrown in the B2C case, when no access token is returned?

@Kasenga
Copy link
Author

Kasenga commented Jan 25, 2021

@henrik-me : Would we want to change MSAL.NET so that a UIRequiredException is thrown in the B2C case, when no access token is returned?

Yes, @jmprieur, this would be ideal!!

@jmprieur
Copy link
Contributor

I think a better idea would be for the service to return the exception (to do like AAD?)
Cc: @nickgmicrosoft

@Rabosa616
Copy link

Hi all,
I've had the same problem I have and application that needs to call 3 apis.
1st call I do it like this:

try
{
    IEnumerable<IAccount> accounts = await _pca.GetAccountsAsync();
    AuthenticationResult authResult = await _pca.AcquireTokenSilent(B2CConstants.ScopesApi1, GetAccountByPolicy(accounts, B2CConstants.PolicySignUpSignIn))
						.WithB2CAuthority(B2CConstants.AuthoritySignInSignUp)
						.ExecuteAsync();

    var newContext = UpdateUserInfo(authResult);
    return newContext;
}
catch (MsalUiRequiredException)
{
    // acquire token interactive
    AuthenticationResult authResult = await _pca.AcquireTokenInteractive(B2CConstants.ScopesApi1)
						.WithPrompt(Prompt.SelectAccount) //This for 1st call
						.ExecuteAsync();

    var newContext = UpdateUserInfo(authResult);
    return newContext;
}

then the other 2 times I do the same but in the interactive part instead of calling it with .WithPrompt(Prompt.SelectAccount) I call it with .WithPrompt(Prompt.Consent) like this:

try
{
    IEnumerable<IAccount> accounts = await _pca.GetAccountsAsync();
    AuthenticationResult authResult = await _pca.AcquireTokenSilent(B2CConstants.ScopesApi2, GetAccountByPolicy(accounts, B2CConstants.PolicySignUpSignIn))
						.WithB2CAuthority(B2CConstants.AuthoritySignInSignUp)
						.ExecuteAsync();

    var newContext = UpdateUserInfo(authResult);
    return newContext;
}
catch (MsalUiRequiredException)
{
    // acquire token interactive
    AuthenticationResult authResult = await _pca.AcquireTokenInteractive(B2CConstants.ScopesApi2)
						.WithPrompt(Prompt.Consent) //This for the 2nd and 3rd call
						.ExecuteAsync();

    var newContext = UpdateUserInfo(authResult);
    return newContext;
}

The result in the UI is:
1st call user enter credentials
2nd and 3rd call user does not do anything but the app open the credential site and close it without any user interaction

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants