AzureAD / microsoft-authentication-library-for-dotnet

Microsoft Authentication Library (MSAL) for .NET
https://aka.ms/msal-net
MIT License
1.36k stars 334 forks source link

MSAL 3 against B2C throws MsalServiceException AADSTS50049 #1165

Closed devdeer-stephan closed 5 years ago

devdeer-stephan commented 5 years ago

Which Version of MSAL are you using ?

Platform

What authentication flow has the issue?

Is this a new or existing app? The app is in production with MSAL 2.7.0, and I have upgraded to MSAL 3.0.8 on my dev-branch

Repro Inside my implementation of IConfigureNamedOptions<OpenIdConnectOptions>:

/// <inheritdoc />
public void Configure(string name, OpenIdConnectOptions options)
{
    options.ClientId = _azureAdB2COptions.ClientId;
    options.Authority = _azureAdB2COptions.Authority;
    options.UseTokenLifetime = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,
        NameClaimType = "name"
    };
    // hook to OpenId events
    options.Events = new OpenIdConnectEvents
    {
        OnRedirectToIdentityProvider = OnRedirectToIdentityProviderAsync,
        OnRemoteFailure = OnRemoteFailureAsync,
        OnAuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
    };
}

/// <summary>
/// Is called whenever B2C retrieves a new auth token.
/// </summary>
/// <param name="context">The context of the received authentication, including the code.</param>
public async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedContext context)
{
    var clientApplication = ConfidentialClientApplicationBuilder
        .Create(_azureAdB2COptions.ClientId)
        .WithAuthority(_azureAdB2COptions.Authority, false)
        .WithRedirectUri(_azureAdB2COptions.RedirectUri)
        .WithClientSecret(_azureAdB2COptions.ClientSecret)
        .Build();
    // try to retrieve the bearer token
    try
    {
        var result = await clientApplication.AcquireTokenByAuthorizationCode(_azureAdB2COptions.ApiScopes.Split(' '), context.ProtocolMessage.Code).ExecuteAsync();
        context.HandleCodeRedemption(result.AccessToken, result.IdToken);
    }
    catch (Exception ex)
    {
        Trace.TraceError(ex.Message);
        throw;
    }
}

Expected behavior When calling clientApplication.AcquireTokenByAuthorizationCode I want to receive a authentication token.

Actual behavior Calling clientApplication.AcquireTokenByAuthorizationCode thows an MsalServiceException with the message "AADSTS50049: Unknown or invalid instance.\r\nTrace ID: 9d5948e6-486f-4ea8-b28c-21af7b681b00\r\nCorrelation ID: 3be8e81f-9bc2-45c9-bbbd-062a71a99b57\r\nTimestamp: 2019-05-21 14:48:15Z"

Possible Solution From what I've researched so far this issue used to be resolved by disabling the authority validation. As far as I can see I did just that. Did I miss something?

jmprieur commented 5 years ago

@devdeer-stephan

devdeer-stephan commented 5 years ago

@jmprieur Thank you, this helped and I feel just a little stupid for not noticing that myself.

Now, if I call var accounts = await clientApplication.GetAccountsAsync() the accounts are always empty, so I cannot use the accounts for a subsequent var token = await clientApplication.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync() Am I missing some scope(s)?

jmprieur commented 5 years ago

no worries, @devdeer-stephan ;) Which token cache implementation are you using? I recommend you look at the sample I mentioned; this line is important https://github.com/Azure-Samples/active-directory-b2c-dotnetcore-webapp/blob/0275b6e2f745ad9fda2958fc5461248702cf41b6/WebApp-OpenIDConnect-DotNet/OpenIdConnectOptionsSetup.cs#L115

Finally even if the following article is not especially for B2C, the principles remain the same: Web app that calls web APIs - code configuration

You might want to read the article: Scenario: Web app that calls web APIs

devdeer-stephan commented 5 years ago

@jmprieur We had some issues with re-deployments where the user's cookie and the deleted in-memory-chache (from the re-deoployment) differed, so we chose to go for a redis cache:

// Get the token cache from redis, if the internal in-memory cache is not yet populated
clientApplication.UserTokenCache.SetBeforeAccess(
    tokenArgs =>
    {
        var msalCacheContent = tokenArgs.TokenCache.SerializeMsalV3();
        if (msalCacheContent != null)
        {
            // no action required if in-memory cache is already existent
            return;
        }
        // get the cache content from redis
        var redisCacheContent = _redisUserCache.Read(tokenArgs.Account.Username);
        if (redisCacheContent.HasValue)
        {
            // load the redis contents to the local in-memory cache
            tokenArgs.TokenCache.DeserializeMsalV3(redisCacheContent);
        }
    });
// write every change in the internal token cache back to the redis token cache
clientApplication.UserTokenCache.SetAfterAccess(
    tokenArgs =>
    {
        if (!tokenArgs.HasStateChanged)
        {
            // no action required if no change has occured
            return;
        }
        // get the local in-memory cache content and write it to redis
        var msalCacheContent = tokenArgs.TokenCache.SerializeMsalV3();
        _redisUserCache.Write(tokenArgs.Account.Username, msalCacheContent);
    });
devdeer-stephan commented 5 years ago

@jmprieur I just realized that tokenArgs.Account.Username resolves to Missing from the token response so the cache won't be able to handle that in any meaningful way. I switched that to tokenArgs.Account.HomeAccountId.ObjectId and the resulting keys in the redis cache look a lot better. Still, the problem with the empty accounts persists and I assume that I simply don't get that information in the token from the B2C. Is there some setting in the B2C I need to activate?

jmprieur commented 5 years ago

@devdeer-stephan : yes tokenArgs.Account.HomeAccountId.ObjectId is a good key. Or otherwise tokenArgs.Account.Identifier which concatenates the tenant Id and the object id.

jmprieur commented 5 years ago

@devdeer-stephan : BTW thanks for sharing the code to serialize to a Redis token cc: @kalyankrishna1 @MarkZuber

Are you happy that your issue is solved? do you want to close this issue?

devdeer-stephan commented 5 years ago

@jmprieur I think I might still have some caching problem. It seems var msalCacheContent = tokenArgs.TokenCache.SerializeMsalV3(); will never result in null, as even a freshly initialized cache already had 17 bytes of content. I might need to enforce deserializing the redis cache at least once initially. I will report back shortly.

jennyf19 commented 5 years ago

@devdeer-stephan any update? are we able to close this issue? thanks.

jmprieur commented 5 years ago

@devdeer-stephan : the right way to handle the cache is to subscribe to the serialization events. Please see this PR about what's right to do: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/pull/106

proposing to close this issue. Don't hesitate to reopen (or open a more explicit issue) if you disagree