blazor.net-8.0microsoft-entra-idmsalmicrosoft.identity.web

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


I have a .NET 8 Blazor Web App (Server) application that I have intergrated with Microsoft.Identity.Web (Entra ID). I can authenticate just fine, but while trying to implement the retrieval of an Access Token I am receiving MSAL errors.

I am using Token Caching (Session) and initially testing the retrieval of the token in the OnInitializedAsync of a page. Here's the thing, if I were to clear the cookies and cache of the local web application, it runs fine. As soon as I restart the application from the IDE, it produces the error:

MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken)
Microsoft.Identity.Client.Internal.Requests.RequestBase+<>c__DisplayClass11_1+<<RunAsync>b__1>d.MoveNext()
Microsoft.Identity.Client.Utils.StopwatchService.MeasureCodeBlockAsync(Func<Task> codeBlock)
Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken)
Microsoft.Identity.Client.ApiConfig.Executors.ClientApplicationBaseExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenSilentParameters silentParameters, CancellationToken cancellationToken)
Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForWebAppWithAccountFromCacheAsync(IConfidentialClientApplication application, ClaimsPrincipal claimsPrincipal, IEnumerable<string> scopes, string tenantId, MergedOptions mergedOptions, string userFlow, TokenAcquisitionOptions tokenAcquisitionOptions)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable<string> scopes, string authenticationScheme, string tenantId, string userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)

Here is some of the relevant code:

program.cs

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.TokenCacheProviders.Distributed;
using Microsoft.Identity.Web.UI;
using SFTownCenter.Components;
using SFTownCenter.Services;
using SFTownCenter.Services.Interfaces;
using Syncfusion.Blazor;
using Syncfusion.Blazor.Popups;
using WebApplication = Microsoft.AspNetCore.Builder.WebApplication;

var builder = WebApplication.CreateBuilder(args);

// Load common settings (appsettings.json)
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
// Get the current environment name (Development, Staging, Production, etc.)
var environment = builder.Environment.EnvironmentName; 
builder.Configuration.AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true);



// Add services to the container.

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
builder.Services.AddRazorPages();
builder.Services.AddSession(options =>
{
    options.Cookie.IsEssential = true;  // Make the session cookie essential
    options.Cookie.SameSite = SameSiteMode.Lax;  // Configure SameSite for session cookies
    options.IdleTimeout = TimeSpan.FromMinutes(30);
});

var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ') ?? builder.Configuration["MicrosoftGraph:Scopes"]?.Split(' ');

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("EntraID"))
    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
    .AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
    .AddSessionTokenCaches();


builder.Services.AddControllersWithViews()
    .AddMicrosoftIdentityUI();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ServerAPI"));
builder.Services.AddApiAuthorization();



builder.Services.Configure<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme,
    options =>
    {
        options.Events.OnRedirectToAccessDenied =
            context =>
            {
                context.Response.Redirect("/AccessDenied");
                return context.Response.CompleteAsync();
            };
    });


builder.Services.AddScoped<GraphService>();


var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseSession();
app.UseStaticFiles();
app.UseAntiforgery();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();
app.MapRazorPages();

app.Run();

Razor Page, @attribute [Authorize] is at the top of the razor page.

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        User = authState.User.FindFirst(c => c.Type == System.Security.Claims.ClaimTypes.Name)?.Value
               ?? authState.User.FindFirst(c => c.Type == "name")?.Value;

        if (authState.User.Identity.IsAuthenticated)
        {
            string[] scopes = { "User.Read" };
            var token = await TokenAcquisition.GetAccessTokenForUserAsync(scopes);
        }
    }

When I set break points in the OnInitializedAsync, I can verify that the User is authenticated in the if statement. But whenever TokenAcquisition.GetAccessTokenForUserAsync is called, the MSAL error is triggered.

I can also verify their is cookies and a session in developer console: enter image description here

I have several changes in this code to include distributed memory caching, changing the call to happen in OnAfterRenderAsync, and more and can't get it to work. It only works when I manually clear cookies and cache. Am I missing something?


Solution

  • As soon as I restart the application from the IDE, it produces the error MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.

    The error indicates that the ITokenAcquisition service is not get authenticated so that it failed to generate an access token for us. Normally, the flow shall be: app runs and detect that there's no user signed in -> redirect to sign in page to ask user sign in-> get authenticated then continue to generate access token. But when the exception occurred, we would see that the app didn't redirect to sign in page but just throw the exception, or we could see that we already signed in but the exception was thrown.

    The second scenario occurred frequently in my side when I test my app and I signed in, then I stopped my app, change some codes and run the app again. It doesn't asked me to sign in and throw the exception directly. Cleaning the cookie in broswer or keep testing in a privacy window can solve the issue.

    enter image description here

    This is because when we signed in, a cooke named .AspNetCore.Cookies will be created, when the browser is closed and reopen, the cookie won't be deleted so that the browser will think current user doesn't need to sign in again. However, our application has restarted, all session state is cleared so that our server required a new signing-in operation.

    I found a workaround from this case which adding an exception handler for this exception, and delete the code in the handler to force a re-singin operation.