Code Monkey home page Code Monkey logo

Comments (53)

andrenpt avatar andrenpt commented on August 15, 2024 10

The AuthorizeForScopes attribute doesn't work when you have an action ie:
[HttpGet]
[AuthorizeForScopes(ScopeKeySection = "Scheduler:SchedulerScope")]
public async Task<IActionResult> Index()
{
return this.View("Index");
}

But then inside of that view you have a protected Ajax Call. IE:

[HttpGet]
public async Task<JsonResult> GetUsers()
{
var accessToken = await this.tokenAcquisition.GetAccessTokenForUserAsync(new[] { this.configuration["Scheduler:SchedulerScope"] });
return null
}

When we run the app for the second time and the cookie its already created the tokenAcquisition will throw an Exception and because this is an Ajax Call your attribute wont be able to handle the situation.

What do you recommend in this cases ?

from active-directory-aspnetcore-webapp-openidconnect-v2.

joegithub avatar joegithub commented on August 15, 2024 9

What is the expected result of GetAccessTokenOnBehalfOfUser when working in a development environment where the client application is being stopped and started frequently? In my situation I have a client web app that uses Azure AD authentication to allow only users in my org directory to log in. I am using "AddInMemoryTokenCaches" but am experiencing the same issue described below when using the session token cache option.

Following examples in the azure-samples projects I've implemented JWT bearer auth with my API which is also registered in Azure AD. When the client web app starts up and follows the standard sign-in process my subsequent calls to GetAccessTokenOnBehalfOfUser return a token as expected. Stepping through I can see the account (IAccount) getting set in the call to GetAccountAsync.

Stopping the application and then starting up again I am still logged into my client web app, but when requesting an access token a MsalServiceException "No account or login hint was passed to the AcquireTokenSilent call" is thrown. Stepping through I see that my account identifier is still set but the call to GetAccountAsync as well as GetAccountsAsync both return null. The only way I get TokenAcquisition working again is to sign out of the client and go through the sign in process again. This is an issue for users if their browser cookie is letting them into the application but then calls to my API fail.

Should the MsalServiceException be handled in some prescribed way to refresh the confidential client app's cache so that an account can be found? In my testing I have also tried using Sql server as a token cache provider and not experienced this issue. I wanted to get some feedback before deciding to use Sql server as opposed to the other token cache options.
Thank you.

from active-directory-aspnetcore-webapp-openidconnect-v2.

pmeems avatar pmeems commented on August 15, 2024 6

@JosephAspey Thanks for the reply. I also had so many problems when using MSAL I moved to Google Authenticate and got it working in a few hours. Here's my repo: https://bitbucket.org/pmeems/matblazorgooglegsuite/src/develop/

from active-directory-aspnetcore-webapp-openidconnect-v2.

mpp avatar mpp commented on August 15, 2024 6

Hi, my solution is to handle that exception in the ErrorController:

public class ErrorController : Controller
{
    private readonly ILogger<ErrorController> _logger;
        
    public ErrorController(ILogger<ErrorController> logger)
    {
        _logger = logger;
    }

    [Route("/error")]
    public async Task<IActionResult> Error()
    {
         var context = HttpContext.Features.Get<IExceptionHandlerFeature>();

        if (context.Error is MsalUiRequiredException ex)
        {
            var appBaseUrl = $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}";
            var azureLogInUrl = $"{appBaseUrl}/MicrosoftIdentity/Account/SignIn";
            await Request.HttpContext.SignOutAsync();
            return Redirect(azureLogInUrl);
        }

        return Problem();
    }
}

(fix the redirect to your login page!)

You tell the app to use it in Startup.cs

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
     }
    [...]
}

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024 5

Closing the browser is not the solution. AuthorizeForScopeAttribute is your solution. If you want to make it global, then you will have to create your own global filter for MsalUIException.

Prod is working fine because you probably never restarted IIS. If you do it, all the users will get the same error.

Again, the try/catch is the recommended pattern to acquire a token silently. We wrapped that logic for you on Microsoft.Identity.Web, but you can also create your own filter. In the end, you want to catch MsalUIException and return a Challenge so the authorization code flow request happens again (the user wont need to type the password if they have an active cookie).

try
{
...
}
catch (MsalUiRequiredException)
{
      return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme);
}

from active-directory-aspnetcore-webapp-openidconnect-v2.

joegithub avatar joegithub commented on August 15, 2024 2

Hi @joegithub, the point that you are bringing indeed happens quite often on the development stage. The problem in question is that the browser still holds the user s cookies when you restart your app, so the user is still logged in. However, restarting the app erases the InMemory cache (that is bootstrapped on the login flow) resulting in this sync problem where the user is set on the frontend but not on the backend.
One of the approaches that we have been using is to decorate the endpoint that consumes GetAccessTokenOnBehalfOfUser, with MsalUiRequiredExceptionFilterAttribute passing the scope being used. For example:

[MsalUiRequiredExceptionFilter(Scopes = new[] {"User.Read"})]
public async Task<IActionResult> ShowUserProfile() { ... }

This is not the only (or "the right") solution available and you might need to analyze if it fits your production application. For example, if you need to revoke the user token cache (ie the account got compromised) and force them to login if this happens, the MsalUiRequiredExceptionFilter might not be ideal. But understanding where the problem comes from, help the developers to implement a solution.
About your question on the SQL token cache, keep in mind that it doesn't bullet proof your application from this problem. There is still that chance where your cache table needs to be erased for some reason, or the classic "accidently drop table command being executed in prod" haha =]
Choosing which way to store the cached token requires an analysis of your business needs. Each of them have their pros and cons.

Thank you for this information. I attempted to use the MsalUiRequiredExceptionFilter attribute and have not had success yet. My code is calling GetAccessTokenOnBehalfOfUser in the "CreateHttpRequestMessageAsync" method of a "BaseClient" class of an API client project, the scenario being that we use NSwagStudio to generate our API client classes and intercept the http request before they're sent to add the JWT bearer auth token. I currently lack the understanding to know where exactly the exception filter attribute should go, but I think I'm correct that the scope I need to pass is that of the API app registered in our Azure portal.

I became curious why the GetAccessTokenOnBehalfOfUser method in TokenAcquisition failed to get the account when the necessary properties are available in the HttpContext. I went as far as creating my own Account class, inheriting from IAccount, and making a new account object after the call to GetAccountAsync returns null. The necessary values to populate Username, Environment, and create the AccountId class can all be pulled from the User.Identity in the http context. Despite this, when my manually created class is passed to AcquireTokenSilent I get a different error in the exception: "No Refresh Token found in the cache", so I'm probably attempting something that was never intended.

On the sql token caching, I ran into a different issue when trying to run multiple client web projects at the same time. The call to MSALPerUserSqlTokenCacheProvider.ReadCacheForSignedInUser throws a CryptographicException with message "The payload was invalid". This still occurred when I configured each web application to use its own token cache db, which leads me to believe this could be caused by the fact that I'm running both projects locally from Visual Studio instead of accessing them from independently deployed apps running on a server but I haven't tested this yet.

I appreciate your advice and have learned (and continue to learn) a great deal going through the Azure Core OpenId connect samples.

In case it might help anyone who is trying to figure out how to deal with the "MsalUiRequiredException" I'll post what I ended up doing that worked for my specific scenario (asp.net core 2.2 web client app using Azure AD for user auth with JWT bearer auth to call an api).
I have an [Authorize] attribute on my HomeController, the first place the app should encounter the need to authenticate. I was getting caught up in the complexity of the Identity flow and not seeing the simple solution - just sign out the user and then redirect them to log in again.

            ...}
            catch (MsalUiRequiredException ex)
            {
                _logger.LogError(ex, ex.Message);
                //situation where client has log in cookie in browser cache but application does not have account info in memory
                var appBaseUrl = $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}";
                var azureLogInUrl = $"{appBaseUrl}/AzureAD/Account/SignIn";
                await Request.HttpContext.SignOutAsync();
                return Redirect(azureLogInUrl);
                
            }
            catch (SwaggerException ex)
            {...

from active-directory-aspnetcore-webapp-openidconnect-v2.

kalyankrishna1 avatar kalyankrishna1 commented on August 15, 2024 1

Sign-out (if possible) or clear cookies and try again please

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024 1

Hi @joegithub, the point that you are bringing indeed happens quite often on the development stage. The problem in question is that the browser still holds the user s cookies when you restart your app, so the user is still logged in. However, restarting the app erases the InMemory cache (that is bootstrapped on the login flow) resulting in this sync problem where the user is set on the frontend but not on the backend.

One of the approaches that we have been using is to decorate the endpoint that consumes GetAccessTokenOnBehalfOfUser, with MsalUiRequiredExceptionFilterAttribute passing the scope being used. For example:

[MsalUiRequiredExceptionFilter(Scopes = new[] {"User.Read"})]
public async Task<IActionResult> ShowUserProfile() { ... }

This is not the only (or "the right") solution available and you might need to analyze if it fits your production application. For example, if you need to revoke the user token cache (ie the account got compromised) and force them to login if this happens, the MsalUiRequiredExceptionFilter might not be ideal. But understanding where the problem comes from, help the developers to implement a solution.

About your question on the SQL token cache, keep in mind that it doesn't bullet proof your application from this problem. There is still that chance where your cache table needs to be erased for some reason, or the classic "accidently drop table command being executed in prod" haha =]

Choosing which way to store the cached token requires an analysis of your business needs. Each of them have their pros and cons.

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024 1

Hi @phil-w, if I understood it correctly, the app server is a web api and you consume it signing-in on a client app and sending the bearer token to the REST endpoint. So, this AuthorizeForScopesAttribute is only valid on the client app. It wont work on the web api.

Another point is about your web api consuming another service. We call it on behalf of flow, and it is indeed a little bit complex scenario but a valid one. One thing though, is that you don't cache tokens in the on behalf of flow. If your web api is caching tokens, this will bring you errors (correct me if I am wrong @jmprieur). The web api shouldnt call AcquireTokenSilent but AcquireTokenOnBehalfOf instead.

from active-directory-aspnetcore-webapp-openidconnect-v2.

NikolaosWakem avatar NikolaosWakem commented on August 15, 2024 1

Oh i see, AuthorizeForScopes is a ExceptionFilterAttribute - so i have to throw it out of the action... ;-)

catch (MsalUiRequiredException)
{
throw;
}

Thanks a lot for you help!

from active-directory-aspnetcore-webapp-openidconnect-v2.

NikolaosWakem avatar NikolaosWakem commented on August 15, 2024 1

Yep... I decorated it but it wasn't doing anything as I though it was like AuthoriseAttribure and executed before the method... but it's a ExceptionFilterAttribute .. so i had to specifically throw MsalUiRequiredException outside the action, rather than it being trapped in action and logged. Maybe old name of MsalUiRequiredExceptionFilterAttribute was more clear ;-)

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024

@NabilKem could you please give us more details on how to reproduce this bug?
I have tried it here and I didn't get any error. Please, provide us details like:

  • Step by step to reproduce the bug
  • Is this happening when running locally or on azure?
  • Did you make any customization in the code?

Thanks

from active-directory-aspnetcore-webapp-openidconnect-v2.

NabilKem avatar NabilKem commented on August 15, 2024

Hi @TiagoBrenck,

I just cloned the solution : 5-WebApp-AuthZ/5-1-Roles, then replaced the appsettings with my own Azure AD info :

`{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "mydomain.onmicrosoft.com",
"TenantId": "fb237ab8-......",
"ClientId": "14229dbf-.......",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath ": "/signout-oidc",

// To call an API
"ClientSecret": "wrt6Qstv/K*............"

},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"GraphApiUrl": "https://graph.microsoft.com/beta"
}`

My API permissions : User.Read and User.ReadBasic.All

  • This issue is happening when running the cloned solution locally (web app not hosted in azure)
  • No customization was made to the code

Please advise

Thanks!

from active-directory-aspnetcore-webapp-openidconnect-v2.

antonGritsenko avatar antonGritsenko commented on August 15, 2024

Same story for project 3-WebApp-multi-APIs
Actually, same story for any project that uses Microsoft.Identity.Web and AddAzureAdV2Authentication/AddMsal
And looks like it related to a guest accounts only.
My repro:
I have two tenants: TenantA with my account and TenantB where application registered.

  1. Clone this repo
  2. Change tenant id, domain and client to app from TenantB
  3. Run the 3-WebApp-multi-APIs, login with account from TenantA -> everything good
  4. Navigate to Profile -> BOOM. You see infinite loop of login attempts in the browser and Microsoft.Identity.Client.MsalUiRequiredException: 'No account or login hint was passed to the AcquireTokenSilent call.' in debuger
    Exception in: TokenAcquisition.cs L372

Very similar to #30 but it marked as resolved.

The real issue is in TokenAcquisition.cs L365, it return empty list.

from active-directory-aspnetcore-webapp-openidconnect-v2.

antonGritsenko avatar antonGritsenko commented on August 15, 2024

And just btw, everything work fine with same account\client id\tenant in this sample: https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-openidconnect-aspnetcore

I faced with subject's error when tried to migrate my real app to MSAL v2, then rollback to samples and then found this topic.

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024

@antonGritsenko thanks for giving us more details. I think I could reproduce your bug in another sample (although I am not getting the loop experience) and I will start investigating it.

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024

@antonGritsenko I have opened the issue: AzureAD/microsoft-authentication-library-for-dotnet#1241

Thanks for your contribution.

from active-directory-aspnetcore-webapp-openidconnect-v2.

jmprieur avatar jmprieur commented on August 15, 2024

thanks @TiagoBrenck for this investigation

The regression is really in the sample (in the library),

I've started trying to fix the sample by not using the Account ID provided by MSAL in the token cache arguments (as they are the home account Id, not the guest tenant id).
The branch is https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/jmprieur/fix115

However this does not quite work yet. Indeed, when we call this line:

var application = GetOrBuildConfidentialClientApplication(context.HttpContext, context.Principal);
, the context.Principal contains a user principal with all the claims that we need (in particular iod/tid)

However, when MSAL.NET wants to save the token, here: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/jmprieur/fix115/Microsoft.Identity.Web/Client/TokenCacheProviders/InMemory/MSALPerUserMemoryTokenCacheProvider.cs#L97, the httpContextAccessor (which was normally injected by dependency injection) returns an HttpContext which User property does not have any claims.

@TiagoBrenck if you want to take it from there ... or otherwise I'll continue on Monday
cc: @MarkZuber @kalyankrishna1

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024

@jmprieur I will pull your branch and continue the work.

from active-directory-aspnetcore-webapp-openidconnect-v2.

jmprieur avatar jmprieur commented on August 15, 2024

@TiagoBrenck : can you please review my latest commit to the branch (jmprieur/fix115)
I tested with the guest scenarios (tenantId = a GUID), and the AAD+MSA scenario (tenantId=common), but would like to be sure this does not break anything else.

cc: @bgavrilMS @MarkZuber
cc: @kalyankrishna1

from active-directory-aspnetcore-webapp-openidconnect-v2.

FlorisDevreese avatar FlorisDevreese commented on August 15, 2024

I encountered the same error message:

MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call

In my situation the problem was that in appsettings.json I used:

"GraphApiUrl": "https://graph.microsoft.com/"

instead of

"GraphApiUrl": "https://graph.microsoft.com/beta"

from active-directory-aspnetcore-webapp-openidconnect-v2.

joegithub avatar joegithub commented on August 15, 2024

Hi @joegithub, the point that you are bringing indeed happens quite often on the development stage. The problem in question is that the browser still holds the user s cookies when you restart your app, so the user is still logged in. However, restarting the app erases the InMemory cache (that is bootstrapped on the login flow) resulting in this sync problem where the user is set on the frontend but not on the backend.

One of the approaches that we have been using is to decorate the endpoint that consumes GetAccessTokenOnBehalfOfUser, with MsalUiRequiredExceptionFilterAttribute passing the scope being used. For example:

[MsalUiRequiredExceptionFilter(Scopes = new[] {"User.Read"})]
public async Task<IActionResult> ShowUserProfile() { ... }

This is not the only (or "the right") solution available and you might need to analyze if it fits your production application. For example, if you need to revoke the user token cache (ie the account got compromised) and force them to login if this happens, the MsalUiRequiredExceptionFilter might not be ideal. But understanding where the problem comes from, help the developers to implement a solution.

About your question on the SQL token cache, keep in mind that it doesn't bullet proof your application from this problem. There is still that chance where your cache table needs to be erased for some reason, or the classic "accidently drop table command being executed in prod" haha =]

Choosing which way to store the cached token requires an analysis of your business needs. Each of them have their pros and cons.

Thank you for this information. I attempted to use the MsalUiRequiredExceptionFilter attribute and have not had success yet. My code is calling GetAccessTokenOnBehalfOfUser in the "CreateHttpRequestMessageAsync" method of a "BaseClient" class of an API client project, the scenario being that we use NSwagStudio to generate our API client classes and intercept the http request before they're sent to add the JWT bearer auth token. I currently lack the understanding to know where exactly the exception filter attribute should go, but I think I'm correct that the scope I need to pass is that of the API app registered in our Azure portal.

I became curious why the GetAccessTokenOnBehalfOfUser method in TokenAcquisition failed to get the account when the necessary properties are available in the HttpContext. I went as far as creating my own Account class, inheriting from IAccount, and making a new account object after the call to GetAccountAsync returns null. The necessary values to populate Username, Environment, and create the AccountId class can all be pulled from the User.Identity in the http context. Despite this, when my manually created class is passed to AcquireTokenSilent I get a different error in the exception: "No Refresh Token found in the cache", so I'm probably attempting something that was never intended.

On the sql token caching, I ran into a different issue when trying to run multiple client web projects at the same time. The call to MSALPerUserSqlTokenCacheProvider.ReadCacheForSignedInUser throws a CryptographicException with message "The payload was invalid". This still occurred when I configured each web application to use its own token cache db, which leads me to believe this could be caused by the fact that I'm running both projects locally from Visual Studio instead of accessing them from independently deployed apps running on a server but I haven't tested this yet.

I appreciate your advice and have learned (and continue to learn) a great deal going through the Azure Core OpenId connect samples.

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024

@joegithub we are glad that you are eager to learn more about Azure and OpenID.

The ActionResult that must be decorated with MsalUiRequiredExceptionFilter is the one that will call GetAccessTokenOnBehalfOfUser(...) to attach it on the bearer token.

You shouldn't be creating the IAccount stuff because this is MSAL.NET library's job and there are many rules behind it. The problem that you are facing (and I have faced as well) is that the cache got wiped out, but the user is still signed-in. Another strategy that you might want to do to understand the problem is adding a try catch on your ActionResult and expect the MsalUiRequiredException. When it happens, signs-out the the user and ask it to sign-in again. You will see the method AddAccountToCacheFromAuthorizationCode being triggered and this will lead you to where the cache is populated.

About the SQL errors, I will investigate it further.

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024

@joegithub do you mind opening a separate issue for the SQL error, with some steps to help me reproduce it? Thanks

from active-directory-aspnetcore-webapp-openidconnect-v2.

phil-w avatar phil-w commented on August 15, 2024

...decorate the endpoint with MsalUiRequiredExceptionFilterAttribute passing the scope being used. For example:

[MsalUiRequiredExceptionFilter(Scopes = new[] {"User.Read"})]
public async Task<IActionResult> ShowUserProfile() { ... }

This is not the only (or "the right") solution available and you might need to analyze if it fits your production application.

I understand why this is happening and just want it to stop.

I can find the MsalUiRequired exception in this Identity.Web code, but I can't figure out how to get Visual Studio to accept "MsalUiRequiredExceptionFilter". No doubt it's obvious even to dogs on the street, but what please do I need to include/ install/ incant in order to make that turn the correct colour in my editor?

And if I ignore the exception, is that benign?

from active-directory-aspnetcore-webapp-openidconnect-v2.

jmprieur avatar jmprieur commented on August 15, 2024

where is this documentation, @phil-w? : I think we forgot to update it.
This is now AuthorizeForScopesAttribute. See:
https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/master/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs

from active-directory-aspnetcore-webapp-openidconnect-v2.

phil-w avatar phil-w commented on August 15, 2024

Oh, right, that code I can see. Hmm. My case is slightly complex and hacked from a sample here. I have an app server with a REST end point which I'm hitting from the client. That end point is decorated with that attribute already, and it seems to work in that if I'm not authenticated [with MS] then I can't access the end point etc. It all works beautifully and appears logical and secure, except for this.

That app server in turn makes calls to an internal service which is a wrapper for the Azure FHIR API service (as opposed to the "to do list" service in the example). The problem occurs in that second, internal, call which hits code with the PrepareAuthenticatedClient() stuff in it. The attribute seems to make zero difference if I put it in there..


I get these "AcquireTokenSilent" exceptions for reasons which look like those above: when I rebuild in development, or deploy to live (Azure) when the client already has a cookie/ token etc. That's a problem, as I can't guarantee that all users will have logged out.

It's that PrepareAuthenticatedClient(); which is crashing, inside my [otherwise working fine] service which wraps the Azure FHIR service.

I don't know how to get the VS debugger to step through the Identity.Web code, but I guess I can wrap the PrepareAuthenticatedClient(); with my own exception handling mirroring that in "AuthorizeForScopes", and hack around it that way, if I can work out what it's supposed to do in this case. That is presumably to dump the old cookie and generate a new one.


But perhaps this is a broader problem than "authenticated developers spend a lot of time deleting cookies", because it happens in production also, when updating live systems and users are not all logged out.

from active-directory-aspnetcore-webapp-openidconnect-v2.

phil-w avatar phil-w commented on August 15, 2024

Thanks, as you suggest, I'm using the AuthorizeForScopesAttribute successfully on the client-facing bit of code. I cribbed the "on behalf of flow" code from the example for "todo list", where the crashing line comes from:

var accessToken = await this._tokenAcquisition.GetAccessTokenOnBehalfOfUserAsync(new[] { this._FhirScope });
Where the scope is what you'd expect and correct. That looks like it's correct for this flow, and it works fine, except when the client token is for an old version of the code. I'll look at it some more!

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024

The GetAccessTokenOnBehalfOfUserAsync currently has the implementation below. It tries to find jwt and then performs the proper on behalf of flow. If it doesn't find the jwt, it goes through the acquire silent flow (which is not OBO). Could you set a breakpoint on this IF and check whether it jwtClaim is not null?

Claim jwtClaim = CurrentHttpContext.User.FindFirst("jwt");
if (jwtClaim != null)
{
    (CurrentHttpContext.User.Identity as ClaimsIdentity).RemoveClaim(jwtClaim);
    var result = await application
                    .AcquireTokenOnBehalfOf(scopes.Except(_scopesRequestedByMsalNet), new UserAssertion(jwtClaim.Value))
                    .ExecuteAsync()
                    .ConfigureAwait(false);
    return result.AccessToken;
}
else
{
    return await GetAccessTokenOnBehalfOfUserAsync(application, CurrentHttpContext.User, scopes, tenant).ConfigureAwait(false);
}

from active-directory-aspnetcore-webapp-openidconnect-v2.

phil-w avatar phil-w commented on August 15, 2024

Thanks. The claim comes back null. There's nothing in CurrentHttpContext.User which looks like "jwt", there is stuff including my signed-in user name etc. (0-7 "claims", one identity. So it hits the "else" - GetAccessTokenOnBehalfOfUserAsync.

However if I then delete the cookie, which makes everything work perfectly, the jwt is also null, so the presence of the cookie isn't affecting that "claim" specifically.

from active-directory-aspnetcore-webapp-openidconnect-v2.

phil-w avatar phil-w commented on August 15, 2024

Here's the stack trace. I can follow this through, but it's kind of tricky as it's async and it looks a bit recursive. I guess I need to try to trace the successful flow and parameters, and compare those with what happens when I have re-built the application (which causes the failure), so I can maybe avoid it.

No account or login hint was passed to the AcquireTokenSilent call at Microsoft.Identity.Client.AcquireTokenSilentParameterBuilder.Validate() at Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder1.ValidateAndCalculateApiId() at Microsoft.Identity.Client.AbstractClientAppBaseAcquireTokenParameterBuilder1.ExecuteAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder1.ExecuteAsync() at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenOnBehalfOfUserAsync(IConfidentialClientApplication application, String accountIdentifier, IEnumerable1 scopes, String loginHint, String tenant) in E:\repository\Microsoft.Identity.Web\TokenAcquisition.cs:line 395 at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenOnBehalfOfUserAsync(IConfidentialClientApplication application, ClaimsPrincipal claimsPrincipal, IEnumerable1 scopes, String tenant) in E:\repository\Microsoft.Identity.Web\TokenAcquisition.cs:line 351 at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenOnBehalfOfUserAsync(IEnumerable1 scopes, String tenant) in E:\repository\Microsoft.Identity.Web\TokenAcquisition.cs:line 181 at FloWebApp.Services.FHIRService.PrepareAuthenticatedClient() in E:\repository\WebApp-Flo-API\Client\Services\FHIRService.cs:line 187 at FloWebApp.Services.FHIRService.GetAsync(String action, Int32 items, Int32 pageNum) in E:\repository\WebApp-Flo-API\Client\Services\FHIRService.cs:line 102 at FloWebApp.Controllers.FhirController.List(String verb, Int32 items, Int32 pageNum) in E:\repository\WebApp-Flo-API\Client\Controllers\FhirController.cs:line 51

from active-directory-aspnetcore-webapp-openidconnect-v2.

jmprieur avatar jmprieur commented on August 15, 2024

@phil-w : if this is a web api, you should have a look at this pattern: https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/blob/66786b06dccf32579114738079d41d94ef7a6c99/2.%20Web%20API%20now%20calls%20Microsoft%20Graph/TodoListService/Controllers/TodoListController.cs#L99-L102

from active-directory-aspnetcore-webapp-openidconnect-v2.

phil-w avatar phil-w commented on August 15, 2024

Of course, that's precisely what I need to do! Thanks.

from active-directory-aspnetcore-webapp-openidconnect-v2.

pmeems avatar pmeems commented on August 15, 2024

I'm having this same error. Not sure where to put this try-catch, I'm new to .NET Core and MVC.

from active-directory-aspnetcore-webapp-openidconnect-v2.

JosephAspey avatar JosephAspey commented on August 15, 2024

@pmeems the try-catch above just feels like we are masking the issue. The issue should be fixed in the library. I've encountered far more issues that i'd have like when using MSAL.

from active-directory-aspnetcore-webapp-openidconnect-v2.

NikolaosWakem avatar NikolaosWakem commented on August 15, 2024

I have come back to a project I wrote in xmas… where I was using this code in Home/Index as a massive hack

catch (Microsoft.Identity.Client.MsalUiRequiredException ex)
{
var appBaseUrl = $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}";
var azureLogInUrl = $"{appBaseUrl}/AzureAD/Account/SignIn";
await Request.HttpContext.SignOutAsync();

            return Redirect(azureLogInUrl);
        }

I have updated to latest of everything.. And seem to still have the problem when the cache gets emptied during development or deployment and sadly the hack doesn't seem to work any more as URL is gone.

Are we getting any closer to some demo code that solves this?

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024

Hi @NikolaosWakem

The current solution is by decorating your action result with:

[AuthorizeForScopes(Scopes = new[] { "<TheScopeThatYouArePassingOnAcquireTokenMethod>" })]

The attribute is from Microsoft.Identity.Web nuget package.

An example of its usage in our samples can be found here

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024

Currently we are using this attribute on every action result that calls AcquireTokenSilent method.

If you are not using incremental consent, and your web app uses the same scope on every call, then you could create a global filter for the MSALUIRequiredException. You can check the logic on AuthorizeForScope as a reference.

Keep in mind that MSALUIRequiredException is expected and documented in MSAL. The try/catch strategy is suggested as the best practice when acquiring a token using MSAL.

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024

I am not sure how you are calling AcquireToken without sending any scope, since it is mandatory for the method, so a code sample would be good to give me an idea. But regardless that, then you can definitely create the global filter that I have talked about, and register in the Startup.cs. Then you wont need to treat it in every action result separately.

from active-directory-aspnetcore-webapp-openidconnect-v2.

TiagoBrenck avatar TiagoBrenck commented on August 15, 2024

Not just throw it. It needs to be treated by returning a ChallengeResult.
If you just decorate the action result with the AuthorizeForScopes the exception will be handled for you.

I hope you got your problem solved. Let us know if you need more help

from active-directory-aspnetcore-webapp-openidconnect-v2.

kenchilada avatar kenchilada commented on August 15, 2024

But how do you handle MsalUiRequiredException when you have a web app with no controllers/actions? I am writing a Blazor app and I can't find any way to accomplish incremental consent.

Maybe I should create a global exception filter, but it still does not know what scopes were requested that are missing. Exception only has claims property.

from active-directory-aspnetcore-webapp-openidconnect-v2.

NikolaosWakem avatar NikolaosWakem commented on August 15, 2024

@TiagoBrenck

Seem to be getting the MsalUiRequiredException wrapped in a MicrosoftIdentityWebChallengeUserException since updating to 0.2.1-preview in Action methods... so the

        catch (MsalUiRequiredException)
        {
            throw;
        }
        catch (Exception ex)
        {
           do other stuff

doesn't throw out via AuthorizeForScopes

IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. This has occurred in a Blazor page. You need to use ConsentHandler.HandleException(ex) to challenge the user.

and it's not a Blazor app ;-)

"Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. This has occurred in a Blazor page. You need to use ConsentHandler.HandleException(ex) to challenge the user.
---> MSAL.NetCore.4.16.1.0.MsalUiRequiredException: \n\tErrorCode: user_null\nMicrosoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
at Microsoft.Identity.Client.AcquireTokenSilentParameterBuilder.Validate()
at Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder1.ValidateAndCalculateApiId() at Microsoft.Identity.Client.AbstractClientAppBaseAcquireTokenParameterBuilder1.ExecuteAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder1.ExecuteAsync() at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenOnBehalfOfUserFromCacheAsync(IConfidentialClientApplication application, IAccount account, IEnumerable1 scopes, String tenant, String userFlow)
at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenOnBehalfOfUserFromCacheAsync(IConfidentialClientApplication application, ClaimsPrincipal claimsPrincipal, IEnumerable1 scopes, String tenant, String userFlow) at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable1 scopes, String tenant, String userFlow, ClaimsPrincipal user)\n\tStatusCode: 0 \n\tResponseBody: \n\tHeaders:
--- End of inner exception stack trace ---
at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable`1 scopes, String tenant, String userFlow, ClaimsPrincipal user)

from active-directory-aspnetcore-webapp-openidconnect-v2.

jmprieur avatar jmprieur commented on August 15, 2024

@NikolaosWakem

  • Thanks for reporting. This is a breaking change :-(
    Are you saying it's not handled by the AuthorizeForScopes attributes? (that's strange?, as it does look to the inner exception, which is an MsalUiRequiredException.

Were you calling GetAccessTokenForUserAsync from a controller action? or a Razor page?
Did you read https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access ?

cc: @jennyf19, @TiagoBrenck

from active-directory-aspnetcore-webapp-openidconnect-v2.

NikolaosWakem avatar NikolaosWakem commented on August 15, 2024

Probably right if there is no other exception handling... However the recommended solution if you do have exception handling is to throw any MsalUiRequiredException up to AuthorizeForScopes...
so now you need

            catch (Exception ex)
            {
                if (ex is MsalUiRequiredException || ex.InnerException is MsalUiRequiredException)
                    throw;

                // do other stuff
            }

will switch to using this methodology FindMsalUiRequiredExceptionIfAny

AzureAD/microsoft-identity-web@26e69d3

        public static bool IsMsalUiRequiredException(this Exception exception)
        {
            if (exception is Microsoft.Identity.Client.MsalUiRequiredException)
                return true;
            else if (exception.InnerException != null)
                return IsMsalUiRequiredException(exception.InnerException);
            else
                return false;
        }

from active-directory-aspnetcore-webapp-openidconnect-v2.

jmprieur avatar jmprieur commented on August 15, 2024

Oh I see.
Thanks for the update, @NikolaosWakem
We'll fix this. I was wondering if the new exception shouldn't inherit from MsalUiRequiredException

from active-directory-aspnetcore-webapp-openidconnect-v2.

sprakash10 avatar sprakash10 commented on August 15, 2024

The same issue as @NikolaosWakem, the only difference is, it is a blazor app though. A user signed in, but OnAuthoizationCodeReceived even is not wired to call "AddAccountToCacheFromAuthorizationCodeAsync", since this method isn't exposed via the interface.

Error: Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. This has occurred in a Blazor page. You need to use ConsentHandler.HandleException(ex) to challenge the user.
---> MSAL.NetCore.4.17.1.0.MsalUiRequiredException:
ErrorCode: user_null
Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.

from active-directory-aspnetcore-webapp-openidconnect-v2.

jennyf19 avatar jennyf19 commented on August 15, 2024

@NikolaosWakem @sprakash10 you'll need to handle the challenge, as mentioned in the error. see this wiki page for details. Let us know if this doesn't resolve the issue for you.

from active-directory-aspnetcore-webapp-openidconnect-v2.

teerasej avatar teerasej commented on August 15, 2024

@jennyf19

I created blazor server project from template with

dotnet new blazorserver2 --auth MultiOrg

I tried to follow wiki page for blazor server and checked out these following method:

In startup.cs

services.AddServerSideBlazor()
         .AddMicrosoftIdentityConsentHandler();

I applied Microsoft Graph service with following code in Startup.cs

Please noted that I used CosmosDB Emulator for token caching by using .AddDistributedTokenCaches() instead of .AddInMemoryTokenCaches()to avoid Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                    .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
                    .EnableTokenAcquisitionToCallDownstreamApi()
                    .AddMicrosoftGraph()
                    .AddDistributedTokenCaches()
                    ;

and in page\Counter.razor (link)

Also applied ConsentHandler:

@page "/counter"

@using Microsoft.Identity.Web
@using Microsoft.Graph
@inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler
@inject GraphServiceClient graphClient

<h1>Counter</h1>

<p>Username: @displayName</p>


@code {
    private string displayName;
    protected override async Task OnInitializedAsync()
    {
        try
        {
           
            var me = graphClient.Me.Request().GetAsync().Result;
            displayName = me.DisplayName;
        }
        catch (Exception ex)
        {
            ConsentHandler.HandleException(ex);
        }

    }
}

Still got following this exception:

Error: Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user

Here is my repo:
https://github.com/teerasej/BlazorServer-MSIdentityWeb-pre4-graphclient/tree/use-azure-cosmosDB-emulator-to-cache-token

You need to run CosmosDB Emulator to store the token cache.

from active-directory-aspnetcore-webapp-openidconnect-v2.

manuelsidler avatar manuelsidler commented on August 15, 2024

@andrenpt

We refresh the token when loading the view. It's a nasty workaround and leads to an error if the token expires between loading the view and doing the Ajax call. But in our case it works so far:

[HttpGet]
[AuthorizeForScopes(ScopeKeySection = "Scheduler:SchedulerScope")]
public async Task<IActionResult> Index()
{
    await this.tokenAcquisition.GetAccessTokenForUserAsync(new[] { this.configuration["Scheduler:SchedulerScope"] });
    return this.View("Index");
}

[HttpGet]
public async Task<JsonResult> GetUsers()
{
    // get users via Graph
}

from active-directory-aspnetcore-webapp-openidconnect-v2.

creativebrother avatar creativebrother commented on August 15, 2024

@manuelsidler, @andrenpt Would you just put the Attribute on the GetUsers() Action directly where the exception is thrown so it can be caught by the Attribute?

from active-directory-aspnetcore-webapp-openidconnect-v2.

manuelsidler avatar manuelsidler commented on August 15, 2024

@creativebrother we're using Razor Pages in our application. Therefore I had to apply the attribute on page level since it cannot be applied on handler methods.

from active-directory-aspnetcore-webapp-openidconnect-v2.

creativebrother avatar creativebrother commented on August 15, 2024

@jmprieur One thing I noticed when I get the error "No account or login hint was passed to the AcquireTokenSilent call" is due to the ConfidentialClientApplication.GetAccountAsync(Id) is returning null due to the fact that the cache entry was cleared upon the reading operation of the cache(read triggers cache purge) and that the default slidingexpiration is only 20 min for Distrubuted SQL cache implementation. Once options.DefaultSlidingExpiration is set to over 60 minutes(default access token lifetime) and not cleared by the purge operation(default every 30 min), refresh token seems doing its job now when access token expires.
Before I changed the DefaultSlidingExpiration to like 90 min, when the page refreshes after 20 min of the access token's written to the cache, the cache may be removed due to purge op triggered by the cache read thus cause "No account or login hint" error.

from active-directory-aspnetcore-webapp-openidconnect-v2.

jmprieur avatar jmprieur commented on August 15, 2024

Thanks for the update, @creativebrother
What would you suggest we could do? would changing the documentation to recommend a distributed SQL DefaultSliding operation to over 60 be enough?

from active-directory-aspnetcore-webapp-openidconnect-v2.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.