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

Implementing Authentication in a .NET 9 MAUI Blazor Hybrid App

Posted on November 18, 2024April 20, 2026
.NET 9 MAUI Blazor Hybrid authentication architecture diagram
Mobile · .NET · Authentication

Implementing Authentication in a .NET 9 MAUI Blazor Hybrid App A shared-project pattern for web and mobile

18 November 2024 · GeoSaffer.com

MAUI Blazor Hybrid lets you share Razor components between a web app and a native mobile app — but hooking up authentication across both targets requires care. This guide walks through a complete, working implementation using a Shared class library, a custom AuthenticationStateProvider, and optional secure storage persistence.

1
Solution Architecture

The solution splits across three projects so that authentication logic, components, and pages live once in YourApp.Shared and are consumed by both the web and mobile targets without duplication.

YourApp.Web

Blazor Web App project — hosts the server-side rendered entry point, wires up Blazor Hub, and registers scoped services for the web runtime.

YourApp.Mobile

.NET MAUI Blazor Hybrid project — hosts the native shell on Android, iOS, and Windows, with a BlazorWebView rendering shared Razor pages.

YourApp.Shared

Class library — owns all Razor pages, layout components, services, and the authentication state provider. Both targets reference this project.

Both YourApp.Web and YourApp.Mobile add a project reference to YourApp.Shared and register the shared services in their respective startup files. The authentication flow is identical on both platforms.


2
Prerequisites & NuGet Packages

Before writing any code, ensure your environment and the Shared project have the correct dependencies.

Environment

  • .NET 9 SDK installed
  • Visual Studio 2022 (17.7 or later)
  • MAUI workload via dotnet workload install maui
  • All projects target net9.0 in their .csproj

YourApp.Shared — NuGet packages

  • 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 YourApp.Shared/_Imports.razor so every Razor file in the shared library can use auth and routing primitives without per-file imports:

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

3
Custom Authentication State Provider

Blazor’s auth pipeline is built around AuthenticationStateProvider. Subclass it in YourApp.Shared/Services/ to give both platforms a single source of truth for the current user identity.

YourApp.Shared / Services / CustomAuthenticationStateProvider.cs
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
    private ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());
    private ClaimsPrincipal _currentUser;

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
        => Task.FromResult(new AuthenticationState(_currentUser ?? _anonymous));

    public void SignIn(string username)
    {
        var identity = new ClaimsIdentity(
            new[] { new Claim(ClaimTypes.Name, username) }, "apiauth_type");
        _currentUser = new ClaimsPrincipal(identity);
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public void SignOut()
    {
        _currentUser = _anonymous;
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }
}

Register it in both startup files. The pattern is identical — the provider is registered as a scoped service, then aliased so the framework’s AuthenticationStateProvider injection resolves to your custom type:

YourApp.Web — Program.cs

  • Call AddRazorPages() and AddServerSideBlazor()
  • Register CustomAuthenticationStateProvider as scoped
  • Alias AuthenticationStateProvider to resolve the custom type
  • Call AddAuthorizationCore()
  • Map Blazor Hub and fallback page

YourApp.Mobile — MauiProgram.cs

  • Call AddMauiBlazorWebView()
  • Register CustomAuthenticationStateProvider as scoped
  • Alias AuthenticationStateProvider to resolve the custom type
  • Call AddAuthorizationCore()
  • Register UserService as scoped

Create a companion UserService in the same Services folder with a simple in-memory credential list. In production, replace AuthenticateUserAsync with a call to your identity API — the interface contract stays the same.


4
Login & Logout Components

Both components live in YourApp.Shared/Pages/ and are automatically available to web and mobile. Login.razor uses an EditForm with model binding; Logout.razor is a headless page that signs out immediately on initialisation.

YourApp.Shared / Pages / Login.razor — key logic
@page "/login"
@inject CustomAuthenticationStateProvider AuthStateProvider
@inject UserService UserService
@inject NavigationManager NavigationManager

<EditForm Model="loginModel" OnValidSubmit="HandleLogin">
    <InputText @bind-Value="loginModel.Username" />
    <InputText type="password" @bind-Value="loginModel.Password" />
    <button type="submit">Login</button>
</EditForm>

@code {
    private async Task HandleLogin()
    {
        var user = await UserService.AuthenticateUserAsync(
            loginModel.Username, loginModel.Password);
        if (user != null)
        {
            AuthStateProvider.SignIn(user.Username);
            NavigationManager.NavigateTo("/");
        }
        else { ErrorMessage = "Invalid username or password."; }
    }
}

Logout.razor is even simpler — inject AuthStateProvider, call SignOut() in OnInitialized, then navigate to /. No template content needed beyond the redirect.


5
Route Protection & Navigation

Protecting a page requires just one attribute; the router handles the rest via CascadingAuthenticationState and AuthorizeRouteView.

1

Decorate protected pages

Add @attribute [Authorize] at the top of any Razor page that requires authentication. No other changes needed in the component itself.

2

Update Routes.razor

Wrap the <Router> in <CascadingAuthenticationState> and replace RouteView with AuthorizeRouteView. Supply a <NotAuthorized> slot that renders a <RedirectToLogin /> component for unauthenticated users.

3

Create RedirectToLogin.razor

A headless component in YourApp.Shared/Components/ — call NavigationManager.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}") inside OnInitialized.

4

Reactive NavMenu

In NavMenu.razor subscribe to AuthenticationStateProvider.AuthenticationStateChanged, update isAuthenticated and userName, then call StateHasChanged(). Unsubscribe in IDisposable.Dispose(). Show Login or Logout links conditionally based on isAuthenticated.


6
Persisting State & Hardening Security

The basic provider loses session state on app restart. The extended version uses SecureStorage on mobile and localStorage on web — both via a compile-time platform check.

Mobile (Android / iOS / Windows)

  • Read on startup: SecureStorage.Default.GetAsync(key)
  • Write on sign-in: SecureStorage.Default.SetAsync(key, json)
  • Clear on sign-out: SecureStorage.Default.Remove(key)
  • Guarded by #if ANDROID || IOS || WINDOWS compile directive

Web (Blazor Server)

  • Inject IJSRuntime into the provider constructor
  • Read: _jsRuntime.InvokeAsync<string>("localStorage.getItem", key)
  • Write: _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, json)
  • Clear: InvokeVoidAsync("localStorage.removeItem", key)

Beyond persistence, three baseline security measures should be in place before any real users authenticate:

Password Hashing Never store plain-text passwords. Use BCrypt.Net or ASP.NET Core’s PasswordHasher<T> to hash credentials at rest.
HTTPS Enforcement Call app.UseHttpsRedirection() and ensure certificates are configured in all deployment environments.
Input Validation Use DataAnnotationsValidator on all forms and validate on the server side — never trust client-only validation in a hybrid app.
OAuth / OIDC Migration Replace the in-memory user store with an identity provider (Microsoft Entra, Auth0, Keycloak) once the basic flow is validated.

GeoSaffer covers practical .NET, mobile, and cross-platform development patterns — from architecture decisions to deployment pipelines. Browse more tutorials on the blog.

Explore GeoSaffer →

Categories

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