In my Blazor project (.Client and .Server), I implemented a CustomAuthenticationStateProvider and used [Authorize] on a protected page, but authentication fails—no cookie is set, and the page acts as if the user is unauthenticated.
Key Setup:
AuthController.cs:
using Dragon.Authentications;using Dragon.Models;using Microsoft.AspNetCore.Authentication.Cookies;using Microsoft.AspNetCore.Authentication;using Microsoft.AspNetCore.Identity;using Microsoft.AspNetCore.Mvc;using Microsoft.IdentityModel.Tokens;using System.IdentityModel.Tokens.Jwt;using System.Security.Claims;using System.Text;using Microsoft.AspNetCore.Authorization;namespace Dragon.Controllers{ [Route("api/auth")] [ApiController] public class AuthController : ControllerBase { private readonly DatabaseContext _dbContext; public AuthController(DatabaseContext dbContext) { _dbContext = dbContext; } [HttpPost("login")] [Authorize] [AllowAnonymous] public async Task<IActionResult> Login([FromBody] LoginModel request) { var user = _dbContext.Users.FirstOrDefault(u => u.Username == request.Username && u.Active); if (user == null || user.Password != request.Password) { return Unauthorized(new { Message = "Invalid credentials." }); } var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, user.FirstName), new Claim(ClaimTypes.Sid, user.Id.ToString()), new Claim(ClaimTypes.Expiration, DateTimeOffset.UtcNow.AddMinutes(300).ToString()) }, "Cookies"); var principal = new ClaimsPrincipal(identity); await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties { IsPersistent = true, AllowRefresh = true, ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30), IssuedUtc = DateTime.UtcNow, }); HttpContext.User = principal; return Ok(); } }}Routes.razor:
<CascadingAuthenticationState><Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }"><Found Context="routeData"><RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" /><FocusOnNavigate RouteData="@routeData" Selector="h1" /></Found></Router></CascadingAuthenticationState>CustomAuthenticationStateProvider.cs:
namespace Dragon.Authentications{ public class CustomAuthenticationStateProvider : AuthenticationStateProvider { private readonly IHttpContextAccessor _httpContextAccessor; public CustomAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public override Task<AuthenticationState> GetAuthenticationStateAsync() { var httpContext = _httpContextAccessor.HttpContext; var user = httpContext?.User; if (user?.Identity != null && user.Identity.IsAuthenticated) { return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(user))); } // Return unauthenticated state return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); } }}Program.cs (.Server):
var builder = WebApplication.CreateBuilder(args);// Register database contextbuilder.Services.AddDbContext<DatabaseContext>(options => options.UseSqlServer("Server=localhost;Database=offline;User Id=sa;Password=admin;TrustServerCertificate=True"));builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents();builder.Services.AddSingleton<ISessionSettings, SettingService>();builder.Services.AddSingleton<IOptionService, OptionService>();builder.Services.AddSingleton<IHtmlProcessorService, HtmlProcessorService>();builder.Services.AddScoped<IEncodingService, EncodingService>();builder.Services.AddHttpClient();builder.Services.AddSingleton<ServerCache>();builder.Services.AddAntiforgery();builder.Services.AddControllers(o =>{ o.AllowEmptyInputInBodyModelBinding = true; o.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;});builder.Services.AddServerSideBlazor(options =>{ options.DetailedErrors = true;});builder.Services.AddScoped<ModalService>();builder.Services.AddScoped<ToastService>();builder.Services.AddScoped<EmailService>();builder.Services.AddScoped<SupportChatService>();builder.Services.AddScoped(sp => new HttpClient{ BaseAddress = new Uri(builder.Configuration["BaseUrl:ApiUrl"])});builder.Services.AddSignalR(op => { op.MaximumReceiveMessageSize = 32 * 1024 * 1024; }).AddMessagePackProtocol();builder.Services.AddResponseCompression(options =>{ options.EnableForHttps = true;});builder.Services.AddCascadingAuthenticationState();builder.Services.AddHttpContextAccessor();builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();builder.Services.AddAuthentication(o =>{ o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; o.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; o.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o =>{ // Cookie settings o.Cookie.HttpOnly = true; // Prevent client-side access o.Cookie.SecurePolicy = CookieSecurePolicy.None; // Use `Always` for HTTPS in production; `None` for local testing o.Cookie.Name = "DragonAuth"; // Custom cookie name o.Cookie.SameSite = SameSiteMode.Lax; // Allows cookie to be sent on same-site navigation o.ExpireTimeSpan = TimeSpan.FromMinutes(30); // Cookie expiration (adjust as needed) o.SlidingExpiration = true; // Refresh expiration on activity // Login and redirect paths o.LoginPath = "/login/"; // Path to login page o.AccessDeniedPath = "/access-denied"; // Path to handle unauthorized access o.LogoutPath = "/logout/"; // Path to logout endpoint // Cookie validation and events o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { // Custom validation logic if (ctx.Principal?.Identity?.IsAuthenticated ?? false) { var expirationClaim = ctx.Principal.FindFirst(ClaimTypes.Expiration)?.Value; if (DateTimeOffset.TryParse(expirationClaim, out var expiration) && expiration < DateTimeOffset.UtcNow) { ctx.RejectPrincipal(); // Reject expired tokens return ctx.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } } else { ctx.RejectPrincipal(); // No identity or not authenticated } return Task.CompletedTask; } };});builder.Services.AddAuthorizationCore();builder.Services.AddAuthorization();var app = builder.Build();if (app.Environment.IsDevelopment()){ app.UseWebAssemblyDebugging();}else{ app.UseExceptionHandler("/Error", createScopeForErrors: true); app.UseHsts();}app.UseResponseCompression();app.UseHttpsRedirection();app.UseStaticFiles();app.UseRouting();app.UseAntiforgery();var cookiePolicyOptions = new CookiePolicyOptions{ MinimumSameSitePolicy = SameSiteMode.Strict,};app.UseCookiePolicy(cookiePolicyOptions);app.UseAuthentication();app.UseAuthorization();// Blazor app routingapp.MapRazorComponents<App>() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode();app.MapControllers();app.MapHub<TeleHub>("/teleHub");app.MapHub<Dragon.Hubs.SupportChat>("/supportChat");app.Run();Program.cs (.Client):
var builder = WebAssemblyHostBuilder.CreateDefault(args);builder.Logging.SetMinimumLevel(LogLevel.Debug);builder.Services.AddSingleton<IUserSettingsService, UserSettingsService>();builder.Services.AddScoped<ToastService>();builder.Services.AddScoped<ModalService>();await builder.Build().RunAsync();Login.razor (on Server project):
@page "/login"@inject NavigationManager Navigation@inject HttpClient Http@inject AuthenticationStateProvider AuthenticationStateProvider@rendermode @(new InteractiveAutoRenderMode(false))<h3>Login</h3>@if (loginFailed){<p class="text-danger">Invalid username or password. Please try again.</p>}<EditForm Model="loginModel" FormName="loginForm" OnValidSubmit="HandleLogin"><DataAnnotationsValidator /><ValidationSummary /><div><label for="username">Username:</label><InputText id="username" @bind-Value="loginModel.Username" /></div><div><label for="password">Password:</label><InputText id="password" @bind-Value="loginModel.Password" InputType="password" /></div><button type="submit">Login</button></EditForm>@code { private LoginModel loginModel = new LoginModel(); private bool loginFailed = false; private async Task HandleLogin() { loginFailed = false; try { var response = await Http.PostAsJsonAsync("/api/auth/login", loginModel); if (response.IsSuccessStatusCode) { //await AuthenticationStateProvider.MarkUserAsAuthenticated(token); //await ((CustomAuthenticationStateProvider)AuthenticationStateProvider).GetAuthenticationStateAsync(); Navigation.NavigateTo("/"); // Successfully logged in, navigate to t } else { // Login failed loginFailed = true; } } catch { loginFailed = true; } }}Problem:Despite these setups, cookies aren't created in the browser, and protected pages are inaccessible. What could be the missing step to make authentication work correctly in this setup?