microsoft-identity-web: [Bug] Calling TokenAcquisition.GetAccessTokenForUserAsync throws a NullReferenceException

Which version of Microsoft Identity Web are you using? 1.16.1

Where is the issue?

  • Web app
    • Sign-in users
    • Sign-in users and call web APIs
  • Web API
    • Protected web APIs (validating tokens)
    • Protected web APIs (validating scopes)
    • Protected web APIs call downstream web APIs
  • Token cache serialization
    • In-memory caches
    • Session caches
    • Distributed caches
  • Other (please describe)

Is this a new or an existing app? The app is in production and I have upgraded to a new version of Microsoft Identity Web. This error started happening after upgrading from 1.15.2 to 1.16.1. I’ve verified this by downgrading the package to 1.15.2 again.

I think maybe this issue is related to #1372 but it has already been closed so I opened a new issue for this.

Repro

public class Startup
{
   public void ConfigureServices(IServiceCollection services)
   {
        services.AddAuthentication()
            .AddMicrosoftIdentityWebApp(azureAdSection, cookieScheme: null)
            .EnableTokenAcquisitionToCallDownstreamApi().AddInMemoryTokenCaches();
   }

    public void Configure(IApplicationBuilder app)
    {
        app.UseAuthentication();
        app.UseAuthorization();
    }
}

public class BaseController : Controller
{
    private readonly SignInManager<User> _signInManager;
    private readonly ITokenAcquisition _tokenAcquisition;
    private readonly IGraphApiClient _graphApiClient;

    public BaseController(
        SignInManager<User> signInManager,        
        ITokenAcquisition tokenAcquisition,
        IGraphApiClient graphApiClient
    )
    {
        _signInManager = signInManager;        
        _tokenAcquisition = tokenAcquisition;
        _graphApiClient = graphApiClient;
    }

    [Route("ExternalLoginCallback")]
    public async Task<IActionResult> ExternalLoginCallback(
        string returnUrl,
        string? remoteError = null
    )
    {
        var info = await _signInManager.GetExternalLoginInfoAsync();

        var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(
            new[] { "User.Read" },
            user: info.Principal
        );
    }
}

Expected behavior I expected ITokenAcquisition.GetAccessTokenForUserAsync() to return an access token like it did in 1.15.2 instead of throwing a NullReferenceException.

Actual behavior The call to ITokenAcquisition.GetAccessTokenForUserAsync() throws the following exception:

System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.Identity.Web.MergedOptions.PrepareAuthorityInstanceForMsal()
   at Microsoft.Identity.Web.TokenAcquisition.BuildConfidentialClientApplication(MergedOptions mergedOptions)
   at Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplication(MergedOptions mergedOptions)
   at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable`1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)
   at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable`1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)
   at TEST.Web.Controllers.BaseController.ExternalLoginCallback(String returnUrl, String remoteError) in D:\Code\DotNet\Test\TEST.Web\Controllers\BaseController.cs:line 159
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.ResponseCaching.ResponseCachingMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 20

Most upvoted comments

Ah sorry, the authentication schemepart was left out. Both schemes are used in the controllers, both lead to the same problem

services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.DefaultScheme = "smart";
                sharedOptions.DefaultChallengeScheme = "smart";
            })
             .AddPolicyScheme("smart", "Authorization Bearer or OIDC", options =>
             {
                 options.ForwardDefaultSelector = context =>
                 {
                     string authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
                     if (authHeader?.StartsWith("Bearer ") == true)
                     {
                         return JwtBearerDefaults.AuthenticationScheme;
                     }
                     return OpenIdConnectDefaults.AuthenticationScheme;
                 };
             });

I solved this problem by implementing custom AuthenticationSchemeProvider according to this thread: https://stackoverflow.com/questions/46464469/how-to-configureservices-authentication-based-on-routes-in-asp-net-core-2-0

After having updated from 1.15.2 to 1.24.1 I now have to pass OpenIdConnectDefaults.AuthenticationScheme as the authenticationScheme parameter to GetAccessTokenForUserAsync for this to work otherwise it throws this new exception the PR added:

System.InvalidOperationException: IDW10503: Cannot determine the cloud Instance. The provided authentication scheme was ''. Microsoft.Identity.Web inferred 'Identity.Application' as the authentication scheme. Available authentication schemes are 'Identity.Application,Identity.External,Identity.TwoFactorRememberMe,Identity.TwoFactorUserId,OpenIdConnect'. See https://aka.ms/id-web/authSchemes. 
   at Microsoft.Identity.Web.TokenAcquisition.GetOptions(String authenticationScheme, String& effectiveAuthenticationScheme)

Is this a required parameter now or is this a bug? The documentation for the parameter says “Authentication scheme. If null, will use OpenIdConnectDefault.AuthenticationScheme if called from a web app…” so I thought it should work without passing a scheme to it? There’s also a spelling mistake there as it should say OpenIdConnectDefaults.AuthenticationScheme and not OpenIdConnectDefault.AuthenticationScheme.

I’m not sure if this matters but I’m not passing OpenIdConnect as the default authentication scheme when I configure authentication because I’m using both cookie and OpenID Connect authentication:

builder.Services.AddAuthentication()
    .AddMicrosoftIdentityWebApp(azureAdSettings, cookieScheme: null)
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

@jennyf19 I’ve written a repro you can run locally here now: https://github.com/davidkarlsson/identity-web-token-issue.