Microsoft-identity-web: [Bug] : NullReferenceException when acquiring a token for a user in a server-side Blazor app.

Created on 13 May 2020  路  25Comments  路  Source: AzureAD/microsoft-identity-web

Which Version of Microsoft Identity Web are you using ?
Microsoft Identity Web 0.1.2-preview

Where is the issue?

  • Web App

    • [ ] Sign-in users

    • [ X] 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

    • [X ] In Memory caches

    • [ ] Session caches

    • [ ] Distributed caches

This is a new application that is a server-side Razor application calling a protected Web API. The application works as expected locally, but fails with a NullReferenceException when running as an Azure App Service. The failure occurs when trying to retrieve a token for a user (GetAccessTokenForUserAsync). The request to get a token for the user occurs in a lower-level HttpClient service when retrieving the token to add as an Authorization header. The HttpClient service is injected into the Blazor page (@inject). As a test, I've added code to the OnGet() method of the Blazor page (which runs on first access of a page from the site), and the same code works as expected both locally and in Azure (GetAccessTokenForUserAsync returns a valid token with no exception).

I have confirmed that the following values are correctly configured:

  • TenantId
  • ClientId
  • ClientSecret

I have confirmed the application has been registered correctly in Azure AD with valid value(s) for:

  • RedirectURIs
  • Scopes

Given I can run the application locally with no exceptions, I don't think I have missing or invalid configuration. I also don't think this issue has anything to do with the Web API itself, this exception occurs simply trying to get a token from the cache and/or from MSAL prior to communication with the (protected) Web API.

Repro

Startup.cs

services
    .AddSignIn(
        openIdOptions => Configuration.Bind("AppAzureAd", openIdOptions),
        identityOptions => Configuration.Bind("AppAzureAd", identityOptions)
    )
    .AddWebAppCallsProtectedWebApi(
        Configuration.GetSection("Api:Scopes").Get<List<string>>(),
        openIdOptions => Configuration.Bind("AppAzureAd", openIdOptions),
        identityOptions =>
        {
            Configuration.Bind("AppAzureAd", identityOptions);
            identityOptions.EnablePiiLogging = environment.IsDevelopment();
        }
    )
    .AddInMemoryTokenCaches()
    .AddTokenAcquisition();

Blazor Page method that works as expected (no exceptions, token acquired) on first page request:

public async Task<IActionResult> OnGet()
{
    if (!User.Identity.IsAuthenticated)
        return Challenge();

    try
    {
        var token = await tokenAcquisition.GetAccessTokenForUserAsync(apiOptions.Value.Scopes);
    }
    catch (MsalUiRequiredException 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);
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        return Challenge();
    }

    return Page();
}

HttpClient setting the authorization header (GetAccessTokenForUserAsync works locally, but fails in Azure):

private async Task AddAuthorizationHeaderAsync(HttpRequestMessage request)
{
    request.Headers.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, await AcquireTokenAsync());
}

private async Task<string> AcquireTokenAsync()
{
    string token;
    try
    {
        token = await tokenAcquisition.GetAccessTokenForUserAsync(apiOptions.Value.Scopes);
    }
    catch (MsalUiRequiredException 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);
        throw;
    }

    return token;
}

Expected behavior
I expect to be able to acquire a valid token for the user at any point during the "request" lifecycle.

Actual behavior
The following stack trace:

[2020-05-12T21:55:47.258Z] Error: System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.Identity.Web.TokenAcquisition.CreateRedirectUri()
   at Microsoft.Identity.Web.TokenAcquisition.BuildConfidentialClientApplicationAsync()
   at Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync()
   at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable`1 scopes, String tenant)
   ...

Possible Solution
At a minimum, better exception handling that leads me to the actual error.

Blazor bug fixed need repro token cache serialization - in memory cache web app

Most helpful comment

@jennyf19 sounds good to me! Thx for the support 馃挭馃徎

All 25 comments

Possibly a duplicate of https://github.com/AzureAD/microsoft-identity-web/issues/136

@gwgrubbs - do you have a stack trace?

I don't think it's a duplicate as the stack trace indicates a different point in the stack for the exception.

Stack trace for #136:

at Microsoft.Identity.Web.HttpContextExtensions.GetTokenUsedToCallWebAPI(HttpContext httpContext) in D:\a\1\s\src\Microsoft.Identity.Web\HttpContextExtensions.cs:line 29

Stack trace for my issue (included in "Actual behavior" of submitted ticket):

at Microsoft.Identity.Web.TokenAcquisition.CreateRedirectUri()

From review of #136, I thought the issue may be related to this line in CreateRedirectUri method of TokenAcquisition.cs:

var request = CurrentHttpContext.Request;

where a root issue of a null HttpContext may be the similarity between #136 and this issue.

I also have this same issue (works perfectly locally but Azure Web App fails).
I notice it was tagged for the new preview but removed now? Do you know which preview this would be added into instead please?

@gwgrubbs

1) why did you add AddTokenAcquisition
2) I'm assuming that you have not activated the App Services authentication? (you should not)

@jmprieur

  1. I thought AddTokenAcquisition was a requirement for use with this package to support fetching and auto-refresh of AD tokens (to add as an authorization header for requests to a protected Web API). Please correct me if I am wrong.
  2. "App Services authentication" is "off" for the app service in Azure.

I do think there is a case with Blazor that may be an issue across the board. As described in the issue report, everything works as expected in the OnGet() method of the Blazor page; i.e. I can use GetAccessTokenForUserAsync and successfully retrieve an AD token. This is because this is the original request to the server, at which time HttpContext is available. In the service methods that I have where it doesn't work, the methods are executed via requests made over a web socket where HttpContext is not available. So I'm concluding that anything in Microsoft.Identity.Web that depends on HttpContext will not work in Blazor applications when the requests are made over web socket.

The reason (I believe) it works locally is that HttpContext is available for subsequent requests by the way local development environments work - it fails in Azure by the way browsers communicate via web socket to the App Service.

I also implemented a rather clunky workaround where in the OnGet() method I set a property on the model with the value of the AD Token that I then inject as a CascadingParameter into the components, which is then passed to my service method as a method arg. A lot of plumbing/passing of the AD Token where it would be ideal to use Microsoft.Identity.Web to request a token when/where needed.

@gwgrubbs you are right with the bit about HttpContext. The workaround is to have a service that caches the token for you. I am doing this here

https://github.com/isaacrlevin/PresenceLight/blob/main/src/PresenceLight.Worker/Startup.cs#L69

This is using Microsoft.Identity.Client so it won't work here directly. Need to modify the process to get the token but it IS doable

@gwgrubbs we believe this has been fixed with this branch, if you want to try it out and get back to us. Thank you.

@jennyf19 I still get a NullReferenceException at TokenAcquisition.cs:line 220

@schmid37 : could you please provide the stack trace?
or/and a repro?
thanks

cc: @jennyf19

@gwgrubbs I'm unable to repro this issue. We now have web app blazor templates, which you can try as well. Do you have repro you can share?

@jennyf19 - I pulled your branch and corrected breaking changes.

I reviewed the sample project "BlazorServerSideWeb-CSharp", specifically to look at Startup.cs to see how to "correctly" configure Microsoft.Identity.Web[.UI]. Here's what I have in my Startup.cs:
```c#
services.AddMicrosoftWebAppAuthentication(Configuration, "AppAzureAd")
.AddMicrosoftWebAppCallsWebApi(Configuration, Configuration.GetSection("Api:Scopes").Get>(), "AppAzureAd")
.AddInMemoryTokenCaches();

When running with assembly references to those from your branch, everything works fine locally.  However, running in Azure I get the following (as reported in Chrome console):

```c#
[2020-07-20T20:14:12.249Z] Error: System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.Identity.Web.TokenAcquisition.BuildConfidentialClientApplicationAsync() in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\TokenAcquisition.cs:line 337
   at Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync() in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\TokenAcquisition.cs:line 326
   at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable`1 scopes, String tenant, String userFlow) in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\TokenAcquisition.cs:line 211
   at OBFUSCATED.Web.App.Service.AppService.AddAuthorizationHeaderAsync(HttpRequestMessage request, String accessToken) in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Service\AppService.cs:line 90
   at OBFUSCATED.Web.App.Service.AppService.<>c__DisplayClass4_0`1.<<GetAsync>b__0>d.MoveNext() in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Service\AppService.cs:line 38
--- End of stack trace from previous location where exception was thrown ---
   at OBFUSCATED.Net.Http.HttpClientService.SendRequestAsync[TReturn](HttpRequestMessage message, Func`2[] configure) in D:\src\OBFUSCATED\src\OBFUSCATED\Net\Http\HttpClientService.cs:line 149
   at OBFUSCATED.Net.Http.HttpClientService.GetAsync[TReturn](Action`1 urlPathBuilder, Func`2[] configure) in D:\src\OBFUSCATED\src\OBFUSCATED\Net\Http\HttpClientService.cs:line 47
   at OBFUSCATED.Web.App.Service.AppService.GetAsync[T](String accessToken, Action`1 urlPathBuilder) in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Service\AppService.cs:line 38
   at OBFUSCATED.Web.App.Service.AssetService.GetPageAsync(String accessToken, PageModel page, String filterToFundId) in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Service\AssetService.cs:line 22
   at OBFUSCATED.Web.App.Pages.Asset.FetchCurrentPage() in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Pages\Asset.razor:line 160
   at OBFUSCATED.Web.App.Pages.Asset.OnInitializedAsync() in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Pages\Asset.razor:line 96
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()

In my service, I'm using the following code to acquire a token, which caused the exception:
c# await tokenAcquisition.GetAccessTokenForUserAsync(apiOptions.Value.Scopes);
tokenAcquisition is an instance of ITokenAcquisition injected in the service which was configured using the service extension methods as shown in Startup.cs above. Looking at line 337 of Microsoft.Identity.Web.TokenAcquisition,
its trying to resolve HttpContext: CurrentHttpContext.Request. HttpContext is null at this point as the "request" is over web socket.

@jennyf19 I still get a NullReferenceException at TokenAcquisition.cs:line 220

@jennyf19, I can see where line 218 in your branch has CurrentHttpContext.User, which may be the source of the NullReferenceException @schmid37 is experiencing.

Thanks @gwgrubbs, did you try with the current master branch? Changes were made to not use the CurrentHttpContext.User where possible. There is an Optional claims principal representing the user that can be passed in.

@jennyf19 I just pulled master branch (18c4950), and I'm still getting the exception; now getting it where @schmid37 was seeing it:

blazor.server.js:15 [2020-07-21T01:45:11.004Z] Error: System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable`1 scopes, String tenant, String userFlow, ClaimsPrincipal user) in D:\src\microsoft-identity-web\src\Microsoft.Identity.Web\TokenAcquisition.cs:line 220
   at OBFUSCATED.Web.App.Service.AppService.AddAuthorizationHeaderAsync(HttpRequestMessage request, String accessToken) in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Service\AppService.cs:line 90
   at OBFUSCATED.Web.App.Service.AppService.<>c__DisplayClass4_0`1.<<GetAsync>b__0>d.MoveNext() in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Service\AppService.cs:line 38
--- End of stack trace from previous location where exception was thrown ---
   at OBFUSCATED.Core.Net.Http.HttpClientService.SendRequestAsync[TReturn](HttpRequestMessage message, Func`2[] configure) in D:\src\OBFUSCATED\src\OBFUSCATED.Core\Net\Http\HttpClientService.cs:line 149
   at OBFUSCATED.Core.Net.Http.HttpClientService.GetAsync[TReturn](Action`1 urlPathBuilder, Func`2[] configure) in D:\src\OBFUSCATED\src\OBFUSCATED.Core\Net\Http\HttpClientService.cs:line 47
   at OBFUSCATED.Web.App.Service.AppService.GetAsync[T](String accessToken, Action`1 urlPathBuilder) in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Service\AppService.cs:line 38
   at OBFUSCATED.Web.App.Service.EntityService.GetPageAsync(String accessToken, PageModel page) in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Service\EntityService.cs:line 23
   at OBFUSCATED.Web.App.Pages.Entity.FetchCurrentPage() in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Pages\Entity.razor:line 252
   at OBFUSCATED.Web.App.Pages.Entity.OnInitializedAsync() in D:\src\OBFUSCATED\src\OBFUSCATED.Web.App\Pages\Entity.razor:line 90
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()

https://github.com/AzureAD/microsoft-identity-web/blob/18c4950c56be965d1819b350e4cdbf8c3d8243c6/src/Microsoft.Identity.Web/TokenAcquisition.cs#L220-L226
I do see the optional claims principal in the method signature, the problem is I have no access to this value in the context of a web socket - not without a bunch of hoops and tying in to the oidc events (as @isaacrlevin did). My hope was you guys could figure out the lift between DI, scoped services, token caching, etc. to be able to manage all of this internally.

@gwgrubbs thanks for the reply. we are working on a solution for this. will keep you posted here.

@gwgrubbs Can you try this branch it should handle the user context for you...proof of concept inspired by this sample

@jennyf19 works so far :)

@jennyf19 Sometimes I get an Exception:

blazor.server.js:19 [2020-07-22T14:44:30.900Z] 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.WebHttpContextExtensions.cs:line 29
at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable`1 scopes, String tenant, String userFlow, ClaimsPrincipal user) in C:\src\Microsoft.Identity.WebTokenAcquisition.cs:line 258

When I delete the cookies and do a reload, Everything works fine. Is this a problem related to the actual chanages?

The CurrentHttpContext at line 258 is null.

@jennyf19 I concur with @schmid37 that it is working so far. I haven't experienced the issue you see, but I think I know what the issue is. if you have a stale cookie and call GetAccessTokenOnBehalfOfUserFromCacheAsync, line 243 will throw a MsalUiRequiredException exception (expecting you to then initiate some redirect to signin to re-authenticate or something). In the catch block, line 258 will cause the result you see, as the rule is CurrentHttpContext will always be null.

@gwgrubbs & @schmid37 glad to hear it's working, thanks for confirming. we are aware of the redirect issue, it's because blazor doesn't have the challenge method, so we're trying to come up with a work around.

@gwgrubbs & @schmid37 glad to hear it's working, thanks for confirming. we are aware of the redirect issue, it's because blazor doesn't have the challenge method, so we're trying to come up with a work around.

@jennyf19 how can we track this? Is there an existing open issue?

@schmid37 this is the only issue for tracking at the moment. we're trying to get this into our next release (today or tomorrow), but if we run out of time (we have an external deadline), then I will close this and open a new issue, ping you both here so you know. sound good?

@jennyf19 sounds good to me! Thx for the support 馃挭馃徎

@schmid37 @gwgrubbs here's the new issue for the second part of the call. will still try to get this in the current milestone if possible.

@gwgrubbs @jaimunday @isaacrlevin @schmid37

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

Was this page helpful?
0 / 5 - 0 ratings