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

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.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 25 (2 by maintainers)

Most upvoted comments

@jennyf19 sounds good to me! Thx for the support 💪🏻

@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 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.

@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.

@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.

@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 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.Web\HttpContextExtensions.cs:line 29 at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable`1 scopes, String tenant, String userFlow, ClaimsPrincipal user) in C:\src\Microsoft.Identity.Web\TokenAcquisition.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 works so far 😃

@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.

@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:

services.AddMicrosoftWebAppAuthentication(Configuration, "AppAzureAd")
	.AddMicrosoftWebAppCallsWebApi(Configuration, Configuration.GetSection("Api:Scopes").Get<List<string>>(), "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):

[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:

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

@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