Posted on

Blazor WebAssembly with JWT Authentication on Web Api Server

After configuring authentication (see previous section), the logical next step is to make your Blazor app communicate securely with your Web API server.

The “Handshake” Summary

  • Auth0: Issues the “passport” (Token) to Blazor.

  • Blazor: Takes the passport, puts it into an envelope (HTTP Header), and sends it to the server.

  • Server (JWT): Says: “I’ll check the passport: it’s signed by Auth0 and it’s valid. You may proceed!”


Configuration on Auth0.com

Single Page Applications

Regular Web Applications (such as Blazor Server) and Single Page Applications (such as Blazor WASM) use two completely different approaches to security:

  • Blazor Server (Regular Web App):
    The “secret” lives on the server. It can safely manage client secrets and protected token exchanges behind the scenes.

  • Blazor WASM (SPA):
    The code runs in the user’s browser. It cannot keep secrets (anyone could press F12 and see them). It uses the PKCE flow (without a client secret).

In our case, it is essential that the Auth0 application is configured as a Single Page Application, otherwise Auth0 will not issue an access token.

In summary:

  • ID Token:
    This is your ID card. Blazor uses it to know who you are (name, email). The audience (aud) is correctly set to the ClientId.
    It cannot be used to call APIs.

  • Access Token:
    This is your “pass” to the server. Its audience (aud) must be the URL of your API.
    If this is missing, the API server sees your ID card and says:
    “I don’t care who you are, I need a pass for this resource.”

Make sure your Auth0 application is a Single Page Application.


Scopes in the Auth0 API

Make sure that the scope you request in code is registered in the API:

Auth0 Dashboard → APIs → [Your API] → Permissions tab

Verify that the exact permission leggi:miecose exists; otherwise, add it.


Web API Server

Your server exposing APIs (so far unprotected) is most likely a classic ASP.NET Core Web API project (controller-based).
This is the most solid and common structure for enterprise APIs.

Install JWT Authentication

First, install the component that handles JWT tokens.
Open the terminal in Visual Studio (or the NuGet Package Manager) and install:

Microsoft.AspNetCore.Authentication.JwtBearer

If your project targets .NET 8 and existing packages are at version 8.0.7, installing a 10.0.0 package (which belongs to a future or preview .NET version) will almost certainly cause dependency conflicts and build-time bugs.

Keeping everything on v8.x.x ensures all components speak the same language.


Configuration in Program.cs

Open Program.cs.
We need to add the authentication service and tell the API to use Auth0.

Locate the section where services are registered (before var app = builder.Build();) and paste the following:

using Microsoft.AspNetCore.Authentication.JwtBearer;

#region Auth0
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
    options.Authority = $"https://{builder.Configuration["Auth0:Domain"]}/";
    options.Audience = builder.Configuration["Auth0:Audience"];
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true
    };
});
#endregion

Then scroll down to where the pipeline is configured (after var app = builder.Build();) and add these lines in this exact order (they must be between UseRouting and MapControllers):

app.UseRouting(); // Add this if it’s missing; it helps manage routes properly

app.UseAuthentication(); // Must come BEFORE UseAuthorization

app.UseAuthorization();

app.MapControllers();

CORS

Since your Web API server and your Blazor app are likely running on different ports or domains, the server must explicitly allow Blazor to call it.

In the API server’s Program.cs, make sure CORS is configured:

// In the API server
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.WithOrigins("https://localhost:XXXX") // Your Blazor WASM URL
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

// ... after builder.Build()
app.UseCors();

Or you can use an “open” policy (if your security scenario allows it):

// OPEN TO ALL
builder.Services.AddCors(options =>
{
    options.AddPolicy("OpenPolicy", policy =>
    {
        policy.AllowAnyOrigin() // Accept calls from anyone (including Blazor WASM)
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

Security note:
With Auth0 authentication enabled, even if CORS is “open”, nobody can read protected data without a valid token. You are still safe.


Configure Credentials in appsettings.json

Open appsettings.json in the API project and add the data from the Auth0 dashboard (API section):

{
  "Auth0": {
    "Domain": "[YOUR-TENANT].auth0.com",
    "Authority": "https://[YOUR-TENANT].auth0.com",
    "Audience": "https://api.yourservice.com"
  }
}

Test Controller

Create a controller to test authorized calls (e.g. AuthController.cs):

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
// This API is protected!
[HttpGet("test-private")]
[Authorize]
public IActionResult GetPrivate()
{
return Ok(new { Message = "If you see this, authentication works!" });
}

// This API is public (like your existing ones)
[HttpGet("test-public")]
public IActionResult GetPublic()
{
return Ok(new { Message = "This is a public API." });
}
}

How to Verify It Works

If you try calling the API directly from Swagger after these changes, you should get a 401 Unauthorized error.
(The public endpoints will still return 200 OK as usual.)

That’s the sign that protection is active.


Changes to the Blazor WebAssembly Client

We need to instruct Blazor to use a special MessageHandler.
This component intercepts outgoing requests and automatically adds: Authorization: Bearer <TOKEN>

Configuration in Program.cs

Compared to the previous version, we must force the PKCE flow in Auth0 configuration and add the HTTP client for calls to our server:

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using TestBlazorWasmArticolo;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

// =========================================================
// >>> BASE REGISTRATIONS (Must come before AddOidcAuthentication) <<<
// =========================================================
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<SignOutSessionStateManager>();
// =========================================================

// =========================================================
// >>> Crucial part for Auth0/OIDC authentication <<<
// =========================================================
builder.Services.AddOidcAuthentication(options =>
{
// Audience setting (CRUCIAL for calling APIs)
// The audience must be passed as an additional OIDC parameter
options.ProviderOptions.AdditionalProviderParameters.Add(
"audience",
builder.Configuration["Auth0:Audience"]!
);

// Set the Authority explicitly
options.ProviderOptions.Authority = builder.Configuration["Auth0:Authority"]!;

// Set the ClientId
options.ProviderOptions.ClientId = builder.Configuration["Auth0:ClientId"]!;

// 2. Force the PKCE flow
options.ProviderOptions.ResponseType = "code";

// Add all required scopes, including custom ones
options.ProviderOptions.DefaultScopes.Add("openid");
options.ProviderOptions.DefaultScopes.Add("profile");
options.ProviderOptions.DefaultScopes.Add("email");
options.ProviderOptions.DefaultScopes.Add("address");
options.ProviderOptions.DefaultScopes.Add("phone");
options.ProviderOptions.DefaultScopes.Add("leggi:miecose"); // <--- API scope

// FORCE CONSENT REQUEST
// options.ProviderOptions.AdditionalProviderParameters.Add("prompt", "consent");
});

// =========================================================

#region API
string urlservice = "https://" + builder.Configuration["TestWebservice:Address"] + "/";
builder.Services.AddScoped<AuthorizationMessageHandler>();

// Register HttpClient using the factory
builder.Services.AddHttpClient("ApiGateway", client =>
{
client.BaseAddress = new Uri(urlservice);
})
.AddHttpMessageHandler(sp =>
{
// Configure the handler on the fly
return sp.GetRequiredService<AuthorizationMessageHandler>()
.ConfigureHandler(
authorizedUrls: new[] { urlservice },
scopes: new[] { "leggi:miecose" }
);
});
#endregion

// HttpClient for local calls (weather, etc.)
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

await builder.Build().RunAsync();

ResponseType = "code" tells Blazor and Auth0 to use the PKCE flow, which is the modern standard for SPAs and solves many “lockup” issues after redirects.


Creating the Protected Page

Now create a new Razor page, for example FetchDati.razor.
This page should only be accessible to logged-in users.

@page "/fetch-dati"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.AspNetCore.Components.Authorization
@attribute [Authorize]
@inject IHttpClientFactory ClientFactory
@inject IAccessTokenProvider TokenProvider
@inject NavigationManager Navigation
<PageTitle>Protected Data</PageTitle>

<AuthorizeView>
<Authorized>
<h3>Data from Server</h3>
@if (messaggioDalServer == null)
{
<p>Loading data...</p>
}
else
{
<div class="alert alert-success">@messaggioDalServer</div>
}

<h3>API Gateway Test</h3>
<button class="btn btn-success" @onclick="CaricaDatiConToken">
Load Data with Token
</button>

<p>@messaggioDalServer</p>
</Authorized>

<Authorizing>
<p>Checking authorization...</p>
</Authorizing>
</AuthorizeView>

@code {
private string? messaggioDalServer = "hello";

protected override async Task OnInitializedAsync()
{
try
{
var client = ClientFactory.CreateClient("ApiGateway");

// First, try a very simple call to check if the token is the issue
var response = await client.GetAsync("api/Auth/test-private");

if (response.IsSuccessStatusCode)
{
messaggioDalServer = await response.Content.ReadAsStringAsync();
}
else
{
messaggioDalServer = $"Server error: {response.StatusCode}";
}
}
catch (AccessTokenNotAvailableException ex)
{
// If the token fails, redirect
ex.Redirect();
}
catch (Exception ex)
{
// Network or other error: log it but don’t block the app
messaggioDalServer = $"Technical error: {ex.Message}";
Console.WriteLine(ex.ToString());
}
}

private async Task CaricaDatiConToken()
{
// 1. Request the token manually
var tokenResult = await TokenProvider.RequestAccessToken();

if (tokenResult.TryGetToken(out var token))
{
try
{
var client = ClientFactory.CreateClient("ApiGateway");

// 2. Manually add the token to the header
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer", token.Value);

var response = await client.GetAsync("api/auth/test-private");
messaggioDalServer = await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
messaggioDalServer = $"Call error: {ex.Message}";
}
}
else
{
// This tells you WHY it failed (e.g. "consent_required", "invalid_scope")
if (tokenResult.Status == AccessTokenResultStatus.RequiresRedirect)
{
messaggioDalServer = "Error: a redirect is required for consent.";
Console.WriteLine("tokenResult.InteractiveRequestUrl: "
+ tokenResult.InteractiveRequestUrl);

// Navigation.NavigateTo(tokenResult.InteractiveRequestUrl);
// Uncomment if you want to force consent
}
else
{
messaggioDalServer = $"GetToken error: {tokenResult.Status}";
}
}
}

public class ApiResponse
{
public string Message { get; set; } = "";
}
}

What to Do Now

The “engine” is installed and synchronized.
Now all that’s left is to start it:

  1. Start the Web API server (make sure it runs on HTTPS).

  2. Start the Blazor WASM app.

  3. Navigate to the fetch-dati.razor page.

Posted on

Blazor WebAssembly with Auth0 Authentication

Focus on Blazor WebAssembly (WASM)

Blazor WebAssembly is the most suitable option for creating client-side applications that consume APIs.

Key Points

  • C# in the Browser: The entire .NET runtime and your application are downloaded by the browser as WebAssembly files. C# code runs directly in the browser within a secure sandbox.

  • SPA Architecture: Blazor WASM creates Single Page Applications (SPA), meaning the app loads once and subsequent user interactions dynamically update only the necessary parts of the page, providing a smooth user experience.

  • Razor Components: User interfaces are built using Razor components (.razor), which combine HTML markup with C# logic (using the @code directive).

  • Open Standards: Building a Blazor app means using Microsoft technology (C#) built upon a totally open and universal foundation (WebAssembly). This ensures your app works on any modern device without depending on proprietary plugins.

Focus on Auth0

  • Auth0 is an Identity Provider that utilizes the OIDC (OpenID Connect) protocol.

  • Blazor WASM is a native OIDC client, and Microsoft provides the Microsoft.AspNetCore.Components.WebAssembly.Authentication libraries to handle it.

  • The Blazor application redirects the user to the Auth0 login page (Universal Login), receives the Access Token, and stores it securely.

Blazor WebAssembly Project

Creating the Project in Visual Studio

  1. Open Visual Studio and create a New Project.

  2. Search for and select the “Blazor WebAssembly App” template (ensure it is not “Blazor Server App”).

  3. In the configuration window, under Authentication, choose “None”.

✅ Summary of Recommended Settings

Option Status Notes
Progressive Web Application (PWA) Unchecked Not necessary for now; reduces complexity.
Do not use top-level statements Unchecked Uses modern, concise .NET code.
Configure for HTTPS Checked Essential. Your app must use HTTPS for Auth0 and API calls.
Include sample pages Checked Useful for base architecture (layout, menu) and quick testing.

Configuration on Auth0.com

1. Application

In the Auth0 portal, create a new application of type “Single Page Application” . Under the Settings tab:

  • Note down: Domain and Client ID.

  • Enable “Cross-Origin Authentication”.

2. API

In the API section, click + Create API. Complete these fields:

  • Name: A descriptive name (e.g., “My Data Service API”).

  • Identifier: This is the Audience!

The Identifier (Audience) Field

The Identifier (Audience) must be a URI (Uniform Resource Identifier) and usually follows this format:

  • Example Audience: https://api.yourdomain.com

  • Important: It doesn’t have to be a working URL, but it must be a unique URI.

Auth0 Configuration and Dependencies

Packages

To authenticate a Blazor WASM app, use Microsoft’s built-in OIDC support. Install these via NuGet:

  1. Microsoft.Authentication.WebAssembly.Msal

  2. Microsoft.AspNetCore.Components.WebAssembly.Authentication (Version 8.0.1)

  3. Microsoft.Extensions.Http

Application Configuration

Create appsettings.json in the wwwroot directory to keep configurations separate from code (Best Practice).

{
  "Auth0": {
    "Domain": "[YOUR-TENANT].auth0.com",
    "Authority": "https://[YOUR-TENANT].auth0.com",
    "ClientId": "[YOUR-AUTH0-CLIENT-ID]",
    "Audience": "https://api.yourdomain.com"
  },
  "TestWebservice": {
    "Address": "localhost:7003"
  }
}

Core Application Files

Program.cs

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

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

// =========================================================
// >>> BASE REGISTRATIONS (Must come before AddOidcAuthentication) <<<
// =========================================================
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<SignOutSessionStateManager>();
// =========================================================

// =========================================================
// >>> Crucial part for Auth0/OIDC Authentication <<<
// =========================================================
builder.Services.AddOidcAuthentication(options =>
{
    // Audience setting (CRUCIAL for calling APIs)
    // The Audience must be passed as an additional OIDC parameter
    options.ProviderOptions.AdditionalProviderParameters.Add(
        "audience",
        builder.Configuration["Auth0:Audience"]!
    );

    // Explicitly assign the Authority
    options.ProviderOptions.Authority = builder.Configuration["Auth0:Authority"]!;

    // Assign the ClientId
    options.ProviderOptions.ClientId = builder.Configuration["Auth0:ClientId"]!;

    // Scope Configuration
    options.ProviderOptions.DefaultScopes.Clear(); // Remove default scopes

    // Add all required scopes, including custom ones:
    options.ProviderOptions.DefaultScopes.Add("openid");
    options.ProviderOptions.DefaultScopes.Add("profile");
    options.ProviderOptions.DefaultScopes.Add("email");
    options.ProviderOptions.DefaultScopes.Add("address");
    options.ProviderOptions.DefaultScopes.Add("phone");
    options.ProviderOptions.DefaultScopes.Add("read:appointments"); // <--- API Scope
});
// =========================================================

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();

MainLayout.razor

To display the login status, use the built-in CascadingAuthenticationState component.

In Layout/MainLayout.razor, wrap the content as follows:

@using Microsoft.AspNetCore.Components.Authorization
@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <CascadingAuthenticationState>
        <main>
            <div class="top-row px-4">
                <LoginDisplay /> 
                <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
            </div>

            <article class="content px-4">
                @Body
            </article>
        </main>
    </CascadingAuthenticationState>
</div>

LoginDisplay.razor

Create a Shared folder and add the LoginDisplay.razor component.

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity?.Name!
        <button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? AuthenticationState { get; set; }

    private async Task BeginSignOut()
    {
        // Redirect user to the Auth0 Logout flow (via Blazor handler)
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

Note: Add the @using YourProjectName.Shared to _Imports.razor.

Authentication Script (index.html)

Add the missing <script> tag in your index.html inside the <body> section, before the Blazor framework script:

<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>

Authentication.razor

Create Pages/Authentication.razor:

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
    [Parameter]
    public string? Action { get; set; }
}

Auth0.com Configuration (Part 2)

Upon launching the app, the “Login” link will appear after a slight delay. Clicking it might trigger “Checking login state…” followed by a “Callback URL mismatch” error.

To fix this, go to your Auth0 Application Settings and add:

  • Allowed Callback URLs: https://localhost:7004/authentication/login-callback

  • Allowed Logout URLs: https://localhost:7004/authentication/logout-callback

(Ensure the port matches your local project).

Now, Login and Logout should work perfectly!