👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Blazor WASM Standalone Authentication with ASP.NET Identity

Blazor WASM Standalone Authentication with ASP.NET Identity

Author - Abdul Rahman (Bhai)

Blazor

34 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

Building a Blazor WebAssembly app with authentication feels like assembling furniture without instructions—you know the pieces fit together somehow, but where do you even start? In this article, let's learn about implementing cookie-based authentication in Blazor WebAssembly Standalone applications using ASP.NET Core Identity. We'll explore how to create a complete authentication system that works seamlessly across a separate backend API and a client-side SPA.

Unlike traditional Blazor Server apps where authentication is straightforward, Blazor WASM Standalone runs entirely in the browser and needs a separate backend for authentication. This creates unique challenges around state management, cookie handling, and cross-origin requests.

Why we gonna do?

The Authentication Dilemma in Blazor WASM

When you build a Blazor WebAssembly app, you're creating a client-side SPA that runs entirely in the browser. But authentication needs a secure backend. Here's what happens without proper setup:


// ❌ This won't work in Blazor WASM - No server-side session
@inject AuthenticationStateProvider AuthStateProvider

@code {
    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;
        // user.Identity.IsAuthenticated will always be false without proper setup
    }
}
            

The problem? Your Blazor WASM app has no built-in session management. It's just static files served to the browser. Authentication state lives on the server, but your UI needs to know about it.

Why Cookie-Based Auth Matters

You might think, "Why not just use JWT tokens?" While tokens work, cookie-based authentication offers several advantages:

  • Automatic credential handling - Browsers send cookies automatically with every request
  • Better security - HttpOnly cookies can't be accessed by JavaScript, protecting against XSS attacks
  • Simpler implementation - No need to manually attach tokens to every API call
  • Native browser support - Logout is as simple as clearing the cookie
  • Works seamlessly with Identity API - ASP.NET Core Identity endpoints use cookies by default

The Two-Server Architecture

A standalone Blazor WASM with Identity requires two separate applications:


┌─────────────────────────────────────────────────────────────────┐
│                    Client Browser                               │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │         Blazor WASM App (https://localhost:5002)          │  │
│  │  • Runs in browser                                        │  │
│  │  • Static files only                                      │  │
│  │  • Makes HTTP calls to backend                            │  │
│  │  • Manages UI state via AuthenticationStateProvider       │  │
│  └───────────────────────────────────────────────────────────┘  │
│                            │                                    │
│                            │ HTTP + Cookies                     │
│                            ▼                                    │
└─────────────────────────────────────────────────────────────────┘
                             │
                             │ CORS-enabled requests
                             │
┌─────────────────────────────────────────────────────────────────┐
│              Backend API (https://localhost:5001)               │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  • ASP.NET Core Web API                                   │  │
│  │  • Identity endpoints (/register, /login, /logout)        │  │
│  │  • EF Core + Identity database                            │  │
│  │  • Cookie authentication                                  │  │
│  │  • Protected endpoints with [Authorize]                   │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
            

Without this separation, you can't have a truly standalone WASM app with server-side authentication. The backend handles all security-sensitive operations while the frontend provides the interactive UI.

How we gonna do?

Step 1: Setting Up the Backend API

Let's start by creating a minimal API backend with Identity support. The backend needs three key components: Identity configuration, CORS policy, and cookie authentication.


using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Backend;

var builder = WebApplication.CreateBuilder(args);

// 1. Establish cookie authentication
builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme)
    .AddIdentityCookies();

// 2. Add the database (in-memory for demo, use real DB in production)
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseInMemoryDatabase("AppDb"));

// 3. Add Identity and opt-in to endpoints
builder.Services.AddIdentityCore<AppUser>()
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<AppDbContext>()
    .AddApiEndpoints();

// 4. Configure CORS for the Blazor WASM client
builder.Services.AddCors(options => options.AddPolicy(
    "wasm",
    policy => policy
        .WithOrigins([
            builder.Configuration["BackendUrl"] ?? "https://localhost:5001",
            builder.Configuration["FrontendUrl"] ?? "https://localhost:5002"
        ])
        .AllowAnyMethod()
        .AllowAnyHeader()
        .AllowCredentials())); // Critical for cookies!

// 5. Add authorization
builder.Services.AddAuthorizationBuilder();

var app = builder.Build();

// 6. Map Identity API endpoints
app.MapIdentityApi<AppUser>();

// 7. Activate CORS (must come before auth middleware)
app.UseCors("wasm");

// 8. Enable authentication and authorization
app.UseAuthentication();
app.UseAuthorization();

// 9. Custom logout endpoint
app.MapPost("/logout", async (
    SignInManager<AppUser> signInManager,
    [FromBody] object empty) =>
{
    if (empty is not null)
    {
        await signInManager.SignOutAsync();
        return Results.Ok();
    }
    return Results.Unauthorized();
}).RequireAuthorization();

// 10. Custom roles endpoint
app.MapGet("/roles", (ClaimsPrincipal user) =>
{
    if (user.Identity is not null && user.Identity.IsAuthenticated)
    {
        var identity = (ClaimsIdentity)user.Identity;
        var roles = identity.FindAll(identity.RoleClaimType)
            .Select(c => new
            {
                c.Issuer,
                c.OriginalIssuer,
                c.Type,
                c.Value,
                c.ValueType
            });
        return TypedResults.Json(roles);
    }
    return Results.Unauthorized();
}).RequireAuthorization();

app.Run();

// Identity database context
class AppDbContext(DbContextOptions<AppDbContext> options) 
    : IdentityDbContext<AppUser>(options)
{
}
            

The AllowCredentials() setting is crucial—without it, browsers won't send cookies cross-origin. The MapIdentityApi call automatically creates endpoints for registration, login, 2FA, and account management.

Step 2: Creating the User Model

Your user model extends IdentityUser to leverage all built-in Identity features. You can add custom properties as needed:


using Microsoft.AspNetCore.Identity;

namespace Backend;

public class AppUser : IdentityUser
{
    // Add custom properties here
    // public string? FirstName { get; set; }
    // public string? LastName { get; set; }
}
            

Step 3: Seeding Initial Data

For testing, seed some users with different roles. In production, you'd register users through your app:


using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace Backend;

public class SeedData
{
    private static readonly IEnumerable<SeedUser> seedUsers =
    [
        new SeedUser()
        {
            Email = "admin@example.com",
            NormalizedEmail = "ADMIN@EXAMPLE.COM",
            NormalizedUserName = "ADMIN@EXAMPLE.COM",
            RoleList = [ "Administrator", "Manager" ],
            UserName = "admin@example.com"
        },
        new SeedUser()
        {
            Email = "user@example.com",
            NormalizedEmail = "USER@EXAMPLE.COM",
            NormalizedUserName = "USER@EXAMPLE.COM",
            RoleList = [ "User" ],
            UserName = "user@example.com"
        },
    ];

    public static async Task InitializeAsync(IServiceProvider serviceProvider)
    {
        using var context = new AppDbContext(
            serviceProvider.GetRequiredService<DbContextOptions<AppDbContext>>());

        if (context.Users.Any())
            return;

        var password = new PasswordHasher<AppUser>();

        // Create roles
        using var roleManager = serviceProvider
            .GetRequiredService<RoleManager<IdentityRole>>();

        string[] roles = [ "Administrator", "Manager", "User" ];

        foreach (var role in roles)
        {
            if (!await roleManager.RoleExistsAsync(role))
            {
                await roleManager.CreateAsync(new IdentityRole(role));
            }
        }

        // Create users
        using var userManager = serviceProvider
            .GetRequiredService<UserManager<AppUser>>();

        foreach (var user in seedUsers)
        {
            var hashed = password.HashPassword(user, "Password123!");
            user.PasswordHash = hashed;
            
            await context.Users.AddAsync(user);

            if (user.Email is not null)
            {
                var appUser = await userManager.FindByEmailAsync(user.Email);
                if (appUser is not null && user.RoleList is not null)
                {
                    await userManager.AddToRolesAsync(appUser, user.RoleList);
                }
            }
        }

        await context.SaveChangesAsync();
    }

    private class SeedUser : AppUser
    {
        public string[]? RoleList { get; set; }
    }
}
            

Step 4: Configuring the Blazor WASM Client

The Blazor WASM app needs several NuGet packages to handle authentication:


<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" 
                      Version="10.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" 
                      Version="10.0.0" />
    <PackageReference Include="Microsoft.Extensions.Http" 
                      Version="10.0.0" />
  </ItemGroup>
</Project>
            

Step 5: Creating the Cookie Handler

The CookieHandler ensures cookies are sent with every API request. This is the bridge between your Blazor UI and the backend authentication:


using Microsoft.AspNetCore.Components.WebAssembly.Http;

namespace BlazorWasmAuth.Identity;

public class CookieHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        // Tell the browser to include cookies with this request
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
        
        // Add header to identify AJAX requests
        request.Headers.Add("X-Requested-With", ["XMLHttpRequest"]);

        return base.SendAsync(request, cancellationToken);
    }
}
            

Without SetBrowserRequestCredentials, browsers won't send cookies to the backend, and authentication will fail silently.

Step 6: Building the Authentication State Provider

This is the heart of the authentication system. The CookieAuthenticationStateProvider manages user state in the Blazor app:


using System.Net;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Components.Authorization;

namespace BlazorWasmAuth.Identity;

public class CookieAuthenticationStateProvider(
    IHttpClientFactory httpClientFactory,
    ILogger<CookieAuthenticationStateProvider> logger)
    : AuthenticationStateProvider, IAccountManagement
{
    private readonly JsonSerializerOptions jsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    };

    private readonly HttpClient httpClient = httpClientFactory.CreateClient("Auth");
    private bool authenticated = false;
    private readonly ClaimsPrincipal unauthenticated = new(new ClaimsIdentity());

    public async Task<FormResult> RegisterAsync(string email, string password)
    {
        try
        {
            var result = await httpClient.PostAsJsonAsync("register", new
            {
                email,
                password
            });

            if (result.IsSuccessStatusCode)
            {
                return new FormResult { Succeeded = true };
            }

            // Parse error details
            var details = await result.Content.ReadAsStringAsync();
            var problemDetails = JsonDocument.Parse(details);
            var errors = new List<string>();
            var errorList = problemDetails.RootElement.GetProperty("errors");

            foreach (var errorEntry in errorList.EnumerateObject())
            {
                if (errorEntry.Value.ValueKind == JsonValueKind.String)
                {
                    errors.Add(errorEntry.Value.GetString()!);
                }
                else if (errorEntry.Value.ValueKind == JsonValueKind.Array)
                {
                    errors.AddRange(
                        errorEntry.Value.EnumerateArray()
                            .Select(e => e.GetString() ?? string.Empty)
                            .Where(e => !string.IsNullOrEmpty(e)));
                }
            }

            return new FormResult
            {
                Succeeded = false,
                ErrorList = [.. errors]
            };
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Registration failed");
            return new FormResult
            {
                Succeeded = false,
                ErrorList = ["An unknown error prevented registration."]
            };
        }
    }

    public async Task<FormResult> LoginAsync(string email, string password)
    {
        try
        {
            // Login with cookies - critical query parameter!
            var result = await httpClient.PostAsJsonAsync(
                "login?useCookies=true", new
                {
                    email,
                    password
                });

            if (result.IsSuccessStatusCode)
            {
                // Notify Blazor to refresh auth state
                NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
                return new FormResult { Succeeded = true };
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Login failed");
        }

        return new FormResult
        {
            Succeeded = false,
            ErrorList = ["Invalid email and/or password."]
        };
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        authenticated = false;
        var user = unauthenticated;

        try
        {
            // Call the secured /manage/info endpoint
            using var userResponse = await httpClient.GetAsync("manage/info");
            userResponse.EnsureSuccessStatusCode();

            var userJson = await userResponse.Content.ReadAsStringAsync();
            var userInfo = JsonSerializer.Deserialize<UserInfo>(
                userJson, 
                jsonOptions);

            if (userInfo != null)
            {
                // Build claims from user info
                var claims = new List<Claim>
                {
                    new(ClaimTypes.Name, userInfo.Email),
                    new(ClaimTypes.Email, userInfo.Email),
                };

                // Add additional claims
                claims.AddRange(
                    userInfo.Claims
                        .Where(c => c.Key != ClaimTypes.Name 
                                 && c.Key != ClaimTypes.Email)
                        .Select(c => new Claim(c.Key, c.Value)));

                // Fetch roles from custom endpoint
                using var rolesResponse = await httpClient.GetAsync("roles");
                rolesResponse.EnsureSuccessStatusCode();

                var rolesJson = await rolesResponse.Content.ReadAsStringAsync();
                var roles = JsonSerializer.Deserialize<RoleClaim[]>(
                    rolesJson, 
                    jsonOptions);

                if (roles?.Length > 0)
                {
                    foreach (var role in roles)
                    {
                        if (!string.IsNullOrEmpty(role.Type) 
                            && !string.IsNullOrEmpty(role.Value))
                        {
                            claims.Add(new Claim(
                                role.Type, 
                                role.Value, 
                                role.ValueType, 
                                role.Issuer, 
                                role.OriginalIssuer));
                        }
                    }
                }

                var id = new ClaimsIdentity(
                    claims, 
                    nameof(CookieAuthenticationStateProvider));
                user = new ClaimsPrincipal(id);
                authenticated = true;
            }
        }
        catch (HttpRequestException ex) 
            when (ex.StatusCode != HttpStatusCode.Unauthorized)
        {
            logger.LogError(ex, "Auth state retrieval failed");
        }

        return new AuthenticationState(user);
    }

    public async Task LogoutAsync()
    {
        var emptyContent = new StringContent(
            "{}", 
            Encoding.UTF8, 
            "application/json");
        await httpClient.PostAsync("logout", emptyContent);
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public async Task<bool> CheckAuthenticatedAsync()
    {
        await GetAuthenticationStateAsync();
        return authenticated;
    }
}
            

The ?useCookies=true parameter in the login endpoint is essential—it tells Identity to use cookie authentication instead of bearer tokens.

Step 7: Registering Services in Program.cs

Wire everything together in the Blazor WASM Program.cs:


using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorWasmAuth;
using BlazorWasmAuth.Identity;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

// Register the cookie handler
builder.Services.AddTransient<CookieHandler>();

// Set up authorization
builder.Services.AddAuthorizationCore();

// Register custom authentication state provider
builder.Services.AddScoped<AuthenticationStateProvider, 
    CookieAuthenticationStateProvider>();

// Register account management interface
builder.Services.AddScoped(sp => 
    (IAccountManagement)sp.GetRequiredService<AuthenticationStateProvider>());

// Default HttpClient for the app
builder.Services.AddScoped(sp => new HttpClient
{
    BaseAddress = new Uri(
        builder.Configuration["FrontendUrl"] ?? "https://localhost:5002")
});

// Auth-specific HttpClient with cookie handler
builder.Services.AddHttpClient(
    "Auth",
    opt => opt.BaseAddress = new Uri(
        builder.Configuration["BackendUrl"] ?? "https://localhost:5001"))
    .AddHttpMessageHandler<CookieHandler>();

await builder.Build().RunAsync();
            

Step 8: Creating Login and Registration Pages

Build clean, functional UI components for authentication. Here's the login page:

@page "/login"
@using System.ComponentModel.DataAnnotations
@using BlazorWasmAuth.Identity
@inject IAccountManagement Acct
@inject NavigationManager Navigation

<PageTitle>Login</PageTitle>

<h1>Login</h1>

<AuthorizeView>
    <Authorized>
        <div class="alert alert-success">
            You're logged in as @context.User.Identity?.Name.
        </div>
    </Authorized>
    <NotAuthorized>
        @foreach (var error in formResult.ErrorList)
        {
            <div class="alert alert-danger">@error</div>
        }

        <EditForm Model="Input" method="post" OnValidSubmit="LoginUser">
            <DataAnnotationsValidator />
            
            <div class="form-floating mb-3">
                <InputText @bind-Value="Input.Email"
                           class="form-control"
                           autocomplete="username"
                           placeholder="name@example.com" />
                <label>Email</label>
                <ValidationMessage For="() => Input.Email" />
            </div>

            <div class="form-floating mb-3">
                <InputText type="password"
                           @bind-Value="Input.Password"
                           class="form-control"
                           autocomplete="current-password"
                           placeholder="password" />
                <label>Password</label>
                <ValidationMessage For="() => Input.Password" />
            </div>

            <button type="submit" class="btn btn-primary">Log in</button>
        </EditForm>
    </NotAuthorized>
</AuthorizeView>

@code {
    private FormResult formResult = new();

    [SupplyParameterFromForm]
    private InputModel? Input { get; set; }

    [SupplyParameterFromQuery]
    private string? ReturnUrl { get; set; }

    protected override void OnInitialized() => Input ??= new();

    public async Task LoginUser()
    {
        if (Input is not null)
        {
            formResult = await Acct.LoginAsync(Input.Email, Input.Password);
        }

        if (formResult.Succeeded && !string.IsNullOrEmpty(ReturnUrl))
        {
            Navigation.NavigateTo(ReturnUrl);
        }
    }

    private sealed class InputModel
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; } = string.Empty;

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; } = string.Empty;
    }
}
            
Login
Login
Profile
Profile
Logout
Logout

Step 9: Using Authorization in Components

Now you can use AuthorizeView anywhere in your app:

@page "/"

<AuthorizeView>
    <Authorized>
        <h1>Welcome, @context.User.Identity?.Name!</h1>
        
        <AuthorizeView Roles="Administrator">
            <Authorized>
                <p>You have admin access.</p>
            </Authorized>
        </AuthorizeView>
    </Authorized>
    <NotAuthorized>
        <h1>Please log in</h1>
        <a href="login">Login</a>
    </NotAuthorized>
</AuthorizeView>
            

Step 10: Protecting API Endpoints with Roles

Back in your backend, protect endpoints with roles:


// Endpoint requiring authentication
app.MapPost("/data-processing", ([FromBody] FormModel model) =>
    Results.Text($"Processed {model.Message.Length} characters"))
    .RequireAuthorization();

// Endpoint requiring specific role
app.MapPost("/admin-action", ([FromBody] FormModel model) =>
    Results.Text($"Admin action completed"))
    .RequireAuthorization(policy => policy.RequireRole("Administrator"));

// Endpoint requiring multiple roles
app.MapPost("/manager-action", ([FromBody] FormModel model) =>
    Results.Text($"Manager action completed"))
    .RequireAuthorization(policy => 
        policy.RequireRole("Administrator", "Manager"));
            

Understanding the Authentication Flow

Here's what happens when a user logs in:


1. User submits login form in Blazor WASM
   ↓
2. CookieAuthenticationStateProvider calls backend /login?useCookies=true
   ↓
3. Backend validates credentials and sets HttpOnly cookie
   ↓
4. Cookie automatically sent with all subsequent requests via CookieHandler
   ↓
5. GetAuthenticationStateAsync calls /manage/info and /roles
   ↓
6. Backend validates cookie and returns user info + roles
   ↓
7. CookieAuthenticationStateProvider builds ClaimsPrincipal
   ↓
8. NotifyAuthenticationStateChanged updates all AuthorizeView components
   ↓
9. UI re-renders showing authenticated content
            

Common Pitfalls and Solutions

Problem: Cookies aren't being sent
Solution: Ensure AllowCredentials() is set in CORS policy and SetBrowserRequestCredentials(Include) is used in CookieHandler.

Problem: Authentication state doesn't update after login
Solution: Call NotifyAuthenticationStateChanged() after successful login.

Problem: CORS errors
Solution: Ensure CORS middleware is added before authentication middleware. Order matters!

Problem: Roles aren't available in claims
Solution: Create a custom /roles endpoint since Identity's default endpoints don't return roles in the user info.

Cross-Domain Hosting Configuration

If you host the backend and frontend on different domains, you need to adjust the cookie configuration for cross-domain scenarios:


// In Backend/Program.cs - Uncomment and modify for cross-domain hosting
builder.Services.ConfigureApplicationCookie(options =>
{
    // Change from Lax to None for cross-domain
    options.Cookie.SameSite = SameSiteMode.None;
    
    // Always use secure cookies for cross-domain
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});
            

The default settings (SameSiteMode.Lax and CookieSecurePolicy.SameAsRequest) work for same-domain hosting. For different domains, you must use SameSiteMode.None with HTTPS-only cookies.

Security Considerations

Never store secrets in client-side code. Blazor WASM runs entirely in the browser, making all code visible. Key security principles:

  • No sensitive data in WASM - Never put connection strings, API keys, or secrets in your Blazor WASM app
  • AuthorizeView is not security - All content in AuthorizeView components is downloadable; it only hides UI elements
  • Always validate on the server - Backend endpoints must enforce authorization with [Authorize] or RequireAuthorization()
  • Use secure authentication flows - Cookie-based auth with HttpOnly cookies prevents JavaScript access
  • Enable HTTPS - Always use HTTPS in production to protect cookies in transit

Additional Identity Features

The MapIdentityApi endpoints provide more than just login and registration:

  • Two-factor authentication (2FA) - Built-in support for TOTP authenticator apps
  • Email confirmation - Verify user email addresses before allowing login
  • Password recovery - Reset passwords via email tokens
  • Account management - Update email, password, and user info at /manage/info

To implement these features, you'll need to configure email services and customize the registration flow to send confirmation emails.

Debugging and Troubleshooting Tips

Enable detailed logging to diagnose authentication issues:


// In BlazorWasmAuth/Program.cs
builder.Logging.SetMinimumLevel(LogLevel.Debug);
builder.Logging.AddFilter("Microsoft.AspNetCore.Components.WebAssembly.Authentication", 
    LogLevel.Trace);
            

Common troubleshooting steps:

  • Clear browser cookies and site data between tests
  • Check network traffic in browser DevTools to see exact API responses
  • Verify URLs in appsettings.json match your actual endpoints
  • Use InPrivate/Incognito mode to avoid cookie conflicts
  • Check that both apps are running on correct ports

Summary

In this article, we explored building a complete authentication system for Blazor WebAssembly Standalone applications using cookie-based authentication with ASP.NET Core Identity. Here are the key takeaways:

  • Two-server architecture - Blazor WASM runs in the browser while authentication lives on a separate backend API
  • Cookie-based auth provides better security and simpler implementation than bearer tokens for browser apps
  • CookieHandler ensures cookies are automatically sent with every API request
  • Custom AuthenticationStateProvider manages user state and claims in the Blazor app
  • CORS configuration with AllowCredentials is critical for cross-origin cookie authentication
  • Identity API endpoints provide built-in registration, login, and account management
  • Role-based authorization works seamlessly once proper claims are established

This pattern gives you a production-ready authentication system that's secure, maintainable, and follows .NET best practices. The separation of concerns between frontend and backend makes it easy to scale, test, and deploy independently.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Blazor
  • Authentication
  • Identity
  • Cookie
  • Standalone
  • WASM