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 context
builder.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 routing
app.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?
This will not work because the cookie is generated in the "httpclient", not the browser. There are 2 ways to solve the issue.
First is blazor has a built-in design that when using httpclient in WASM(client), the httpclient will share its cookie to browser. So you could put Login.razor
in client project and use rendermode @(new InteractiveWebAssemblyRenderMode(false))
for it.
On the other hand, if you want to use it on server mode, the httpclient doesn't have this design. You have to copy the cookies from httplient to browser manualy. This could be done using custom HttpClientHandler
public class CustomHttpHandler : HttpClientHandler
@inject HttpClient _httpClient
@inject CustomHttpHandler _customHttpHander
...
_httpClient = new HttpClient(_customHttpHander);
//A sample to copy cookies
public class CustomHttpHandler : HttpClientHandler
{
private readonly IJSRuntime _jsRuntime;
public CustomHttpHandler(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
// Copy cookies to browser
if (response.Headers.Contains("Set-Cookie"))
{
var cookies = response.Headers.GetValues("Set-Cookie");
foreach (var cookie in cookies)
{
await _jsRuntime.InvokeVoidAsync("document.cookie", cookie);
}
}
return response;
}
}
builder.Services.AddSingleton<IJSRuntime, JSRuntime>();
builder.Services.AddScoped<HttpClient>(sp =>
{
var jsRuntime = sp.GetRequiredService<IJSRuntime>();
var handler = new CustomHttpHandler(jsRuntime);
return new HttpClient(handler);
});