A few evenings into my Blazor app, I hit a simple decision: I don’t want to run local authentication. Without password resets, no user tables, no goodbye emails. I want a provider I can plug in and move on. I chose Auth0.

Below is the small, reliable setup I’m using: start from a clean Blazor Web App (Interactive Server), then add two Razor Pages for the Auth0 handshake. That’s it.

What we’re going to build

  • Blazor Web App (.NET 8), Interactive Server
  • Auth routes at /auth/login and /auth/logout (Razor Pages, not components)
  • Cookie-based sign-in via Auth0 (OIDC)
  • Login/Logout buttons in the layout with <AuthorizeView>
  • A couple of guardrails for proxies and HTTPS

Create a clean Blazor app

dotnet new blazor -n Auth0Blazor.NET -int Server --empty

cd Auth0Blazor.NET

dotnet run

Open the URL from the console. You have an interactive Blazor app without authentication.

Create your Auth0 account (or log in)

  • Create an Auth0 account and choose a tenant name/region (e.g., EU).
    Your tenant domain will look like your-tenant.eu.auth0.com (region may vary).
  • The tenant domain is permanent, so pick something you’re happy with.

Create the application

In Dashboard → Applications → Create Application:

  • Name: Auth0Blazor.NET (or your app name)
  • Application Type: Regular Web Application
  • Click Create.

Open the Settings tab and copy

  • Domain (e.g., your-tenant.eu.auth0.com)
  • Client ID
  • Client Secret

Configure allowed URLs

Add these and adjust to your ports/domains:

  • Allowed Callback URLs:
    https://localhost:<PORT>/callback
  • Allowed Logout URLs:
    https://localhost:<PORT>/
  • Allowed Web Origins:
    https://localhost:<PORT>

Click Save Changes.

Why /callback? The Auth0 ASP.NET Core package is used /callback by default for the sign-in redirect. We’ll keep that default to avoid extra plumbing.

Enable at least one connection

You need a way for users to log in:

  • Authentication → Database: enable Username-Password-Authentication and (optionally) create a test user.

Install packages

dotnet add package Auth0.AspNetCore.Authentication

Add configuration values

Use User Secrets in dev (recommended):

dotnet user-secrets init

dotnet user-secrets set "Auth0:Domain" "YOUR_TENANT.eu.auth0.com"
dotnet user-secrets set "Auth0:ClientId" "YOUR_CLIENT_ID"
dotnet user-secrets set "Auth0:ClientSecret" "YOUR_CLIENT_SECRET"

You can also mirror these keys in appsettings.json for production or manage them as secrets on Fly.io if you deploy there.

Wire it up in Program.cs

using Auth0.AspNetCore.Authentication;
using Auth0Blazor.NET.Components;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

// Razor Pages for /auth/* endpoints
builder.Services.AddRazorPages(options =>
{
    options.RootDirectory = "/Components"; // Override the default root directory from /Pages to /Components
});

builder.Services.AddHttpContextAccessor();

// Auth0 (OIDC) + cookie sign-in
builder.Services.AddAuth0WebAppAuthentication(options =>
{
    options.Domain = builder.Configuration["Auth0:Domain"]!;
    options.ClientId = builder.Configuration["Auth0:ClientId"]!;
    options.ClientSecret = builder.Configuration["Auth0:ClientSecret"];
    // If/when you need API tokens: options.Audience = "...";
});

// Override the default cookie authentication options
builder.Services.PostConfigure<CookieAuthenticationOptions>(
    CookieAuthenticationDefaults.AuthenticationScheme,
    o =>
    {
        o.LoginPath = "/auth/login";
        o.LogoutPath = "/auth/logout";
    });

builder.Services.AddAuthorization();

var app = builder.Build();

// If deploying behind a proxy (Nginx, Fly.io, etc.)
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor
});

app.UseHttpsRedirection(); // if you want redirection as well

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    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.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorPages(); // will host /auth/login and /auth/logout (cshtml)

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

Why Razor Pages for /auth/* The Auth0 login/logout flow uses full-page redirects handled by ASP.NET Core authentication middleware. Keeping these as .cshtml pages avoids Blazor circuit state during the handshake and keeps things simple.

Enable auth state for components

We want every routed .razor page to have access to AuthenticationState, so <AuthorizeView> and [Authorize] work as expected. Put the cascade inside Routes.razor, right around the RouteView. This keeps your /auth/*.cshtml Razor Pages separate (they don’t use Blazor’s cascade anyway).

Components/Routes.razor

@using Auth0Blazor.NET.Components.Layout
@using Microsoft.AspNetCore.Components.Authorization

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
                <NotAuthorized>
                    
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
    </Router>
</CascadingAuthenticationState>

Add the two auth endpoints

Pages/Auth/Login.cshtml

@page "/auth/login"
@using System.Diagnostics

@using Microsoft.AspNetCore.Authentication
@using Auth0.AspNetCore.Authentication

@functions {
    public async Task OnGetAsync()
    {
        var redirectUri = string.IsNullOrWhiteSpace(Request.Query["redirectUri"])
            ? "/" // fallback
            : Request.Query["redirectUri"].ToString();

        await HttpContext.ChallengeAsync(Auth0Constants.AuthenticationScheme, new AuthenticationProperties
        {
            RedirectUri = redirectUri
        });
    }
}

Pages/Auth/Logout.cshtml

@page "/auth/logout"
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@using Auth0.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authorization

@attribute [Authorize]

@functions {
    public async Task OnGetAsync()
    {
        var redirectUri = string.IsNullOrWhiteSpace(Request.Query["redirectUri"])
            ? "/" // fallback
            : Request.Query["redirectUri"].ToString();

        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

        await HttpContext.SignOutAsync(Auth0Constants.AuthenticationScheme, new AuthenticationProperties
        {
            RedirectUri = redirectUri
        });
    }
}

Add a minimal Home page to test auth

Update Components/Pages/Home.razor (or Pages/Home.razor depending on your template) with a simple greeting, Login/Logout buttons, and an optional claims viewer.

@page "/"
@using Microsoft.AspNetCore.Components.Authorization
@inject NavigationManager Nav

<PageTitle>Home</PageTitle>

<AuthorizeView Context="auth">
    <Authorized>
        <h1>Hello, @GetDisplayName(auth.User) 👋</h1>
        <p>You’re signed in.</p>

        <button @onclick="Logout">Logout</button>

        <details style="margin-top:1rem">
            <summary>Show my claims</summary>
            <table>
                <thead><tr><th style="text-align:left">Type</th><th style="text-align:left">Value</th></tr></thead>
                <tbody>
                @foreach (var c in auth.User.Claims)
                {
                    <tr><td>@c.Type</td><td>@c.Value</td></tr>
                }
                </tbody>
            </table>
        </details>
    </Authorized>

    <NotAuthorized>
        <h1>Welcome</h1>
        <p>You’re not signed in yet.</p>
        <button @onclick="Login">Login</button>
    </NotAuthorized>
</AuthorizeView>

@code {
    void Login()
    {
        var redirect = Uri.EscapeDataString(Nav.ToBaseRelativePath(Nav.Uri));
        Nav.NavigateTo($"/auth/login?redirectUri={redirect}", forceLoad: true);
    }

    void Logout()
    {
        var redirect = Uri.EscapeDataString(Nav.ToBaseRelativePath(Nav.Uri));
        Nav.NavigateTo($"/auth/logout?redirectUri={redirect}", forceLoad: true);
    }

    static string GetDisplayName(System.Security.Claims.ClaimsPrincipal user)
        => user.Identity?.Name
           ?? user.FindFirst("name")?.Value
           ?? user.FindFirst("email")?.Value
           ?? "User";
}

Now the app is ready to run. Start it with dotnet run (or F5), open https://localhost:<PORT>, and click Login. You’ll bounce to Auth0’s Universal Login, sign in, and land back on / with your name and a Logout button—no local user tables involved, just the auth cookie. If you deploy to Fly.io, set the three Auth0 secrets and make sure your Allowed Callback URL includes your deployed domain plus /callback.

Wrapping it up

I didn’t want my Blazor app to turn into an identity project. Auth0 let me add sign-in with a couple of secrets and two tiny Razor Pages—no passwords to store, no local user tables, no fragile callbacks. For a small app or demo, the free plan is sufficient; I can ship, learn, and iterate without setting up Keycloak or paying for a subscription. If I outgrow it later, I’ll reassess. For now, it just works.

I’ve published the full source code on GitHub, in case you want to explore or reuse it: Auth0Blazor.NET

Tagged in:

, ,