AzureAD / microsoft-identity-web

Helps creating protected web apps and web APIs with Microsoft identity platform and Azure AD B2C
MIT License
679 stars 211 forks source link

IDW10502: An MsalUiRequiredException #1803

Open michiproep opened 2 years ago

michiproep commented 2 years ago

Microsoft.Identity.Web Library

Microsoft.Identity.Web

Microsoft.Identity.Web version

1.25.1

Web app

Sign-in users and call web APIs

Web API

Not Applicable

Token cache serialization

Distributed caches

Description

To me, it seems to be a bug that var res = await _api.CallWebApiForUserAsync(ServiceName, options => { options.RelativePath = "/api/getsomething"; //options.Scope = config.Scope //injected via config }, user: user); throws "IDW10502: An MsalUiRequiredException => inner: An error occured during token acquisition: No account or login hint was passed to the AcquireTokenSilent call" while at the same time this works: var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(new string[] { config.Scope }, user: user);

Reproduction steps

  1. Setup AddMicrosoftIdentityWebApp(b2cConfig) .EnableTokenAcquisitionToCallDownstreamApi(new string[] { config.Scope }) .AddDownstreamWebApi("MyApi", builder.Configuration.GetSection("MyApi")) .AddDistributedTokenCaches();//I do use redis
  2. Within Oidc's OnTokenValidated event call -tokenAcquisition.GetAccessTokenForUserAsync //success -_api.CallWebApiForUserAsync both with "user: context.Principal"

Error message

IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. ---> MSAL.NetCore.4.35.1.0.MsalUiRequiredException: ErrorCode: user_null Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.

Id Web logs

fail: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[17] Exception occurred while processing message. Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. ---> MSAL.NetCore.4.35.1.0.MsalUiRequiredException: ErrorCode: user_null Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call. at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.ApiConfig.Executors.ClientApplicationBaseExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenSilentParameters silentParameters, CancellationToken cancellationToken) at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForWebAppWithAccountFromCacheAsync(IConfidentialClientApplication application, ClaimsPrincipal claimsPrincipal, IEnumerable1 scopes, String authority, MergedOptions mergedOptions, String userFlow, TokenAcquisitionOptions tokenAcquisitionOptions) at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions) StatusCode: 0 ResponseBody:
Headers: --- End of inner exception stack trace --- at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions) at Microsoft.Identity.Web.DownstreamWebApi.CallWebApiForUserAsync(String serviceName, String authenticationScheme, Action1 calledDownstreamWebApiOptionsOverride, ClaimsPrincipal user, StringContent content) at STPv2.Infrastructure.UrmApi.Me(ClaimsPrincipal user) in C:\Users\mlproe\source\repos\STPv2\STPv2\Infrastructure\UrmApi.cs:line 24 at STPv2.Program.UrmAuthentication(TokenValidatedContext ctx) in C:\Users\mlproe\source\repos\STPv2\STPv2\Program.cs:line 154 at STPv2.Program.<>c.<b2_7>d.MoveNext() in C:\Users\mlproe\source\repos\STPv2\STPv2\Program.cs:line 103 --- End of stack trace from previous location --- at Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.<>c__DisplayClass11_1.<b2>d.MoveNext() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RunTokenValidatedEventAsync(OpenIdConnectMessage authorizationResponse, OpenIdConnectMessage tokenEndpointResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt, String nonce) at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync() Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler: Error: Exception occurred while processing message.

Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. ---> MSAL.NetCore.4.35.1.0.MsalUiRequiredException: ErrorCode: user_null Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call. at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.ApiConfig.Executors.ClientApplicationBaseExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenSilentParameters silentParameters, CancellationToken cancellationToken) at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForWebAppWithAccountFromCacheAsync(IConfidentialClientApplication application, ClaimsPrincipal claimsPrincipal, IEnumerable1 scopes, String authority, MergedOptions mergedOptions, String userFlow, TokenAcquisitionOptions tokenAcquisitionOptions) at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions) StatusCode: 0 ResponseBody:
Headers: --- End of inner exception stack trace --- at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions) at Microsoft.Identity.Web.DownstreamWebApi.CallWebApiForUserAsync(String serviceName, String authenticationScheme, Action1 calledDownstreamWebApiOptionsOverride, ClaimsPrincipal user, StringContent content) at STPv2.Infrastructure.UrmApi.Me(ClaimsPrincipal user) in C:\Users\mlproe\source\repos\STPv2\STPv2\Infrastructure\UrmApi.cs:line 24 at STPv2.Program.UrmAuthentication(TokenValidatedContext ctx) in C:\Users\mlproe\source\repos\STPv2\STPv2\Program.cs:line 154 at STPv2.Program.<>c.<b2_7>d.MoveNext() in C:\Users\mlproe\source\repos\STPv2\STPv2\Program.cs:line 103 --- End of stack trace from previous location --- at Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.<>c__DisplayClass11_1.<b2>d.MoveNext() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RunTokenValidatedEventAsync(OpenIdConnectMessage authorizationResponse, OpenIdConnectMessage tokenEndpointResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt, String nonce) at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync()

Relevant code snippets

var res = await _api.CallWebApiForUserAsync(ServiceName, options =>
{
  options.RelativePath = "/api/getsomething";
  //options.Scope = config.Scope //injected via config
}, user: user);
//AND
var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(new string[] { config.Scope }, user: user);

Regression


Expected behavior

_api.CallWebApiForUserAsync get the required access token just as tokenAcquisition.GetAccessTokenForUserAsync does

jmprieur commented 2 years ago

@michiproep, do you have a [AuthorizeForScope] attribute on your controller or controller action?

michiproep commented 2 years ago

@jmprieur , I'm not calling this from a controller but within OnTokenValidated within Authentication middleware

jmprieur commented 2 years ago

I see. That means you didn't request the necessary scopes or claims Look at the Claims member in the exception.

michiproep commented 2 years ago

Well, of course I can request all scopes within the initial authorize request but that still does not explain why "ItokenAcquisition" is working while "Idownstreamapi" does not. When I look at the exception, it says "ErrorCode: user_null". But I do provide the same claimPrinciple to both calls.

jmprieur commented 2 years ago

Fair question, @michiproep. we'll have a look why ITokenAcquisition works, rather (as if used in the OnTokenValidated event I would not expect it to work either) cc: @jennyf19

nikhilvats7oct commented 2 years ago

Do we have any updates on this please, I'm also having same scenario where I'm trying to get the access token on authentication middleware and getting the same exception, however if we are trying to get token from controller, then it always works but not at the time of OnTokenValidated?

jmprieur commented 2 years ago

In a web app, OnTokenValidated is called when the ID Token is received. The token is not yet obtained by MSAL.NET by redeeming the authentication code, which is why I'm not expecting things to work at that stage.

They could work if you chain the events and make sure that the previous event handler (set by Microsoft identity web) is called before your own code processes the event. This is shown here : https://github.com/AzureAD/microsoft-identity-web/wiki/customization#how-to-query-microsoft-graph-on-token-validated

nikhilvats7oct commented 2 years ago

Thank you for your reply. I tried to chain the events as mentioned in above link but it also doesn't work.

One thing I noticed when app redirects to identity, it is showing the response_type=code, it seems wrong, shouldn't it be "id_token". I'm using AddMicrosoftIdentityWebApp to register my web app and the default response type in this case is "id_token".

I'm not sure if something is missing in my implementation or it is always the case.

https://login.microsoftonline.com/f009f285-5242-433a-9365-daa1edf145c3/oauth2/v2.0/authorize? client_id=xxxx-xxx-xxxx-xxxx &redirect_uri=https%3A%2F%2Flocalhost%3A55878%2Fsignin-oidc &response_type=code &scope=openid%20profile%20offline_access%20api%3A%2F%2Fffe42174-xxxx-xxxx-xxxx-xxxxxxxxe%2Faccess_as_user &code_challenge=qD_Sjpq585bvtLbqog2Cus6IW9r3940wo9s27a1hlXg &code_challenge_method=S256 &response_mode=form_post &nonce=637969021956837698.NzVhZjljZDctOTA1Zi00MmMwLWFhODMtMzEwMzA3ZDI1NGUyZjkxOGVmMGYtZTliZC00MmUyLThhMmEtMmJkZDRmNjE1MjBm &client_info=1 &x-client-brkrver=IDWeb.1.25.1. &state=CfDJ8PpKgloFJPhJla6verKay9QYDQxX4gDLnh4U4fQfcv1TX-Zf-UJm8AfS_zt4qdnZAbA9i7ZfodaIbvJkwTiErrqRD71lvllrP6x_JWPd28rvaIN2UiaLSNTQqaINzmKdRlu2O0faGFiATeZerKyoljtXtXi9PcNYY4osgfmbH9em9I6jq9UVzBvREFnkOOxOpe_iOPjRzBnPLytLSd7KJACZHiXkrIr7bhOlYfDt1MtietU0dK34Rcj4NZ1Dw-7PGAL8QGHFHjmWg6sDoJi3_9EJEP7JkaSWDsp5gcR4p2Hcb3Peh60LZL1x8KvOKCLU04dPUTOr1HpTXkLtqkksCWlwuhpEyCmj3cdIi6Z6I5W6EvnnenwD1aaN4inX6wQv_Q &x-client-SKU=ID_NET6_0 &x-client-ver=6.20.0.0

jmprieur commented 2 years ago

@nikhilvats7oct, many customers don't want Id.Web to use ID tokens (because they read about the implicit flow, and unfortunately, to get an ID token, in the portal you need to check a checkbox which is in an area named "implicit flow" (whereas, indeed, code_idtoken is perfectly ok. Therefore Id.Web only uses code.

If you want to have id_token, you can change the response type in the OpenIdConnect options.

michiproep commented 2 years ago

This discussion is going into a complete wrong direction. The initial question was "why CallWebApiForUserAsync is not able to get a token while GetAccessTokenForUserAsync does". It's still not clear why they behave differently.

udaykiran1105 commented 1 year ago

I encountered the same issue (ITokenAcquisition.GetAccessTokenForUserAsync works but IDownstreamWebApi.CallWebApiForUserAsync doesn't) on a controller action.

As for the onTokenValidated(), I've found that overriding it with our own code skips the default available in the library where it sets this item in the HttpContext: "JwtSecurityTokenUsedToCallWebAPI". At the time of the token acquisition call, the code checks for this item within the context, and if not found, it returns the error 'no account or login hint was found....'.

So, if you still want to override onTokenValidated(), make sure to include a hack where you explicitly set the 'JwtSecurityTokenUsedToCallWebAPI' key in the HttpContext.Items dictionary. It would look similar to what's being done in the library here: https://github.com/AzureAD/microsoft-identity-web/blob/491b8efb8d8aea7987483a8822aa1628b1d64869/src/Microsoft.Identity.Web/HttpContextExtensions.cs

Do make sure to explicitly remove the item from the dictionary when you are done with the downstream API call (a try-finally block could be used for this)

udaykiran1105 commented 1 year ago

@jmprieur Any further update on this investigation?

michiproep commented 1 year ago

I encountered the same issue (ITokenAcquisition.GetAccessTokenForUserAsync works but IDownstreamWebApi.CallWebApiForUserAsync doesn't) on a controller action.

As for the onTokenValidated(), I've found that overriding it with our own code skips the default available in the library where it sets this item in the HttpContext: "JwtSecurityTokenUsedToCallWebAPI". At the time of the token acquisition call, the code checks for this item within the context, and if not found, it returns the error 'no account or login hint was found....'.

So, if you still want to override onTokenValidated(), make sure to include a hack where you explicitly set the 'JwtSecurityTokenUsedToCallWebAPI' key in the HttpContext.Items dictionary. It would look similar to what's being done in the library here: https://github.com/AzureAD/microsoft-identity-web/blob/491b8efb8d8aea7987483a8822aa1628b1d64869/src/Microsoft.Identity.Web/HttpContextExtensions.cs

Do make sure to explicitly remove the item from the dictionary when you are done with the downstream API call (a try-finally block could be used for this)

For this, I call the tokenAquisition with context.Principal where context comes from onTokenValidated = async context =>{...}

dmamagiannos commented 1 year ago

Any further update on this issue?

michiproep commented 1 year ago

I haven't checked on this issue in quite a while but with the latest version (2.9.0) neither one is working now. Additional info: -I do request the scope in the initial .EnableTokenAcquisitionToCallDownstreamApi(new[] { MyScope }) -I can see the accessToken in TokenEndpointResponse within the TokenValidatedContext which is actually new in v2.x => So, I could actually pass this token along but then I cannot make use of ITokenAquisition nor IDownstreamApi

To me, it seems like the tokenCache is not initialzed after redeemAuthCode internally anymore but this is just a guess

dmamagiannos commented 1 year ago

This problems still remains on version 2.11.0 of Microsoft.Identity.Web NuGet package. I am obliged to force log out to solve the problem, but this leds to complains from my customers. Is there any update on this issue?

On a NET 7.0 blazor web server, I have the below configuration :

builder.Services .AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAdB2C") .EnableTokenAcquisitionToCallDownstreamApi(builder.Configuration.GetSection("AzureADB2CScopesSettings").Get<AzureADB2CScopesSettings>().Scopes) .AddSessionTokenCaches();

I have checked both "Access tokens (used for implicit flows)" and "ID tokens (used for implicit and hybrid flows)" on blazor's application.

The user flow configuration is :

May is something that I have misconfigured ? Thank you