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)
@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 aMsalUiRequiredException
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 isCurrentHttpContext
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:
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 can see where line 218 in your branch has
CurrentHttpContext.User
, which may be the source of theNullReferenceException
@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:
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):
In my service, I’m using the following code to acquire a token, which caused the exception:
tokenAcquisition
is an instance ofITokenAcquisition
injected in the service which was configured using the service extension methods as shown in Startup.cs above. Looking at line 337 ofMicrosoft.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