Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] : NullReferenceException when acquiring a token for a user in a server-side Blazor app. #157

Closed
6 tasks
gwgrubbs opened this issue May 12, 2020 · 25 comments

Comments

@gwgrubbs
Copy link

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.

@bgavrilMS
Copy link
Member

Possibly a duplicate of #136

@gwgrubbs - do you have a stack trace?

@gwgrubbs
Copy link
Author

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.

@jennyf19 jennyf19 added this to the 0.1.4-preview milestone May 15, 2020
@jmprieur jmprieur removed this from the 0.1.4-preview milestone May 27, 2020
@jaimunday
Copy link

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?

@jmprieur
Copy link
Collaborator

@gwgrubbs

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

@gwgrubbs
Copy link
Author

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

@jmprieur jmprieur added this to the [6] Support new scenarios milestone Jun 26, 2020
@isaacrlevin
Copy link

@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

@jennyf19
Copy link
Collaborator

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

@dansmitt
Copy link

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

@jmprieur
Copy link
Collaborator

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

cc: @jennyf19

@jennyf19
Copy link
Collaborator

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

@gwgrubbs
Copy link
Author

gwgrubbs commented Jul 20, 2020

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

@gwgrubbs
Copy link
Author

@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
Copy link
Collaborator

jennyf19 commented Jul 20, 2020

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.

@gwgrubbs
Copy link
Author

@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()

accessToken = await GetAccessTokenOnBehalfOfUserFromCacheAsync(
_application,
user ?? CurrentHttpContext.User,
scopes,
tenant,
userFlow)
.ConfigureAwait(false);

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.

@jennyf19
Copy link
Collaborator

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

@jennyf19
Copy link
Collaborator

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

@dansmitt
Copy link

@jennyf19 works so far :)

@dansmitt
Copy link

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

@gwgrubbs
Copy link
Author

@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
Copy link
Collaborator

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

@dansmitt
Copy link

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

@jennyf19
Copy link
Collaborator

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

@dansmitt
Copy link

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

@jennyf19
Copy link
Collaborator

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

@pmaytak
Copy link
Contributor

pmaytak commented Jul 25, 2020

@gwgrubbs @jaimunday @isaacrlevin @schmid37

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

@pmaytak pmaytak closed this as completed Jul 25, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants