Azure-Samples / active-directory-aspnetcore-webapp-openidconnect-v2

An ASP.NET Core Web App which lets sign-in users (including in your org, many orgs, orgs + personal accounts, sovereign clouds) and call Web APIs (including Microsoft Graph)
MIT License
1.38k stars 996 forks source link

MsalUIRequiredException: No Account or login hint was passed to the AcquireTokenSilent call #540

Closed Pruzzo closed 2 years ago

Pruzzo commented 3 years ago

Please provide us with the following information:

This issue is for a: (mark with an x)

- [x] bug report -> please search issues before submitting
- [ ] feature request
- [ ] documentation issue or request
- [ ] regression (a behavior that used to work and stopped in a new release)

The issue was found for the following scenario:

Please add an 'x' for the scenario(s) where you found an issue

  1. Web app that signs in users
    1. [x] with a work and school account in your organization: 1-WebApp-OIDC/1-1-MyOrg
    2. [ ] with any work and school account: /1-WebApp-OIDC/1-2-AnyOrg
    3. [ ] with any work or school account or Microsoft personal account: 1-WebApp-OIDC/1-3-AnyOrgOrPersonal
    4. [ ] with users in National or sovereign clouds 1-WebApp-OIDC/1-4-Sovereign
    5. [ ] with B2C users 1-WebApp-OIDC/1-5-B2C
  2. Web app that calls Microsoft Graph
    1. [ ] Calling graph with the Microsoft Graph SDK: 2-WebApp-graph-user/2-1-Call-MSGraph
    2. [x] With specific token caches: 2-WebApp-graph-user/2-2-TokenCache
    3. [ ] Calling Microsoft Graph in national clouds: 2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph
  3. [ ] Web app calling several APIs 3-WebApp-multi-APIs
  4. [ ] Web app calling your own Web API
    1. [ ] with a work and school account in your organization: 4-WebApp-your-API/4-1-MyOrg
    2. [ ] with B2C users: 4-WebApp-your-API/4-2-B2C
    3. [x] with any work and school account: 4-WebApp-your-API/4-3-AnyOrg
  5. Web app restricting users
    1. [ ] by Roles: 5-WebApp-AuthZ/5-1-Roles
    2. [ ] by Groups: 5-WebApp-AuthZ/5-2-Groups
  6. [ ] Deployment to Azure
  7. [ ] Other (please describe)

Repro-ing the issue

Repro steps

I've configured an App registration with delegated permission to create an onlineMeeting (onlineMeetings.ReadWrite). I've configured a .Net Core 5 app to call api in this way `services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "user.read", "onlineMeetings.readWrite" }) .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi")) .AddDistributedTokenCaches();

        //TokenCache
        services.AddDistributedSqlServerCache(options =>
        {
            options.ConnectionString = Configuration.GetConnectionString("DefaultConnection");
            options.SchemaName = "dbo";
            options.TableName = "TokenCache";
            options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
        });`

Try to call the Graph api through await graphServiceClient.Me.OnlineMeetings.Request().AddAsync(onlineMeeting).ConfigureAwait(false);

Expected behavior i am expecting the token to refresh what it's expired

Actual behavior but after token is expired i receive the error MsalUiRequiredException: No Account or login hint was passed to the AcquireSilent call. First login everything works fine and the token is correctly saved on the db

Possible Solution I followed all the threads opened and i've tried to use [AuthorizeForScopes(Scopes = new[] { "<TheScopeThatYouArePassingOnAcquireTokenMethod>" })] found here i've also tried to get the token manually var pca = PublicClientApplicationBuilder.Create(clientId).WithRedirectUri("https://localhost:44341").Build(); but seems that the https doen not work infact i get the error: var pca = PublicClientApplicationBuilder.Create("07ee5b5a-dec2-4904-ae26-b2f2901e2f06").WithRedirectUri("https://localhost:44341").Build();

Additional context/ Error codes / Screenshots

Any log messages given by the failure

Add any other context about the problem here, such as logs. This is the log of the exception: `2021-09-23 09:43:29.0079|1|ERROR|Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware|An unhandled exception has occurred while executing the request. Status Code: 0 Microsoft.Graph.ServiceException: Code: generalException Message: An error occurred sending the request.

---> 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.36.0.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 ---`

OS and Version?

Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?)

Versions

of ASP.NET Core, of MSAL.NET .NET 5.0

Attempting to troubleshooting yourself:

Mention any other details that might be useful


Thanks! We'll be in touch soon.

jmprieur commented 3 years ago

@Pruzzo do you get this exception on the debugger and the app re-signs-in the user? or does it just crash?

Pruzzo commented 3 years ago

I get the error in the debugger, if i logout and login again it works fine but the exception is just thrown, nothing else

jmprieur commented 3 years ago

But when you continue the execution on the debugger, you are automatically re-signed-in? aren't you?

Pruzzo commented 3 years ago

No i am not re-signed in automatically :)

jmprieur commented 3 years ago

you mean the application crashes? you don't see the sign-in dialog?

Pruzzo commented 3 years ago

No, when this error comes i do not see the signin dialog

jmprieur commented 3 years ago

I mean when you continue. I'm trying to understand if the app crashes? The fact that there is an exception is expected/normal,. But it should be filtered by [AuthorizeForScopes]

jmprieur commented 3 years ago

I'm assuming you have [AuthorizeForScopes(new string[] { "user.read", "onlineMeetings.readWrite" }] ?

Pruzzo commented 3 years ago

Yes, that is what i have. [AuthorizeForScopes(Scopes = new string[] { "user.read", "onlineMeetings.readWrite" })] I've also followed the idea of @TiagoBrenck here and i've seen that the attribute is an exception filter but the exception thrown is a lever lower, so i copied the code and i wrote mine to catch the MsalUiRequiredException

` MsalUiRequiredException msalUiRequiredException = context.Exception as MsalUiRequiredException; if (msalUiRequiredException == null) { msalUiRequiredException = context.Exception?.InnerException as MsalUiRequiredException; if (msalUiRequiredException == null) msalUiRequiredException = context.Exception?.InnerException?.InnerException as MsalUiRequiredException;

        }`

this works and msalUiRequiredException is not null but i did not manage to make the filter work properly (an exception is thrown on properties.SetParameter<ICollection<string>> [Object cannot be null ] )

`public class MsalUIExceptionFilter : ExceptionFilterAttribute { ///

/// Scopes to request /// public string[] Scopes { get; set; }

    /// <summary>
    /// Key section on the configuration file that holds the scope value
    /// </summary>
    public string ScopeKeySection { get; set; }

    /// <summary>
    /// Handles the MsaUiRequiredExeception
    /// </summary>
    /// <param name="context">Context provided by ASP.NET Core</param>
    public override void OnException(ExceptionContext context)
    {
        MsalUiRequiredException msalUiRequiredException = context.Exception as MsalUiRequiredException;
        if (msalUiRequiredException == null)
        {
            msalUiRequiredException = context.Exception?.InnerException as MsalUiRequiredException;
            if (msalUiRequiredException == null)
                msalUiRequiredException = context.Exception?.InnerException?.InnerException as MsalUiRequiredException;

        }

        if (msalUiRequiredException != null)
        {
            if (CanBeSolvedByReSignInUser(msalUiRequiredException))
            {
                // the users cannot provide both scopes and ScopeKeySection at the same time
                if (!string.IsNullOrWhiteSpace(ScopeKeySection) && Scopes != null && Scopes.Length > 0)
                {
                    throw new InvalidOperationException($"Either provide the '{nameof(ScopeKeySection)}' or the '{nameof(Scopes)}' to the 'AuthorizeForScopes'.");
                }

                // If the user wishes us to pick the Scopes from a particular config setting.
                if (!string.IsNullOrWhiteSpace(ScopeKeySection))
                {
                    // Load the injected IConfiguration
                    IConfiguration configuration = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();

                    if (configuration == null)
                    {
                        throw new InvalidOperationException($"The {nameof(ScopeKeySection)} is provided but the IConfiguration instance is not present in the services collection");
                    }

                    Scopes = new string[] { "onlineMeetings.readWrite" };
                }

                var properties = BuildAuthenticationPropertiesForIncrementalConsent(Scopes, msalUiRequiredException, context.HttpContext as HttpContext);
                context.Result = new ChallengeResult(properties);
            }
        }

        base.OnException(context);
    }

    private bool CanBeSolvedByReSignInUser(MsalException ex)
    {
        // ex.ErrorCode != MsalUiRequiredException.UserNullError indicates a cache problem.
        // When calling an [Authenticate]-decorated controller we expect an authenticated
        // user and therefore its account should be in the cache. However in the case of an
        // InMemoryCache, the cache could be empty if the server was restarted. This is why
        // the null_user exception is thrown.

        return (ex.ErrorCode.Contains(MsalError.UserNullError) || ex.ErrorCode.Contains(MsalError.InvalidGrantError));
    }

    /// <summary>
    /// Build Authentication properties needed for an incremental consent.
    /// </summary>
    /// <param name="scopes">Scopes to request</param>
    /// <param name="ex">MsalUiRequiredException instance</param>
    /// <param name="context">current http context in the pipeline</param>
    /// <returns>AuthenticationProperties</returns>
    private AuthenticationProperties BuildAuthenticationPropertiesForIncrementalConsent(
        string[] scopes, MsalUiRequiredException ex, HttpContext context)
    {
        var properties = new AuthenticationProperties();

        // Set the scopes, including the scopes that ADAL.NET / MASL.NET need for the Token cache
        //string[] additionalBuildInScopes = {OidcConstants.StandardScopes.OpenId, OidcConstants.StandardScopes.OfflineAccess, OidcConstants.StandardScopes.Profile};

        //properties.SetParameter<ICollection<string>>(OpenIdConnectParameterNames.Scope,scopes.Union(additionalBuildInScopes).ToList());

        // Attempts to set the login_hint to avoid the logged-in user to be presented with an account selection dialog
        var loginHint = context.User.GetLoginHint();
        if (!string.IsNullOrWhiteSpace(loginHint))
        {
            properties.SetParameter(OpenIdConnectParameterNames.LoginHint, loginHint);

            var domainHint = context.User.GetDomainHint();
            properties.SetParameter(OpenIdConnectParameterNames.DomainHint, domainHint);
        }

        // Additional claims required (for instance MFA)
        if (!string.IsNullOrEmpty(ex.Claims))
        {
            //properties.Items.Add(OidcConstants. AdditionalClaims, ex.Claims);
        }

        return properties;
    }
}`
aremo-ms commented 2 years ago

Hello @Pruzzo I'm so sorry to start with the issue few months later. I'm on it now Can you please kindly update if you still need help?

Pruzzo commented 2 years ago

Hi @aremo-ms, it was necessary to me to carry out with the job and so i found a walkaround, setting a control that simpy tries to use the token and if it gives an exception the user is logged out forcing the login again.

aremo-ms commented 2 years ago

Hi @aremo-ms, it was necessary to me to carry out with the job and so i found a walkaround, setting a control that simpy tries to use the token and if it gives an exception the user is logged out forcing the login again.

@Pruzzo If you had to find workaround, then something is wrong or you have a special case. I don't remember any issue in our samples. Do you think that the sample is incorrect?

Pruzzo commented 2 years ago

I was not reffering at the sample, my case is that i was just expection the token to auto refresh using the AcquireTokenSilent but it seems to not be working. Honestly i do not remember precisely all the steps i've followed. In principle i remeber that i was not able to acquire silently the token in order to auto refresh it when expires.

aremo-ms commented 2 years ago

@Pruzzo So you've tried to make something like this and it didn't work?

aremo-ms commented 2 years ago

Closing this issue after 14 days without response

Dupie123 commented 2 years ago

Hi, ive been having the same issue. I am currently building a .net core mvc application and I keep getting the "no account or login hint was passed to the acquiretokensilent call" error. I have tried it with the same code as provide here as well as using the GraphServiceClient provided by the controller. It works on initial loading of the page, but once the page is reloaded the error reappears. It looks asif the cache gets cleared, but i have no way of confirming this

first code section : try { var token = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "User.Read", "User.Read.All", "User.ReadBasic.All", "Tasks.Read", "Tasks.Read.Shared", "Tasks.ReadWrite", "Tasks.ReadWrite.Shared", "Group.ReadWrite.All" });

GraphServiceClient graphServiceClient = new GraphServiceClient("https://graph.microsoft.com/v1.0", new DelegateAuthenticationProvider(request => { request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", token); return Task.CompletedTask; }));

User user = await graphServiceClient.Me.Request().GetAsync(); } catch { }

second code section: User user = await _graphServiceClient.Me.Request().GetAsync();

and the error -> MsalUiRequiredException : No account or login hint was passed to the AcquireTokenSilent call.

Pruzzo commented 2 years ago

If it could help you i found this walkaround. try { var taRes = await tokenAcquisition.GetAuthenticationResultForUserAsync(new List<string> { "user.read", "onlinemeetings.readwrite" }, "common", null, User, new TokenAcquisitionOptions() { ForceRefresh = true }).ConfigureAwait(false); } catch { foreach (var cookie in Request.Cookies) { Response.Cookies.Delete(cookie.Key); } return BadRequest(new ErrorResult("session_expired")); }

This logs out the user forcing to login again

crobbo commented 2 years ago

I have the same issue...

I set up graph client in the startup class like below. I thought adding AddSessionTokenCaches() would allow the tokens to persist in a user session even if the server restarts. However, it only works whilst the server is online for the first time - if I restart the server then the access tokens no longer work. I have to clear my cookies to force the app to reauthenticate and get a new access token which then works.

      public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                    .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
                    .EnableTokenAcquisitionToCallDownstreamApi()
                    .AddMicrosoftGraph(Configuration.GetSection("GraphApi"))
                    .AddSessionTokenCaches();
 }

I have the below code in a Graph API service method like so...

DriveItem = await graphserviceClient
                    .Sites[{siteId}]
                    .Drive
                    .Items[{itemId}]
                    .Children
                    .Request()
                    .AddResponseAsync(driveItem);

@Pruzzo So you've tried to make something like this and it didn't work?

Is there a way to use the example you have said, in my graph API service, to make sure a new access token is passed in each time I call the graph api on behalf of a user?

aremo-ms commented 2 years ago

Hello @crobbo You can refer to this page for detailed explanations. The session token cache you've chose to use is for a memory cache, The memory exists only while server is on. If you need to persist the token, then use SQL server option. In case you'd like to get a fresh token every time you call Graph, then you should consider calling AcquireTokenSilent method. See example here for public and confidential applications

crobbo commented 2 years ago

Thanks, I'll take a look at the example. My workaround has been to add the AuthorizeForScopes attribute to my controller and this seems to work (I think it calls AquireTokenSilent in the background which is needed as our tokens were setup to expire after 15 or 30mins)

Its weird as in a development environment the AuthorizeForScopes throws an exception and causes the debugger to pause the code. Whereas once the app is deployed it works fine. Not really sure what's going on but it doesn't feel like this should be throwing any exceptions.

I'll have a read of the example to see if I can maybe refactor my code to call the AquireTokenSilent manually, rather than relying on the AuthorizeForScopes attribute.

jmprieur commented 2 years ago

Thanks, I'll take a look at the example. My workaround has been to add the AuthorizeForScopes attribute to my controller and this seems to work (I think it calls AquireTokenSilent in the background which is needed as our tokens were setup to expire after 15 or 30mins)

This is not a workaround, @crobbo. There can be exceptions anyway in some scenarios, so it's better for the attribute (which is an ExceptionFilter) to process them. This is the way to do. See https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access

crobbo commented 2 years ago

Does [AuthorizeForScopes(Scopes = new[] { "{scopes}" })] not process and handle MsalUiRequiredException?

As mentioned, since I added AthourizeForScopes to my controller it works when deployed, the app does not crash due to the exception, I get a new user token and the user is able to access SharePoint via the app as intended. The exception only forces the code to stop in development locally and once I continue through the exception the app works as intended...

I followed your guide adding the extra code to the startup class and Micrsoft.Web.Indentity.UI. I also manually called tokenAquisition.GetAccessTokenForUserAsync({scopes}) and when I do this I get exactly the same exception, `MsalUiRequiredException - no account or login was passed to AquireTokenSilent.

Ihink I'll try the clearing cookie method next...

jmprieur commented 2 years ago

@crobbo : yes it does. That's what exception filters do.

abhiphirke commented 1 year ago

So, is there any solution to this problem? or any workaround at least?? I too am stuck here...

nicholusi2021 commented 1 year ago

I am also stuck, I've spent so many hours trying to find a workaround. The workaround listed here doesn't work for me.

StevTechy commented 1 year ago

Closing this issue after 14 days without response

So you take several months to respond to the issue, then close the issue just 2 weeks of not getting a response when there is clearly an issue

I'm going through this now and still not seeing a straightforward solution

crobbo commented 1 year ago

Have you all tried adding the AuthorizeForScopes to the controller?

It worked for me but as I wrote above it raises an exception, not ideal, but the app will still work in production. In local development environment just get Visual studio to ignore to the exception too.

nicholusi2021 commented 1 year ago

I am yes, I'm using....

[Authorize(AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] [AuthorizeForScopes(Scopes = new string[] { "user.read" })]

The issue is that it works fine if the user doesn't have a token, it forces the login. However if the user has a token but it's expired it does not force the login and the code in the method continues. When I then try to get graph user information, I get the error posted in this issue.

So I'm in a state where it won't force the user to re-login and I can't get the user information.

StevTechy commented 1 year ago

I am yes, I'm using....

[Authorize(AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] [AuthorizeForScopes(Scopes = new string[] { "user.read" })]

The issue is that it works fine if the user doesn't have a token, it forces the login. However if the user has a token but it's expired it does not force the login and the code in the method continues. When I then try to get graph user information, I get the error posted in this issue.

So I'm in a state where it won't force the user to re-login and I can't get the user information.

In addition to this, this solution only works when you hit the controller, I want to refresh the token in Blazor UI before calling web API

jmprieur commented 1 year ago

@StevTechy: doesn't this work in your blazor page? https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access#in-the-blazor-page-itself

Bidthedog commented 1 year ago

I suggest you re-open this @aremo-ms. We see this issue occurring in our Blazor Server app regularly. The issue goes away if we clear the browser cookies, but persists until we do. This is not a viable solution for large-scale applications with thousands of users.

I have set up everything as per the documentation as far as I can tell. AAD redirect works fine for the most part, but at some point - I think when a cached token expires and / or does not match the cookie, the call to MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException(ex) stops performing the AAD redirect. The code in question is as follows:

try
            {
                // Always call GetAccessTokenForUserAsync so a new token is provided if the old token has expired.

                // Note that this call can fail / halt silently without raising an exception
                // and code will stop executing. This only happens if this method is called synchronously.
                // See the below link for more info:
                // https://github.com/AzureAD/microsoft-identity-web/issues/1801#issuecomment-1182986661
                var token = await _tokenAcquisition
                    .GetAccessTokenForUserAsync(new[]
                    {
                        _options.DownstreamAPIAccessTokenScope
                    });
                _tokenProvider.AccessToken = token;
                _tokenProvider.Scheme = JwtBearerDefaults.AuthenticationScheme;
            } catch(MicrosoftIdentityWebChallengeUserException ex)
            {
                // The below line will redirect the user to AAD (for consent and / or to get a new / existing access token)
                // if they do not have an active access token for the API with the specified scope.
                _consentHandler.HandleException(ex);
                return null;
            }

We are using SQL Server caching, and need to handle everything on the blazor-server side; we cannot apply attributes to the downstream API, because the same software is used with different authorisation mechanisms depending on the deployment. The auth-related setup is as follows:

        public static IServiceCollection AddClientAzureADAuth(this IServiceCollection services, IConfiguration config)
        {
            // AAD specific services
            services.AddScoped<IClientIdentityService, AzureADIdentityService>();
            services.AddScoped<IAPITokenService, AzureAADTokenService>();

            var initialScopes = config
                .GetValue<string>(ClientAuthConstants.DownstreamAPIScopesConfigKeyName)?
                .Split(' ');

            services
                .Configure<AzureAADBlazorOptions>(config.GetSection(ClientConfigurationConstants.AzureADSectionName));

            services
                .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApp(config.GetSection(ClientAuthConstants.AzureADConfigSectionName))
                .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                .AddDownstreamWebApi(ClientAuthConstants.APIName, config.GetSection(ClientConfigurationConstants.WebAPISectionName))
                // AddInMemoryTokenCaches() is good for testing ang development, but a distributed
                // token cache is more suitable for production
                // https://docs.microsoft.com/en-gb/azure/active-directory/develop/msal-net-token-cache-serialization?tabs=aspnet
                // .AddInMemoryTokenCaches();
                .AddDistributedTokenCaches();

            // Add distributed cache options
            services.AddDistributedSqlServerCache(options =>
            {
                var cacheConnectionString = config.GetValue<string>(ClientAuthConstants.AzureADTokenCacheConnectionStringKey);
                options.ConnectionString = cacheConnectionString;
                options.SchemaName = "dbo";
                options.TableName = "TokenCache";

                // You don't want the SQL token cache to be purged before the access token has expired. Usually
                // access tokens expire after 1 hour (but this can be changed by token lifetime policies), whereas
                // the default sliding expiration for the distributed SQL database is 20 mins. 
                // Use a value which is above 60 mins (or the lifetime of a token in case of longer lived tokens)
                options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
            });

            services
                .AddControllersWithViews()
                .AddMicrosoftIdentityUI();

            // Custom Auth handler
            services.AddScoped<IAuthorizationHandler, AzureADUserRequirementAuthorizationHandler>();

            services
                .AddAuthorization(options =>
                {
                    options.AddPolicy(PolicyNames.UserPolicy,
                        policy =>
                        {
                            var aadOptions = config
                                .GetSection(ClientAuthConstants.AzureADConfigSectionName)
                                .Get<AzureAADBlazorOptions>();
                            policy.RequireAuthenticatedUser(); // Adds DenyAnonymousAuthorizationRequirement

                            // Some claims are required by the app
                            policy.RequireClaim(ClaimConstants.PreferredUserName);
                            policy.RequireClaim(ClaimConstants.ObjectId);
                            policy.RequireClaim(ClaimConstants.TenantId, aadOptions.TenantId);
                            if(aadOptions.RequiresAppRole)
                            {
                                policy.RequireClaim(ClaimConstants.Role, aadOptions.RequiredRole);
                            }

                            // User must be a  user
                            policy.AddRequirements(new AzureADUserRequirement());
                        });

                    // Set the default auth policy so that the entire app uses the same one
                    var UserPolicy = options.GetPolicy(PolicyNames.UserPolicy);
                    options.DefaultPolicy = UserPolicy ?? options.DefaultPolicy;
                });

            services.AddMicrosoftIdentityConsentHandler();

            return services;
        }

I have updated to the latest assemblies, but still get the same issue. The only way I can see to fix it at present is to somehow know that HandleException hasn't redirected, delete the client cookies, then retry... but as we know, the first two steps are not possible from the client.

I have enabled verbose logging for MSAL, which produces the following output when attempting to connect to the app (sanitised):

31/03/2023 11:01:34.170 +01:00 [INF] Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager | User profile is available. Using 'C:\Users\ME\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest. | {"EventId":{"Id":63,"Name":"UsingProfileAsKeyRepositoryWithDPAPI"}}
31/03/2023 11:01:35.701 +01:00 [INF] Microsoft.Hosting.Lifetime | Now listening on: https://[::]:12343 | {"EventId":{"Id":14,"Name":"ListeningOnAddress"}}
31/03/2023 11:01:35.705 +01:00 [INF] Microsoft.Hosting.Lifetime | Now listening on: https://localhost:12343 | {"EventId":{"Id":14,"Name":"ListeningOnAddress"}}
31/03/2023 11:01:35.712 +01:00 [INF] Microsoft.Hosting.Lifetime | Application started. Press Ctrl+C to shut down. | {}
31/03/2023 11:01:35.737 +01:00 [INF] Microsoft.Hosting.Lifetime | Hosting environment: Development | {}
31/03/2023 11:01:35.740 +01:00 [INF] Microsoft.Hosting.Lifetime | Content root path: D:\Git\Clients\COMPANYNAME\AzureRepos\COMPANYNAME.APPNAME\src-BlazorApp\COMPANYNAME.APPNAME.Web.App | {}
31/03/2023 11:01:38.429 +01:00 [INF] Microsoft.AspNetCore.Hosting.Diagnostics | Request starting HTTP/2 GET https://localhost:12343/ - - | {"Protocol":"HTTP/2","Method":"GET","ContentType":null,"ContentLength":null,"Scheme":"https","Host":"localhost:12343","PathBase":"","Path":"/","QueryString":"","EventId":{"Id":1},"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:38.790 +01:00 [DBG] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:38Z] ConfidentialClientApplication 38350037 created | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:38.834 +01:00 [DBG] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:38Z - 6ceb106f-6438-421e-9f0a-ad51ff4577a0] [Cache Session Manager] Entering the cache semaphore. Real semaphore: False. Count: 1 | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:38.835 +01:00 [DBG] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:38Z - 6ceb106f-6438-421e-9f0a-ad51ff4577a0] [Cache Session Manager] Entered cache semaphore | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:38.844 +01:00 [DBG] Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapter | [MsIdWeb] MemoryCache: Read cacheKey 7e20173d-1a94-4a6d-a491-f6d2e4580b37.a8a2a680-e1fa-4c90-9702-31d178ccc348 cache size 0  | {"EventId":{"Id":106,"Name":"MemoryCacheRead"},"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.162 +01:00 [DBG] Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapter | [MsIdWeb] DistributedCache: Read cacheKey 7e20173d-1a94-4a6d-a491-f6d2e4580b37.a8a2a680-e1fa-4c90-9702-31d178ccc348 cache size 0 InRetry? false  | {"EventId":{"Id":100,"Name":"DistributedCacheState"},"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.167 +01:00 [DBG] Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapter | [MsIdWeb] DistributedCache: Read Time in MilliSeconds 317.6345  | {"EventId":{"Id":102,"Name":"DistributedCacheReadTime"},"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.170 +01:00 [DBG] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 6ceb106f-6438-421e-9f0a-ad51ff4577a0] [Cache Session Manager] Released cache semaphore | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.178 +01:00 [DBG] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 6ceb106f-6438-421e-9f0a-ad51ff4577a0] GetAccounts found 0 RTs and 0 accounts in MSAL cache.  | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.180 +01:00 [INF] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 6ceb106f-6438-421e-9f0a-ad51ff4577a0] [Region discovery] Not using a regional authority.  | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.182 +01:00 [DBG] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 6ceb106f-6438-421e-9f0a-ad51ff4577a0] [Instance Discovery] Tried to use network cache provider for login.microsoftonline.com. Success? False.  | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.183 +01:00 [DBG] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 6ceb106f-6438-421e-9f0a-ad51ff4577a0] [Instance Discovery] Tried to use known metadata provider for login.microsoftonline.com. Success? True.  | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.183 +01:00 [DBG] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 6ceb106f-6438-421e-9f0a-ad51ff4577a0] GetAccounts found 0 RTs and 0 accounts in MSAL cache after environment filtering.  | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.183 +01:00 [DBG] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 6ceb106f-6438-421e-9f0a-ad51ff4577a0] Filtered by home account id. Remaining accounts 0  | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.184 +01:00 [INF] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z] Found 0 cache accounts and 0 broker accounts | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.188 +01:00 [INF] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z] Returning 0 accounts | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.193 +01:00 [INF] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 0a1f588b-7b7d-4663-88c4-59d594dee6c3] MSAL MSAL.NetCore with assembly version '4.46.0.0'. CorrelationId(0a1f588b-7b7d-4663-88c4-59d594dee6c3) | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.196 +01:00 [INF] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 0a1f588b-7b7d-4663-88c4-59d594dee6c3] === AcquireTokenSilent Parameters === | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.198 +01:00 [INF] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 0a1f588b-7b7d-4663-88c4-59d594dee6c3] LoginHint provided: False | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.204 +01:00 [INF] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 0a1f588b-7b7d-4663-88c4-59d594dee6c3] Account provided: False | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.207 +01:00 [INF] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 0a1f588b-7b7d-4663-88c4-59d594dee6c3] ForceRefresh: False | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.213 +01:00 [INF] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 0a1f588b-7b7d-4663-88c4-59d594dee6c3] 
=== Request Data ===
Authority Provided? - True
Scopes - api://COMPANYNAME.com/.default
Extra Query Params Keys (space separated) - 
ApiId - AcquireTokenSilent
IsConfidentialClient - True
SendX5C - False
LoginHint ? False
IsBrokerConfigured - False
HomeAccountId - False
CorrelationId - 0a1f588b-7b7d-4663-88c4-59d594dee6c3
UserAssertion set: False
LongRunningOboCacheKey set: False
Region configured: 
 | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.216 +01:00 [INF] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 0a1f588b-7b7d-4663-88c4-59d594dee6c3] === Token Acquisition (SilentRequest) started:
     Scopes: api://COMPANYNAME.com/.default
    Authority Host: login.microsoftonline.com | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.223 +01:00 [DBG] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 0a1f588b-7b7d-4663-88c4-59d594dee6c3] No account passed to AcquireTokenSilent.  | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.223 +01:00 [DBG] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 0a1f588b-7b7d-4663-88c4-59d594dee6c3] Token cache could not satisfy silent request. | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.234 +01:00 [ERR] Microsoft.Identity.Web.TokenAcquisition | False MSAL 4.46.0.0 MSAL.NetCore .NET 6.0.15 Microsoft Windows 10.0.22621 [2023-03-31 10:01:39Z - 0a1f588b-7b7d-4663-88c4-59d594dee6c3] Exception type: Microsoft.Identity.Client.MsalUiRequiredException
, ErrorCode: user_null
HTTP StatusCode 0
CorrelationId 

   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) | {"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}
31/03/2023 11:01:39.242 +01:00 [INF] Microsoft.Identity.Web.TokenAcquisition | [MsIdWeb] An error occured during token acquisition: No account or login hint was passed to the AcquireTokenSilent call.  | {"EventId":{"Id":300,"Name":"TokenAcquisitionError"},"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K","ExceptionDetail":{"HResult":-2146233088,"Message":"No account or login hint was passed to the AcquireTokenSilent call. ","Source":"Microsoft.Identity.Client","TargetSite":"Void MoveNext()","Classification":"AcquireTokenSilentFailed","StatusCode":0,"Claims":null,"ResponseBody":null,"Headers":null,"CorrelationId":null,"IsRetryable":false,"ErrorCode":"user_null","Type":"Microsoft.Identity.Client.MsalUiRequiredException"}}
MSAL.NetCore.4.46.0.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, IEnumerable`1 scopes, String tenantId, MergedOptions mergedOptions, String userFlow, TokenAcquisitionOptions tokenAcquisitionOptions)
   at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable`1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)
    StatusCode: 0 
    ResponseBody:  
    Headers: 
31/03/2023 11:01:39.277 +01:00 [ERR] Microsoft.AspNetCore.Server.Kestrel | Connection id "0HMPHPRLKLS6K", Request id "0HMPHPRLKLS6K:00000001": An unhandled exception was thrown by the application. | {"EventId":{"Id":13,"Name":"ApplicationError"},"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ExceptionDetail":{"Type":"System.NullReferenceException","HResult":-2147467261,"Message":"Object reference not set to an instance of an object.","Source":"COMPANYNAME.APPNAME.Core.Client.Model.APPNAMEWebAPI","TargetSite":"Void MoveNext()"}}
System.NullReferenceException: Object reference not set to an instance of an object.
   at COMPANYNAME.APPNAME.Core.Client.Model.APPNAMEWebAPI.Client.APPNAMEWebAPIClient.GetAPPNAMEIdentityAsync(APPNAMEIdentityRequest req) in D:\Git\Clients\COMPANYNAME\AzureRepos\COMPANYNAME.APPNAME\src-APPNAMEClient\COMPANYNAME.APPNAME.Core.Client.Model.APPNAMEWebAPI\Client\APPNAMEWebAPIClient.cs:line 168
   at COMPANYNAME.APPNAME.Core.Client.Auth.AzureAD.AzureADAPPNAMEUserRequirementAuthorizationHandler.HandleRequirementAsync(AuthorizationHandlerContext context, AzureADAPPNAMEUserRequirement requirement) in D:\Git\Clients\COMPANYNAME\AzureRepos\COMPANYNAME.APPNAME\src-BlazorApp\COMPANYNAME.APPNAME.Core.Client.Auth\AzureAD\AzureADAPPNAMEUserRequirementAuthorizationHandler.cs:line 35
   at Microsoft.AspNetCore.Authorization.AuthorizationHandler`1.HandleAsync(AuthorizationHandlerContext context)
   at Microsoft.AspNetCore.Authorization.DefaultAuthorizationService.AuthorizeAsync(ClaimsPrincipal user, Object resource, IEnumerable`1 requirements)
   at Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator.AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, Object resource)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at COMPANYNAME.APPNAME.Web.App.APPNAMERequestLocalizationCookiesMiddleware.InvokeAsync(HttpContext context, RequestDelegate next) in D:\Git\Clients\COMPANYNAME\AzureRepos\COMPANYNAME.APPNAME\src-BlazorApp\COMPANYNAME.APPNAME.Web.App\APPNAMERequestLocalizationCookiesMiddleware.cs:line 47
   at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.<>c__DisplayClass6_1.<<UseMiddlewareInterface>b__1>d.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware.InvokeAsync(HttpContext context)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
31/03/2023 11:01:39.319 +01:00 [INF] Microsoft.AspNetCore.Hosting.Diagnostics | Request finished HTTP/2 GET https://localhost:12343/ - - - 500 0 - 892.4113ms | {"ElapsedMilliseconds":892.4113,"StatusCode":500,"ContentType":null,"ContentLength":0,"Protocol":"HTTP/2","Method":"GET","Scheme":"https","Host":"localhost:12343","PathBase":"","Path":"/","QueryString":"","EventId":{"Id":2},"RequestId":"0HMPHPRLKLS6K:00000001","RequestPath":"/","ConnectionId":"0HMPHPRLKLS6K"}

The last error is because the null that is returned by our wrapper (the first bit of pasted code) is not ever expected to be used by the calling code... because the blazor app should have redirected by then.

This is difficult to replicate, I am only able to provide this right now because my workstation is in the correct state.

Bidthedog commented 1 year ago

Further information: as soon as a clear the .AspNetCore.Cookies cookie, the redirect works. If I then replace the value of the .AspNetCore.Cookies cookie with the OLD cookie value, it continues to work, so I expect there is a conflict somewhere with the value cached in the Token cache database and the cookie value.

bmackeydhcs commented 1 year ago

This is happening for me each time I relaunch visual studio. The cookie is bound to the session. So to counter-act this problem I create a GUID with each session and name the cookie after the GUID. That way the cookie is bound to the session also.

builder.Services.Configure<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
    options.Cookie.Name = Guid.NewGuid().ToString();
    options.Cookie.IsEssential = false;
});
LinQiaoPorco commented 8 months ago

Same error on my project, which is using .Net 6 Blazor Server.

builder.Services
    // Add support for OpenId authentication
    .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    // Microsoft identity platform web app that requires an auth code flow
    .AddMicrosoftIdentityWebApp(options =>
    {
        builder.Configuration.Bind("AzureAd", options);
        options.Prompt = "select_account";
        options.Events.OnTokenValidated = async context =>
        {
            var tokenAcquisition = context.HttpContext.RequestServices
                .GetRequiredService<ITokenAcquisition>();
            var logger = context.HttpContext.RequestServices
                .GetRequiredService<ILogger<IStartup>>();
            var token = await tokenAcquisition
                .GetAccessTokenForUserAsync(initialScopes!, user: context.Principal);
            var graphClient = new GraphServiceClient(
                new DelegateAuthenticationProvider(async (request) =>
                {
                    await Task.Run(() =>
                    {
                        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
                    });
                })
            );
        };
    })

    // Add ability to call Microsoft Graph APIs with specific permissions
    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)

    // Enable dependency injection for GraphServiceClient
    .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))

    // Add token cache
    // .AddInMemoryTokenCaches();
    .AddDistributedTokenCaches();

It would worked when user login for the first time, then it will fail with error:user_null when page reloaded.

I guess the login action triggered the method to add user token in request, and fail when page reload does not add the token back into request header:

options.Events.OnTokenValidated = async context =>
        {
            var tokenAcquisition = context.HttpContext.RequestServices
                .GetRequiredService<ITokenAcquisition>();
            var logger = context.HttpContext.RequestServices
                .GetRequiredService<ILogger<IStartup>>();
            var token = await tokenAcquisition
                .GetAccessTokenForUserAsync(initialScopes!, user: context.Principal);
            var graphClient = new GraphServiceClient(
                new DelegateAuthenticationProvider(async (request) =>
                {
                    await Task.Run(() =>
                    {
                        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
                    });
                })
            );
        };

So, can we have some configuration in the method .AddMicrosoftGraph() to have the GraphServiceClient always have the user token in the request header?

iot-crazy commented 8 months ago

I have the same problem using he sample code. The exception occurs only when calling the second line of the code below.

var client =  this.GetGraphServiceClient();
var me = await client.Me.GetAsync();'
paritoshromy commented 1 month ago

This needs a proper solution for Blazor rather than a workaround. Still a pain to handle.

dany28 commented 3 weeks ago

For Blazor (net9) adding:

builder.Services.AddMicrosoftIdentityConsentHandler();

did a job.

var user = await graph.Me.GetAsync();

started working on component