
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 theClientId.
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:
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:
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):
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:
Or you can use an “open” policy (if your security scenario allows it):
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):
Test Controller
Create a controller to test authorized calls (e.g. AuthController.cs):
[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:
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.
<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:
-
Start the Web API server (make sure it runs on HTTPS).
-
Start the Blazor WASM app.
-
Navigate to the
fetch-dati.razorpage.

