I'm building a Blazor Web App (.NET 8) using Server interactivity and authenticating users via Microsoft Entra ID. Authentication works — I see the user is logged in, and their claims (oid, tid, etc.) are present.
My problems comes when I try to call the API from the blazor app:
var token = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes, user);
The error from the above call is
MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call
Verbose MSAL logs say:
Found 0 accounts in MSAL cacheNo refresh tokens or accounts foundToken cache could not satisfy silent request
This happens even though the user is authenticated.
My setup:
// Program.csbuilder.Services.AddBlazorServerAuthentication(builder.Configuration);builder.Services.AddBlazorServerAuthorization();// Add services to the container._ = builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddMicrosoftIdentityConsentHandler();_ = builder.Services.AddServerSideBlazor() .AddMicrosoftIdentityConsentHandler();_ = builder.Services.AddHttpContextAccessor();_ = builder.Services.AddCascadingAuthenticationState(); = builder.Services.AddTransient<AccessTokenHandler>();_ = builder.Services .AddHttpClient("WebApiClient", client => client.BaseAddress = builder.Configuration.GetValue<Uri>("DownstreamApi:BaseUrl")) .AddHttpMessageHandler<AccessTokenHandler>();
public static void AddBlazorServerAuthentication(this IServiceCollection services, IConfiguration configuration){ JwtSecurityTokenHandler.DefaultMapInboundClaims = false; string[] scopes = configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(';'); _ = services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddBearerToken() .AddMicrosoftIdentityWebApp(options => { configuration.Bind("AzureAd", options); options.MaxAge = TimeSpan.FromHours(8); options.AutomaticRefreshInterval = TimeSpan.FromMinutes(15); options.Events.OnRemoteFailure = async context => { try { AuthenticationProperties authenticationProperties = new() { RedirectUri = "/" }; authenticationProperties.Parameters["logoutHint"] = context?.HttpContext?.User?.Identity?.Name; await context.HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, authenticationProperties); await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); Console.WriteLine("Authentication failed. Logging out user silently"); context.HandleResponse(); await Task.CompletedTask; } catch (Exception) { throw; } }; }) .EnableTokenAcquisitionToCallDownstreamApi(scopes) .AddInMemoryTokenCaches();}public static void AddBlazorServerAuthorization(this IServiceCollection services){ _ = services.AddAuthorization(options => { options.FallbackPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); });}
//AccessTokenHandlerpublic class AccessTokenHandler : DelegatingHandler{ private readonly ITokenAcquisition _tokenAcquisition; private readonly IConfiguration _config; private readonly IHttpContextAccessor _httpContextAccessor; public AccessTokenHandler(ITokenAcquisition tokenAcquisition, IConfiguration config, IHttpContextAccessor httpContextAccessor) { _tokenAcquisition = tokenAcquisition; _config = config; _httpContextAccessor = httpContextAccessor; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { string[] scopes = _config["DownstreamApi:Scopes"]?.Split('', StringSplitOptions.RemoveEmptyEntries); if (scopes?.Length > 0) { string token = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes, user: _httpContextAccessor.HttpContext?.User, authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } return await base.SendAsync(request, cancellationToken); }}
"AzureAd": {"Instance": "https://login.microsoftonline.com/","ClientId": "xxxxxxx","ClientSecret": "xyxyxyxyxy","TenantId": "zzzzzzzzz"},"DownstreamApi": {"BaseUrl": "https://localhost:44385","Scopes": "api://yyyyyyyyyy/access_as_user"}