Skip to content
Menu
GeoSaffer.com
  • Shop
  • Support
GeoSaffer.com

Implementing Authentication in a .NET 9 MAUI Blazor Hybrid App (SSR)

Posted on November 20, 2024April 20, 2026
Diagram showing .NET MAUI Blazor Hybrid authentication with platform-specific service implementations
.NET · MAUI Blazor · Authentication

Implementing Authentication
in a .NET 9 MAUI Blazor Hybrid App (SSR)

20 November 2024 · GeoSaffer.com

The Blazor Hybrid model lets you share Razor components between a Blazor Server web app and a MAUI mobile app — but authentication cannot be shared the same way. SSR imposes strict constraints on cookie auth, while MAUI relies on platform secure storage. This guide walks through both paths in a single three-project solution.

1 Solution Structure

The solution uses three projects. Shared Razor components and services live in YourApp.Shared; the web and mobile apps each register platform-specific implementations of a common auth interface without duplicating any UI logic.

Web YourApp.Web

Blazor Server (SSR) app. Handles cookie auth via IHttpContextAccessor and ASP.NET Core middleware.

Mobile YourApp.Mobile

.NET MAUI Blazor Hybrid app. Uses SecureStorage for persistent, platform-native auth state.

Shared YourApp.Shared

Class library containing the auth interface, all Razor pages, and UserService. Referenced by both other projects.


2 Prerequisites & NuGet Packages

All four NuGet packages go into YourApp.Shared — neither the web nor the mobile project needs to reference them directly for auth to work.

Environment

  • .NET 9 SDK installed
  • Visual Studio 2022 v17.12 or later
  • MAUI workload: dotnet workload install maui
  • All three projects target net9.0

NuGet — YourApp.Shared

  • Microsoft.AspNetCore.Authorization 9.0.0
  • Microsoft.AspNetCore.Components.Authorization 9.0.0
  • Microsoft.AspNetCore.Components.Web 9.0.0
  • Microsoft.Maui.Essentials 9.0.10

Add the following global usings to _Imports.razor in YourApp.Shared so all Razor pages can access auth types without per-file imports:

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using YourApp.Shared.Layout
@using YourApp.Shared.Pages
@using YourApp.Shared.Services

3 Platform-Specific Auth Services

The core pattern is a single ICustomAuthenticationService interface with two concrete implementations — one for the server using cookie auth, one for MAUI using SecureStorage. Each project’s DI container registers its own version.

Define the interface in YourApp.Shared/Services/ICustomAuthenticationService.cs:

public interface ICustomAuthenticationService
{
    Task SignInAsync(string username);
    Task SignOutAsync();
    Task<ClaimsPrincipal> GetCurrentUserAsync();
}

Blazor Server (Web)

  • Inject IHttpContextAccessor into the service
  • SignInAsync calls HttpContext.SignInAsync("Cookies", principal)
  • SignOutAsync calls HttpContext.SignOutAsync("Cookies")
  • Provide a ServerAuthenticationStateProvider that reads HttpContext.User
  • Register cookie middleware and AddHttpContextAccessor in Program.cs

MAUI (Mobile)

  • No HTTP context — IHttpContextAccessor is unavailable in MAUI
  • SignInAsync writes username to SecureStorage and calls NotifyAuthenticationStateChanged
  • SignOutAsync removes the key and resets to an anonymous principal
  • Register CustomAuthenticationStateProvider as both its concrete type and as AuthenticationStateProvider
  • State survives app restarts via SecureStorage.Default.GetAsync

In YourApp.Web/Program.cs, configure cookie authentication and register the server-specific services:

builder.Services.AddAuthentication("Cookies")
    .AddCookie("Cookies", options => { options.LoginPath = "/login"; });

builder.Services.AddAuthorizationCore();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICustomAuthenticationService, ServerAuthenticationService>();
builder.Services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
builder.Services.AddScoped<UserService>();

In YourApp.Mobile/MauiProgram.cs, register the MAUI-specific auth services:

builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(provider =>
    provider.GetRequiredService<CustomAuthenticationStateProvider>());
builder.Services.AddScoped<ICustomAuthenticationService, CustomAuthenticationService>();
builder.Services.AddScoped<UserService>();

4 User Store & Auth Components

These three components live entirely in YourApp.Shared and are consumed by both web and mobile without modification. The user store is intentionally minimal — replace plain-text passwords with a hashed database before going anywhere near production.

1
UserService — in-memory credential store

Create YourApp.Shared/Services/UserService.cs. It holds a hardcoded list of UserCredential objects with Username, Password, and Role. AuthenticateUserAsync does a LINQ match — plaintext strings for now, swap in IPasswordHasher<T> before shipping.

2
Login.razor — form with inline error handling

Route: @page "/login". Inject ICustomAuthenticationService, UserService, and NavigationManager. On valid submit, call AuthenticateUserAsync; on success call AuthService.SignInAsync(user.Username) and navigate to /. On failure, render an inline error string — no page reload needed.

3
Logout.razor — immediate sign-out on initialise

Route: @page "/logout". Override OnInitializedAsync to call AuthService.SignOutAsync() and then NavigationManager.NavigateTo("/"). No user interaction required — navigating to the route triggers the logout immediately.


5 Routing, Authorization & Navigation

Three files need updating to wire auth into the app’s routing and navigation UI. All three live in or reference YourApp.Shared.

1
Protected pages — @attribute [Authorize]

Add the [Authorize] attribute to any Razor page that requires a signed-in user. Inside, call AuthService.GetCurrentUserAsync() in OnInitializedAsync to retrieve the current identity and display user.Identity?.Name.

2
Routes.razor — wrap router in CascadingAuthenticationState

Replace the default <Router> with <CascadingAuthenticationState> wrapping <AuthorizeRouteView>. The <NotAuthorized> template should check context.User.Identity.IsAuthenticated to distinguish “not signed in” from “signed in but lacking permission”.

3
NavMenu.razor — subscribe to auth state changes

Inject AuthenticationStateProvider and implement IDisposable. In OnInitializedAsync, fetch the current auth state and subscribe to AuthenticationStateChanged. Render Login or Logout links based on isAuthenticated. Unsubscribe in Dispose() — the event holds a strong reference and will leak memory if you skip this.


6 Security Hardening Before Shipping

The implementation above is a functional skeleton — suitable for local development and prototyping. Four areas must be addressed before it touches real users.

Password hashing Replace plain-text storage with BCrypt or ASP.NET Core’s IPasswordHasher<T>. Never compare passwords as raw strings in production.
HTTPS enforcement Add app.UseHttpsRedirection() and set SecurePolicy = CookieSecurePolicy.Always. Cookie auth over HTTP exposes session tokens in transit.
Input validation Add DataAnnotations to LoginModel and use <DataAnnotationsValidator> in the form. Validate server-side too — Blazor Server renders on the server, but don’t rely solely on client-side checks.
External auth providers For production, replace the custom user store with OAuth 2.0 or OpenID Connect via Microsoft.AspNetCore.Authentication.OpenIdConnect or Microsoft Identity Web.

GeoSaffer covers .NET architecture, mobile development, and developer tooling. Browse more articles on cross-platform Blazor, MAUI patterns, and backend integration.

More articles on GeoSaffer →

Categories

  • 3D Printing
  • Apps
  • CNC Routing
  • DevOps
  • Electronics
  • Infrastructure
  • Laser Cutting
  • Manufacturing
  • Networking
  • Software
©2026 GeoSaffer.com | WordPress Theme by Superbthemes.com