I am trying to authenticate an user from Blazor to access resources on a ASP.NET Core Web API using ASP.NET Core Identity.
I have two authentication methods configured: JWT and Cookies.
When I login from direct API call (Swagger, Postman), I can normally access the resources, with both JWT and cookie authentication. When I login from Blazor, the browser sets the cookie, but I get an Unauthorized response.
Login.razor
@page "/Account/Login"@inject SignInManager<UserModel> SignInManager@inject ILogger<Login> Logger@inject NavigationManager NavigationManager@inject IdentityRedirectManager RedirectManager@inject IAuthService AuthService<PageTitle>Log in</PageTitle><MudText Typo="Typo.h3" GutterBottom="true">Log in</MudText><MudGrid><MudItem md="6"><StatusMessage Message="@errorMessage" /><EditForm Model="Input" method="post" OnValidSubmit="LoginUser" FormName="login"><DataAnnotationsValidator /><MudText GutterBottom="true" Typo="Typo.body1">Use a local account to log in.</MudText><MudGrid><MudItem md="12"><MudStaticTextField For="@(() => Input.Email)" @bind-Value="Input.Email" Label="Email" Placeholder="name@example.com" UserAttributes="@(new() { { "autocomplete", "username" }, { "aria-required", "true" } } )" /></MudItem><MudItem md="12"><MudStaticTextField For="@(() => Input.Password)" @bind-Value="Input.Password" Label="Password" InputType="InputType.Password" Placeholder="password" UserAttributes="@(new() { { "autocomplete", "current-password" }, { "aria-required", "true" } } )" /></MudItem><MudItem md="12"> @* <MudStaticCheckBox For="@(() => Input.RememberMe)" @bind-Value="Input.RememberMe">Remember me</MudStaticCheckBox> *@</MudItem><MudItem md="12"><MudStaticButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true" FormAction="FormAction.Submit">Log in</MudStaticButton></MudItem></MudGrid></EditForm><MudGrid Class="mt-4"><MudItem md="12"><MudLink Href="Account/ForgotPassword">Forgot your password?</MudLink><br /><MudLink Href="@(NavigationManager.GetUriWithQueryParameters("Account/Register", new Dictionary<string, object?> { ["ReturnUrl"] = ReturnUrl }))">Register as a new user</MudLink><br /><MudLink Href="Account/ResendEmailConfirmation">Resend email confirmation</MudLink></MudItem></MudGrid></MudItem><MudItem md="6"><MudText GutterBottom="true" Typo="Typo.body1">Use another service to log in.</MudText><ExternalLoginPicker /></MudItem></MudGrid>@code { private string? errorMessage; [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; [SupplyParameterFromForm] private LoginRequestDto Input { get; set; } = new(); [SupplyParameterFromQuery] private string? ReturnUrl { get; set; } protected override async Task OnInitializedAsync() { if (HttpMethods.IsGet(HttpContext.Request.Method)) { // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); } } public async Task LoginUser() { await AuthService.Login(Input); RedirectManager.RedirectTo(ReturnUrl); }}AuthService.cs
public async Task<AuthResponseDto?> Login(LoginRequestDto loginDto){ UserModel? userDb = default!; if (!string.IsNullOrEmpty(loginDto.Email)) { userDb = await _userManager.FindByEmailAsync(loginDto.Email); } else if (!string.IsNullOrEmpty(loginDto.UserName)) { userDb = await _userManager.FindByNameAsync(loginDto.UserName); } if (userDb is null || !(await _signInManager.CheckPasswordSignInAsync(userDb, loginDto.Password, false)).Succeeded) { return null; } var result = await _signInManager.PasswordSignInAsync(userDb, loginDto.Password, false, false); if (result.Succeeded) { var token = await GenerateAccessToken(userDb); var response = userDb.ToAuthResponseDto(token); return response; } return null;}Program.cs (in SocialMedia.Api):
var builder = WebApplication.CreateBuilder(args);// Devlooped servicesbuilder.Services.AddServices();// Add services to the container.// Ignore object cycling on queries with related databuilder.Services.AddControllers().AddJsonOptions(options =>{ options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;});builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen(c =>{ var securityScheme = new OpenApiSecurityScheme { Name = "JWT Authentication", Description = "Enter your JWT token in this field", In = ParameterLocation.Header, Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT", }; c.AddSecurityDefinition("Bearer", securityScheme); var securityRequirement = new OpenApiSecurityRequirement() { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer", }, }, Array.Empty<string>() }, }; c.AddSecurityRequirement(securityRequirement);});builder.Services.AddRazorPages();builder.Services.AddProblemDetails();builder.Services.AddIdentity<UserModel, IdentityRole>(options =>{ options.SignIn.RequireConfirmedAccount = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireDigit = false; options.Password.RequireUppercase = false; options.User.RequireUniqueEmail = true; options.User.AllowedUserNameCharacters ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";}) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders();builder.Services.AddAuthentication(options =>{ options.DefaultScheme = IdentityConstants.ApplicationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;}).AddJwtBearer(options =>{ options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = builder.Configuration["JWT:Issuer"], ValidateAudience = true, ValidAudience = builder.Configuration["JWT:Audience"], ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( System.Text.Encoding.UTF8.GetBytes(builder.Configuration["JWT:SignInKey"] !)), }; options.IncludeErrorDetails = true;});builder.Services.ConfigureApplicationCookie(options =>{ options.ExpireTimeSpan = TimeSpan.FromHours(1); options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.None; options.Cookie.Name = "APICOOKIE";});builder.Services.AddAuthorization(options =>{ var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder( IdentityConstants.ApplicationScheme, JwtBearerDefaults.AuthenticationScheme); defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser(); options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();});builder.Services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));builder.Services.AddCors(options =>{ options.AddPolicy("Default", policy => { _ = policy.WithOrigins("https://localhost:7213") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); });});builder.Services.AddHttpContextAccessor();var app = builder.Build();app.UseCors();app.UseHttpsRedirection();app.UseAuthentication();app.UseAuthorization();app.MapControllers();app.MapRazorPages();// Configure the HTTP request pipeline.if (app.Environment.IsDevelopment()){ _ = app.UseSwagger(); _ = app.UseSwaggerUI(); // _ = app.MapControllers().AllowAnonymous();}app.Run();Program.cs (in SocialMedia.Front):
var builder = WebApplication.CreateBuilder(args);// Devlooped servicesbuilder.Services.AddServices();// Add MudBlazor servicesbuilder.Services.AddMudServices();// Add services to the container.builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents() .AddAuthenticationStateSerialization( options => options.SerializeAllClaims = true);builder.Services.AddCascadingAuthenticationState();builder.Services.AddIdentity<UserModel, IdentityRole>(options =>{ options.SignIn.RequireConfirmedAccount = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireDigit = false; options.Password.RequireUppercase = false; options.User.RequireUniqueEmail = true; options.User.AllowedUserNameCharacters ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";}) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders();builder.Services.AddAuthentication(options =>{ options.DefaultScheme = IdentityConstants.ApplicationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;});builder.Services.ConfigureApplicationCookie(options =>{ options.ExpireTimeSpan = TimeSpan.FromHours(1); options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.None;});builder.Services.AddHttpClient("Auth", options =>{ options.BaseAddress = new Uri(builder.Configuration["ApiUrl"]!);}).AddHttpMessageHandler<CookieHandler>();builder.Services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));builder.Services.AddDatabaseDeveloperPageExceptionFilter();var app = builder.Build();// Configure the HTTP request pipeline.if (app.Environment.IsDevelopment()){ app.UseWebAssemblyDebugging(); _ = app.UseMigrationsEndPoint();} else{ _ = 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.UseAntiforgery();app.MapStaticAssets();app.MapRazorComponents<App>() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(SocialMedia.Front.Client._Imports).Assembly);// Add additional endpoints required by the Identity /Account Razor components.app.MapAdditionalIdentityEndpoints();app.Run();Program.cs (in SocialMedia.Front.Client):
var builder = WebAssemblyHostBuilder.CreateDefault(args);// Devlooped servicesbuilder.Services.AddServices();builder.Services.AddMudServices();builder.Services.AddAuthorizationCore();builder.Services.AddCascadingAuthenticationState();builder.Services.AddAuthenticationStateDeserialization();builder.Services.AddHttpClient("Auth", options =>{ options.BaseAddress = new Uri(builder.Configuration["ApiUrl"]!);}).AddHttpMessageHandler<CookieHandler>();await builder.Build().RunAsync();PostsController.cs
namespace SocialMedia.Api.Controllers{ [Route("api/[controller]")] [ApiController] [Authorize] public class PostsController(IPostService postService) : ControllerBase { private readonly string _postNotFoundMsg = "The post was not found!"; [HttpPost] public async Task<IActionResult> Create([FromBody] CreatePostRequestDto postToCreate) { var createdPost = await postService.Create(postToCreate); if (createdPost == null) { return NotFound("User not found!"); } return CreatedAtAction( nameof(GetById), new { createdPost.Id }, createdPost); } [HttpGet] public async Task<IActionResult> GetAll() { var posts = await postService.GetAll(); return Ok(posts); } [HttpGet("{id}")] public async Task<IActionResult> GetById([FromRoute] int id) { var post = await postService.GetById(id); if (post == null) { return NotFound(_postNotFoundMsg); } return Ok(post); } [HttpPut] public async Task<IActionResult> Update([FromBody] UpdatePostRequestDto postToUpdate) { var updatedPost = await postService.Update(postToUpdate); if (updatedPost == null) { return NotFound(_postNotFoundMsg); } return Ok(updatedPost); } [HttpDelete("{id}")] public async Task<IActionResult> Delete([FromRoute] int id) { var deletedPost = await postService.Delete(id); if (deletedPost == null) { return NotFound(_postNotFoundMsg); } return NoContent(); } }}I saw that for cookies to be sent, it should have a CookieHander. I implemented it, but no success.
CookieHander.cs
namespace SocialMedia.Front.Client.Middlewares{ [Service(ServiceLifetime.Transient)] public class CookieHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { _ = request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); request.Headers.Add("X-Requested-With", ["XMLHttpRequest"]); return await base.SendAsync(request, cancellationToken); } }}I already tried using SignInManager instead of my own AuthService class, but it did not work either. Tried to change a lot of things on the Program.cs of both projects, but none of these had effect.