ASP.NET Core supporta la configurazione e gestione della sicurezza nelle applicazioni Blazor: tuttavia le applicazioni Blazor Server e Blazor WebAssembly differiscono completamente nel contesto della sicurezza.
Poiché un’app Blazor Server gira sul server possiamo inserire dei controlli di autorizzazione per stabilire:
- le opzioni di user interface (UI) disponibili per un utente (ad esempio la visualizzazione o meno di una voce di menù)
- regole di accesso per le aree e i componenti dell’app
Le app Blazor WebAssembly invece girano sul client, quindi l’autorizzazione può essere usata solo per determinare quali opzioni di UI mostrare, dato che qualsiasi controllo client-side può essere facilmente modificato o by-passato da un utente. Questo comunque non è una limitazione di Blazor WASM, ma di tutti i framework per le Single Page Application.
L’approccio più comune per risolverlo è quello di usare una implementazione del protocollo OAuth 2.0, come ad esempio OpenID Connect (OIDC). Blazor WebAssembly supporta l’autenticazione e autorizzazione con OIDC attraverso la libreria AspNetCore.Components.WebAssembly.Authentication, che sicuramente permette una autenticazione semplice a backend scritti con ASP.NET Core, ma può anche autenticare su qualsiasi Identity provider di terze parti che supporti OIDC.
Questa scelta ci porta quasi sempre a un’autenticazione basata su token JSON Web (JWT) piuttosto che una basata su cookie. Una lista delle motivazioni funzionali e di sicurezza che supportano tale scelta può essere trovata a questo link ufficiale della Microsoft.
Comunque Blazor usa meccanismi di autenticazione esistenti in ASP.NET Core per stabilire l’identità di un utente. Una app WebAssembly, come detto prima, non può eseguire controlli relativi all’autenticazione: quindi occorre usare un servizio chiamato AuthenticationStateProvider di cui parleremo tra breve.
AuthenticationStateProvider, AuthorizeView e CascadingAuthenticationState
AuthorizeView è un componente nativo di Blazor che permette di visualizzare contenuto in una pagina in base allo stato di autenticazione dell’utente.
<AuthorizeView>
<Authorized>
<h4>Ciao, @context.User.Identity.Name!</h4>
<p>Contenuto visibile se l’utente è autenticato</p>
</Authorized>
<NotAuthorized>
<p>Non sei autenticato</p>
</NotAuthorized>
</AuthorizeView>
Il componente, alimentato proprio da AuthenticationStateProvider, fornisce due frammenti dall’ovvio significato: Authorized e NotAuthorized ed espone una variable chiamata context di tipo AuthenticationState che serve a mostrare informazioni sull’utente collegato. Possiamo, ad esempio, mostrare i ClaimsPrincipal (non temete, verranno introdotti tra poco).
<AuthorizeView>
<Authorized>
<h4>Ciao, @context.User.Identity.Name!</h4>
<p>Contenuto visibile se l’utente è autenticato</p>
@if (context.User.Claims.Count() > 0)
{
<ul>
@foreach (var claim in context.User.Claims)
{
<li>@claim.Type: @claim.Value</li>
}
</ul>
}
</Authorized>
<NotAuthorized>
<p>Non sei autenticato</p>
</NotAuthorized>
</AuthorizeView>

L’effettiva propagazione dello stato di autenticazione lungo l’intera applicazione è garantita circondando un qualsiasi elemento della nostra UI con il componente <CascadingAuthenticationState>. Possiamo farlo, ad esempio, nel componente App.Razor
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
….
</Router>
</CascadingAuthenticationState>
Se abbiamo bisogno dello stato di autenticazione all’interno del codice di un nostro componente possiamo inserire in esso un parametro [CascadingParameter] di tipo Task<AuthenticationState>
@code {
[CascadingParameter] Task<AuthenticationState> authenticationStateTask { get; set; }
….
async Task LogUsername()
{
var authState = await authenticationStateTask;
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
// Since the user is a ClaimsPrincipal, you can also enumerate claims,
// evaluate membership in roles, etc.
message = $"Ciao, {user.Identity.Name}";
}
else
{
message = "Non sei autenticato!";
}
}
....
Si può anche iniettare direttamente in un componente il servizio AuthenticationStateProvider. Il problema principale di questo approccio è che il componente non sarà notificato automaticamente se lo stato di autenticazione dovesse cambiare.
@inject AuthenticationStateProvider authenticationStateProvider
…
private async Task GetClaimsPrincipalData()
{
var authState = await authenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
_authMessage = $"{user.Identity.Name} è autenticato.";
_claims = user.Claims;
}
else
{
_authMessage = "L'utente non è autenticato ";
}
}
E’ possibile creare una implementazione custom della classe AuthenticationStateProvider. Ciò non è consigliato nel caso di applicazioni Blazor Server visto che quella nativa si integra già con ASP.NET Core. Nel caso di applicazioni Blazor WASM, invece, questa è una pratica abbastanza comune quando volete integrare qualsiasi sistema esterno di autenticazione indipendentemente dal vostro codice lato server. E’ possibile ad esempio partire da una implementazione di questo tipo per poi modificarla in base alle vostre esigenze.
public class FakeAuthenticationStateProvider : AuthenticationStateProvider
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "Utente finto"),
}, "Tipo finto di autenticazione");
var user = new ClaimsPrincipal(identity);
return Task.FromResult(new AuthenticationState(user));
}
}
Questa classe va registrata col motore di dependency injection in Program.cs
builder.Services.AddScoped<AuthenticationStateProvider, FakeAuthenticationStateProvider>();
Come conseguenza, tutti gli utenti saranno considerati autenticati con lo username Utente finto.
La demo
E’ il momento di mostrare un esempio concreto. Partiremo da un’applicazione vuota contenente un progetto ASP.NET Web API con la versione 6.0 del framework ,che utilizzerà un database SQLite, Entity Framework Core e ASP.NET Identity e aggiungeremo una app Blazor WASM, che userà questa API per registrare un nuovo utente e consentirgli di autenticarsi.
Lato client, faremo anche uso del Local Storage di HTML 5 per conservare il token alla chiusura o al refresh del browser. Inseriamo anche una libreria di classi per tutto il codice condiviso. Vi sorprenderà ma eviterò i template col supporto all’autenticazione: preferisco sempre partire da zero.
Riassumendo, ecco la soluzione vuota

a cui aggiungiamo la API Web ASP.NET Core

e l’applicazione Blazor WASM (ho scelto il supporto alle app progressive)

e infine la libreria condivisa

Nel complesso avremo quindi

Partiamo dal progetto API aggiungendo tutte le dipendenze necessarie, che vado a mostrare di seguito come appaiono nel file DemoSicurezza.API.csproj
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>
Il passo successivo è creare in un folder chiamato Data una classe derivata da IdentityDbContext che verrà utilizzata dall’applicazione mediante Entity Framework Core.
public class ApplicationDbContext: IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
Cominciamo a configurare l’applicazione nella classe Program (vi ricordo che in .NET 6 è sparita la classe Startup.cs). La configurazione CORS ovviamente è pensata per una demo locale. C’è poi la configurazione di SQLite e di ASP.NET Core Identity
builder.Services.AddCors(options =>
{
options.AddPolicy("MyPolicy", builder =>
{
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
});
});
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("MyDb")));
builder.Services.AddControllers();
builder.Services.AddDefaultIdentity<IdentityUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
Inseriamo poi in app.settings.json i parametri necessari per la stringa di connessione al database e per il token JWT.
{
"ConnectionStrings": {
"MyDb": "Data Source = ./Data/mydb.db"
},
"Jwt": {
"SecurityKey": "miachiavesupersicura",
"Issuer": "mydomain.com",
"Audience": "mydomain.com",
"AccessTokenExpirationMinutes": "60",
"RefreshTokenExpirationMinutes": "60"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
e configuriamo in Program.cs il servizio di Autenticazione
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
builder.Configuration["Jwt:SecurityKey"])),
RequireExpirationTime = true,
ClockSkew = TimeSpan.Zero
};
});
Completiamo il lavoro con la registrazione degli ultimi servizi e l’aggiunta dei middleware necessari
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddRouting(options => options.LowercaseUrls = true);
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseRouting();
app.UseCors("MyPolicy");
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
app.MapControllers();
});
app.Run();
Creiamo il database SQLite utilizzando i tool di Entity Framework Core aprendo un terminale nel folder del progetto Api e digitando i seguenti comandi:
dotnet ef migrations add PrimaMigrazione -o Data/Migrations
dotnet ef database update
Verifichiamo con un qualsiasi applicativo che apra database SQLite la creazione del database con tutte le tabelle necessarie

Passiamo ora a un Controller che ci servirà per gestire la registrazione e l’autenticazione dell’utente. Prima di farlo, però, nel progetto Shared in un folder chiamato Models andiamo a creare una classe (per semplicità ridotta al minimo) per gestire la registrazione dell’utente.
public class RegisterRequest
{
[Required]
[EmailAddress]
[Display(Name = "Email address")]
public string Email { get; set; } = String.Empty;
[Required]
[DataType(DataType.Password)]
[StringLength(50, ErrorMessage = "La password deve essere lunga tra {2} e {1} caratteri", MinimumLength = 6)]
public string Password { get; set; } = String.Empty;
}
Ecco invece il codice del Controller con tutte le iniezioni di dipendenze necessarie
[Route("api/[controller]")]
[ApiController]
public class AccountsController : ControllerBase
{
private readonly SignInManager<IdentityUser> signInManager;
private readonly UserManager<IdentityUser> userManager;
private readonly IConfiguration configuration;
public AccountsController(SignInManager<IdentityUser> signInManager,
UserManager<IdentityUser> userManager, IConfiguration configuration)
{
this.signInManager = signInManager;
this.userManager = userManager;
this.configuration = configuration;
}
….
}
e il codice per la registrazione dell’utente
[HttpPost]
[AllowAnonymous]
[Route("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest user)
{
IdentityUser identityUser = new IdentityUser
{
Email = user.Email,
UserName = user.Email
};
IdentityResult identityResult = await userManager.CreateAsync(identityUser, user.Password);
if (identityResult.Succeeded == true)
{
return StatusCode( StatusCodes.Status201Created, new { identityResult.Succeeded });
}
else
{
string errorsToReturn = "Registrazione fallita";
foreach (var error in identityResult.Errors)
{
errorsToReturn += Environment.NewLine;
errorsToReturn += $"Codice di errore: {error.Code}, {error.Description}";
}
return StatusCode(StatusCodes.Status500InternalServerError,
errorsToReturn);
}
Il codice dell’autenticazione restituisce in caso di successo un token JWT. Abbiamo quindi bisogno di un metodo che lo generi. Piuttosto che avere un servizio dedicato, aggiungiamolo alla classe Controller sfruttando un’annotazione specifica.
[NonAction]
[ApiExplorerSettings(IgnoreApi = true)]
private async Task<string> GeneraJSONWebToken(IdentityUser identityUser)
{
SymmetricSecurityKey symmetricSecurityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["Jwt:SecurityKey"]));
SigningCredentials credentials = new SigningCredentials(
symmetricSecurityKey, SecurityAlgorithms.HmacSha256);
IList<string> roleNames = await userManager.GetRolesAsync(identityUser);
var claims = new List<Claim>()
{
new Claim(ClaimTypes.NameIdentifier, identityUser.Id),
new Claim(ClaimTypes.Name, identityUser.Email),
new Claim(ClaimTypes.Email, identityUser.Email),
new Claim(JwtRegisteredClaimNames.Sub, identityUser.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
}.Union(roleNames.Select(role => new Claim(ClaimTypes.Role, role)));
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(
configuration["Jwt:Issuer"],
configuration["Jwt:Audience"],
claims, DateTime.UtcNow, DateTime.UtcNow.AddMinutes(10),
credentials
);
return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}
Ecco la action di autenticazione
[HttpPost]
[Route("signin")]
[AllowAnonymous]
public async Task<IActionResult> SignIn([FromBody] RegisterRequest user)
{
Microsoft.AspNetCore.Identity.SignInResult signInResult = await signInManager.PasswordSignInAsync
(user.Email, user.Password, false, false);
if (signInResult.Succeeded == true)
{
IdentityUser identityUser = await userManager.FindByEmailAsync(user.Email);
string JSONWebTokenAsString = await GeneraJSONWebToken(identityUser);
return Ok(JSONWebTokenAsString);
}
else
{
return Unauthorized(user);
}
}
Possiamo avviare il progetto API e verificare grazie all’interfaccia OpenAPI la corretta registrazione dell’utente


Possiamo finalmente passare al progetto WASM. Partiamo anche qui dall’installazione di tutti i pacchetti nuget necessari.
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.1" PrivateAssets="all" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.15.0" />
<PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
</ItemGroup>
Creiamo quindi una classe derivata da AuthenticationStateProvider. A titolo di esempio, inietteremo in essa il servizio per scrivere nello storage locale di HTML 5 (grazie al pacchetto Blazored.LocalStorage)
public class AppAuthenticationStateProvider: AuthenticationStateProvider
{
private readonly ILocalStorageService localStorageService;
public AppAuthenticationStateProvider(
ILocalStorageService localStorageService)
{
this.localStorageService = localStorageService;
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
throw new NotImplementedException();
}
}
Nel metodo GetAuthenticationStateAsync di cui dobbiamo eseguire l’override andiamo a verificare l’esistenza del token nello storage locale e che non sia scaduto.
private readonly JwtSecurityTokenHandler jwtSecurityTokenHandler = new();
…
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
string savedToken = await localStorageService.GetItemAsync<string>
("bearerToken");
if (string.IsNullOrWhiteSpace(savedToken))
{
return new AuthenticationState(
new ClaimsPrincipal(
new ClaimsIdentity()));
}
JwtSecurityToken jwtSecurityToken =
jwtSecurityTokenHandler.ReadJwtToken(savedToken);
DateTime expires = jwtSecurityToken.ValidTo;
if (expires < DateTime.UtcNow)
{
await localStorageService.RemoveItemAsync("bearerToken");
return new AuthenticationState(
new ClaimsPrincipal(
new ClaimsIdentity()));
}
IList<Claim> claims = jwtSecurityToken.Claims.ToList();
claims.Add(new Claim(ClaimTypes.Name, jwtSecurityToken.Subject));
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"));
return new AuthenticationState(user);
}
catch (Exception)
{
return new AuthenticationState(
new ClaimsPrincipal(
new ClaimsIdentity()));
}
}
Alla stessa classe aggiungiamo due metodi per gestire il SignIn e SignOut
public async Task SignIn()
{
string savedToken = await localStorageService.GetItemAsStringAsync("bearerToken");
JwtSecurityToken jwtSecurityToken = jwtSecurityTokenHandler.ReadJwtToken(
savedToken);
IList<Claim> claims = jwtSecurityToken.Claims.ToList();
claims.Add(new Claim(ClaimTypes.Name, jwtSecurityToken.Subject));
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"));
Task<AuthenticationState> authentication =
Task.FromResult(new AuthenticationState(user));
NotifyAuthenticationStateChanged(authentication);
}
Notate la presenza di un metodo chiamato NotifyAuthenticationStateChanged proveniente dalla classe base. Quando scopriamo che lo stato di autenticazione è cambiato, questo metodo notifica tutti i consumer che è necessario renderizzare i nuovi dati.
Ecco il metodo di SignOut
public void SignOut()
{
ClaimsPrincipal nobody = new ClaimsPrincipal(new ClaimsIdentity());
Task<AuthenticationState> authentication =
Task.FromResult(new AuthenticationState(nobody));
NotifyAuthenticationStateChanged(authentication);
}
Configuriamo i servizi per l’applicazione nella classe Program
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AppAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(provider =>
provider.GetRequiredService<AppAuthenticationStateProvider>());
await builder.Build().RunAsync();
Andiamo a modificare il menu dell’applicazione (componente NavMenu) che terrà conto dello stato di autenticazione dell’utente per mostrare o meno i collegamenti alle pagine che andremo ad aggiungere successivamente
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<nav class="flex-column">
....
<AuthorizeView>
<Authorized>
<div class="nav-item px-3">
<button class="btn btn-primary">
<span class="oi oi-account-logout" aria-hidden="true"></span> Sign Out
</button>
</div>
</Authorized>
<NotAuthorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="signin">
<span class="oi oi-account-login" aria-hidden="true"></span> Sign in
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="register">
<span class="oi oi-person" aria-hidden="true"></span> Register
</NavLink>
</div>
</NotAuthorized>
</AuthorizeView>
</nav>
</div>

Inseriamo la pagina di Registrazione. Notate che il codice si aspetta che l’API sia disponibile sulla porta 6001. E’ quindi necessario modificare la porta di avvio del progetto DemoSicurezza.API
@page "/register"
@inject HttpClient httpClient
<h3>Registrazione utente</h3>
@if(registeredWithSuccess == false)
{
<EditForm Model="userToRegister" OnValidSubmit="RegisterUser">
<DataAnnotationsValidator />
<ValidationSummary></ValidationSummary>
<div class="form-group my-3">
<label for="email">Email address</label>
<InputText @bind-Value="userToRegister.Email" id="email" class="form-control" />
<ValidationMessage For="() => userToRegister.Email"/>
</div>
<div class="form-group my-3">
<label for="password">Password</label>
<InputText @bind-Value="userToRegister.Password" type="password" id="email" class="form-control" />
<ValidationMessage For="() => userToRegister.Password"/>
</div>
<button class="btn btn-primary" type="submit">OK</button>
@if(attemptFailed == true)
{
<p class="my-3 text-danger">@attemptRegisterFailedErrorMessage</p>
}
</EditForm>
} else
{
<h4>Register successful</h4>
<a href="/signin" class="btn btn-primary">Login</a>
}
@code {
private RegisterRequest userToRegister = new();
private bool registeredWithSuccess = false;
private bool attemptFailed = false;
private string? attemptRegisterFailedErrorMessage = null;
private async Task RegisterUser()
{
HttpResponseMessage httpResponseMessage =
await httpClient.PostAsJsonAsync(
"https://localhost:6001/api/accounts/register", userToRegister);
if (httpResponseMessage.IsSuccessStatusCode)
{
registeredWithSuccess = true;
}
else
{
attemptRegisterFailedErrorMessage =
await httpResponseMessage.Content.ReadAsStringAsync();
attemptFailed = true;
}
}
}

Verifichiamo che l’utente è stato inserito correttamente nella tabella AspnetUsers.

Ecco invece il codice della pagina di SignIn
@page "/signin"
@inject HttpClient httpClient
@inject ILocalStorageService storageService
@inject AuthenticationStateProvider authenticationStateProvider
@using System.Net.Http.Headers
@using DemoSicurezza.WASM.Providers
<h3>SignIn</h3>
@if(signedInWithSuccess == false)
{
<EditForm Model="userToSignIn" OnValidSubmit="SignInUser">
<DataAnnotationsValidator />
<ValidationSummary></ValidationSummary>
<div class="form-group my-3">
<label for="email">Email address</label>
<InputText @bind-Value="userToSignIn.Email" id="email" class="form-control" />
<ValidationMessage For="() => userToSignIn.Email"/>
</div>
<div class="form-group my-3">
<label for="password">Password</label>
<InputText @bind-Value="userToSignIn.Password" type="password" id="password" class="form-control" />
<ValidationMessage For="() => userToSignIn.Password"/>
</div>
<button class="btn btn-primary" type="submit">OK</button>
@if(attemptFailed == true)
{
<p class="my-3 text-danger">Sign in failed</p>
}
</EditForm>
} else
{
<h4>Sign in successful</h4>
<a href="/fetchdata" class="btn btn-primary">Fetch data</a>
}
@code {
private RegisterRequest userToSignIn = new();
private bool signedInWithSuccess = false;
private bool attemptFailed = false;
private async Task SignInUser()
{
HttpResponseMessage httpResponseMessage =
await httpClient.PostAsJsonAsync(
"https://localhost:6001/api/accounts/signin", userToSignIn);
if (httpResponseMessage.IsSuccessStatusCode)
{
signedInWithSuccess = true;
string jwtToken = await httpResponseMessage.Content.ReadAsStringAsync();
await storageService.SetItemAsync("bearerToken", jwtToken);
await ((AppAuthenticationStateProvider)authenticationStateProvider).SignIn();
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("bearer", jwtToken);
}
else
{
attemptFailed = true;
}
}
}
Verifichiamo attraverso gli strumenti di debug del browser che il token sia stato correttamente scritto nello storage.

Resta da eseguire solo il Sign Out che gestiamo nel codice nel componente NavMenu.
@using DemoSicurezza.WASM.Providers
@inject AuthenticationStateProvider authenticationStateProvider
@inject NavigationManager navigationManager
….
<AuthorizeView>
<Authorized>
<div class="nav-item px-3">
<button class="btn btn-primary" @onclick="SignOut">
<span class="oi oi-account-logout" aria-hidden="true"></span> Sign Out
</button>
</div>
</Authorized>
…..
</AuthorizeView>
</nav>
</div>
@code {
…
private void SignOut()
{
((AppAuthenticationStateProvider)authenticationStateProvider).SignOut();
navigationManager.NavigateTo("/signin");
}
}
Conclusioni
Per il momento ci fermiamo qui. Il codice è disponibile al seguente indirizzo nella branch parte1. Nel prossimo articolo metteremo in sicurezza l’api scritta consentendo l’accesso ad essa solo a chi sia autorizzato.