Microsoft-identity-web: [Bug] Blazor server does not handle the MsalUiRequiredException

Created on 23 Jul 2020  路  10Comments  路  Source: AzureAD/microsoft-identity-web

When calling Blazor server for the acquire token silent (or OBO) part, or when there is a need for more scopes or conditional access, Blazor does not handle context.Result = new ChallengeResult(properties); in AuthorizeForScopesAttribute, like in MVC or Razor.

Blazor bug fixed web app

Most helpful comment

@jennyf19

Case where a cookie exists, but the token does not exist in cache and an attempt to acquire a new token (running locally).

in _Host.cshtml:
```c#
@model _HostAuthPageModel

method in _HostAuthPageModel.cs:
```c#
public async Task<IActionResult> OnGet()
{
    if (!User.Identity.IsAuthenticated)
        return Challenge();

    try
    {
        AccessToken = await tokenAcquisition.GetAccessTokenForUserAsync(apiOptions.Value.Scopes);
    }
    catch (MicrosoftIdentityWebChallengeUserException ex)
    {
        //can't get a token from the token store, MUST assume a sign-out path as requests to API will NOT be authenticated
        logger.LogError(ex, ex.Message);
        consentHandler.HandleException(ex);
    }

    return Page();
}

So, at initial page load of the application the OnGet() method is executed, the user is authenticated (due to the cookie), then attempting to acquire an access token throws an MicrosoftIdentityWebChallengeUserException exception (as expected). When the consentHandler.HandleException(ex) executes, the following exception occurs:

System.ArgumentNullException: Value cannot be null. (Parameter 'claimsPrincipal')
   at Microsoft.Identity.Web.ClaimsPrincipalExtensions.GetDisplayName(ClaimsPrincipal claimsPrincipal) in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\ClaimsPrincipalExtensions.cs:line 128
   at Microsoft.Identity.Web.ClaimsPrincipalExtensions.GetLoginHint(ClaimsPrincipal claimsPrincipal) in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\ClaimsPrincipalExtensions.cs:line 90
   at Microsoft.Identity.Web.IncrementalConsentAndConditionalAccessHelper.BuildAuthenticationProperties(String[] scopes, MsalUiRequiredException ex, ClaimsPrincipal user) in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\IncrementalConsentAndConditionalAccessHelper.cs:line 74
   at Microsoft.Identity.Web.MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException(Exception exception) in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\MicrosoftIdentityCircuitHandler.cs:line 85
   at OBFUSCATED.Web.App.Pages._HostAuthPageModel.OnGet() in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Pages\_HostAuthPageModel.cs:line 55
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Convert[T](Object taskAsObject)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Execute(Object receiver, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()
   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()
--- End of stack trace from previous location where exception was thrown ---
   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.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

All 10 comments

@gwgrubbs @schmid37
Do you mind trying this branch with the following code changes in the CallWebApi.razor page:
First you'll need to inject the following services and a using statement:

@using Microsoft.Identity.Web
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager

and then in OnInitializedAsync() add the following code:

protected override async Task OnInitializedAsync()
{
        try
        {
            apiResult = await downstreamAPI.CallWebApiAsync();
        }
        catch (MicrosoftIdentityWebChallengeUserException ex)
        {
            await BlazorHelper.ChallengeUserAsync(
                ex,
                AuthenticationStateProvider,
                NavigationManager,
                GetType().Name).ConfigureAwait(false);
        }
}

Here's an example.

We might take this as a "quick fix", with a better long term solution. Let us know if this works for you. Thanks.

cc: @jmprieur

@jennyf19 What I did:

protected override async Task OnInitializedAsync()
{
    try
    {
        retVal = await AadService.GetAsync();
    }
    catch (MicrosoftIdentityWebChallengeUserException ex)
    {
        await BlazorHelper.ChallengeUserAsync(
            ex,
            AuthenticationStateProvider,
            NavigationManager,
            GetType().Name).ConfigureAwait(false);
    }
}

Exception:

blazor.server.js:19 [2020-07-24T12:48:31.731Z] Error: System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.Identity.Web.HttpContextExtensions.GetTokenUsedToCallWebAPI(HttpContext httpContext) in C:\src\Microsoft.Identity.Web\HttpContextExtensions.cs:line 29
at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable`1 scopes, String tenant, String userFlow,

thanks @schmid37 @gwgrubbs
if you have time, mind trying this branch and doing the following in the CallWebApi page:

@inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler;
...
@code {
    private string apiResult;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            apiResult = await downstreamAPI.CallWebApiAsync();
        }
        catch (Exception ex)
        {
            ConsentHandler.HandleException(ex);
        }
    }

And then in Startup.cs:

services.AddServerSideBlazor()
             .AddMicrosoftIdentityConsentHandler();

Appreciate your patience as we sort this out.

@jennyf19

First login works.
Restart server, calling the function, exception is:

Error: System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.Identity.Web.MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException(Exception ex) in C:\src\Microsoft.Identity.Web\MicrosoftIdentityCircuitHandler.cs:line 92

--> Delete cookies, and it works again.

@jennyf19 so what I want to say, after a server restart, it never works.

@jennyf19

Case where a cookie exists, but the token does not exist in cache and an attempt to acquire a new token (running locally).

in _Host.cshtml:
```c#
@model _HostAuthPageModel

method in _HostAuthPageModel.cs:
```c#
public async Task<IActionResult> OnGet()
{
    if (!User.Identity.IsAuthenticated)
        return Challenge();

    try
    {
        AccessToken = await tokenAcquisition.GetAccessTokenForUserAsync(apiOptions.Value.Scopes);
    }
    catch (MicrosoftIdentityWebChallengeUserException ex)
    {
        //can't get a token from the token store, MUST assume a sign-out path as requests to API will NOT be authenticated
        logger.LogError(ex, ex.Message);
        consentHandler.HandleException(ex);
    }

    return Page();
}

So, at initial page load of the application the OnGet() method is executed, the user is authenticated (due to the cookie), then attempting to acquire an access token throws an MicrosoftIdentityWebChallengeUserException exception (as expected). When the consentHandler.HandleException(ex) executes, the following exception occurs:

System.ArgumentNullException: Value cannot be null. (Parameter 'claimsPrincipal')
   at Microsoft.Identity.Web.ClaimsPrincipalExtensions.GetDisplayName(ClaimsPrincipal claimsPrincipal) in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\ClaimsPrincipalExtensions.cs:line 128
   at Microsoft.Identity.Web.ClaimsPrincipalExtensions.GetLoginHint(ClaimsPrincipal claimsPrincipal) in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\ClaimsPrincipalExtensions.cs:line 90
   at Microsoft.Identity.Web.IncrementalConsentAndConditionalAccessHelper.BuildAuthenticationProperties(String[] scopes, MsalUiRequiredException ex, ClaimsPrincipal user) in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\IncrementalConsentAndConditionalAccessHelper.cs:line 74
   at Microsoft.Identity.Web.MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException(Exception exception) in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\MicrosoftIdentityCircuitHandler.cs:line 85
   at OBFUSCATED.Web.App.Pages._HostAuthPageModel.OnGet() in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Pages\_HostAuthPageModel.cs:line 55
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Convert[T](Object taskAsObject)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Execute(Object receiver, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()
   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()
--- End of stack trace from previous location where exception was thrown ---
   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.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Thank you @gwgrubbs
I've just pushed up a fix for the null ref (hopefully), i think it got lost either in a PR that wasn't merged where this was fixed, or something else. Also, this has been merged into master.
If you're using a razor page, you can use the [AuthorizeForScopes] attribute to handle the challenge on the class describing the page.

@schmid37 I'm not able to reproduce this. Do you have a basic repro you can share w/exact steps? That will help determine a fix faster. Thank you. You can email it if you want too: [email protected]

@jennyf19 Thank you for the help. I emailed you!

@gwgrubbs @schmid37

This is included in Microsoft Identity Web 0.2.1-preview release.

Was this page helpful?
0 / 5 - 0 ratings