Quando si gestiscono più progetti API distribuiti, replicare la logica di autenticazione su ciascun servizio è inefficiente e difficile da mantenere. La soluzione ideale è la creazione di un Authentication Server centralizzato (un unico punto di rilascio dei token) che protegga tutti i microservizi o le API figlie.
Ecco una semplice soluzione su ome impostare l’architettura di base sfruttando le funzionalità di C# 10 e ASP.NET Core.
1. Architettura della Soluzione
Per realizzare un sistema scalabile, separiamo le responsabilità in due macro-componenti:
- Auth Server: Un’applicazione ASP.NET Core dedicata esclusivamente alla verifica delle credenziali e alla generazione dei token JWT (JSON Web Token).
- Resource Servers (Le API): I singoli progetti API che si limitano a validare la firma crittografica del token ricevuto, senza interrogare direttamente il database degli utenti.
2. Implementazione del Token Issuer (Auth Server)
In C# 10 possiamo sfruttare i file-scoped namespaces e i tipi record per mantenere il codice pulito e conciso nei DTO di login:
namespace AuthServer.Models; public record LoginRequest(string Username, string Password); public record TokenResponse(string Token, DateTime Expiration);
All’interno del controller o di una Minimal API, gestiamo la validazione e la creazione del token firmato:
// Esempio con Minimal API in ASP.NET Core
app.MapPost("/api/auth/login", (LoginRequest request, IConfiguration config) =>
{
// 1. Validazione credenziali (mock o db)
if (request.Username != "admin" || request.Password != "password-sicura")
return Results.Unauthorized();
// 2. Definizione dei Claims
var claims = new List<Claim>
{
new(ClaimTypes.Name, request.Username),
new(ClaimTypes.Role, "Administrator")
};
// 3. Generazione della chiave e del token
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]!));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddHours(2),
Issuer = config["Jwt:Issuer"],
Audience = config["Jwt:Audience"],
SigningCredentials = credentials
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return Results.Ok(new TokenResponse(tokenHandler.WriteToken(token), tokenDescriptor.Expires.Value));
});
3. Configurazione dei Resource Servers (Le API)
Ogni progetto API differente che deve sottostare a questo sistema di accesso non ha bisogno di conoscere la logica di login. Nel file Program.cs di ciascuna API basta configurare il middleware di autenticazione JWT:
- Registrare il pacchetto
Microsoft.AspNetCore.Authentication.JwtBearer. - Impostare i parametri di validazione (
Issuer,Audience,IssuerSigningKey) identici a quelli dell’Auth Server. - Applicare l’attributo
[Authorize]sui controller o.RequireAuthorization()sulle route delle Minimal API.
Nota di sicurezza: Assicurati di archiviare la chiave segreta (
Jwt:Key) in modo sicuro usando le User Secrets in ambiente di sviluppo e le variabili d’ambiente o Azure Key Vault in produzione.
A questo punto utilizzeremo una libreria dedicata che ci consenta di gestire i flussi. La libreria si chiama OpenIddict.
Configurazione dell’Authorization Server
Registrazione dei pacchetti NuGet
Nel progetto dell’Auth Server, installa i pacchetti necessari per Entity Framework Core e OpenIddict:
dotnet add package OpenIddict.AspNetCore dotnet add package OpenIddict.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Configurazione del DbContext
Configura il contesto del database ereditando da DbContext e registrando le entità di OpenIddict:
using Microsoft.EntityFrameworkCore;
namespace AuthServer.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.UseOpenIddict();
}
}
Configurazione in Program.cs
Configura i servizi di OpenIddict all’interno del file di bootstrap dell’applicazione:
using AuthServer.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Configurazione Database
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
options.UseOpenIddict();
});
builder.Services.AddControllersWithViews();
// Configurazione OpenIddict
builder.Services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
})
.AddServer(options =>
{
options.SetAuthorizationEndpointUis("/connect/authorize")
.SetLogoutEndpointUis("/connect/logout")
.SetTokenEndpointUis("/connect/token")
.SetUserinfoEndpointUis("/connect/userinfo");
options.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow()
.AllowClientCredentialsFlow();
options.RegisterScopes("api", OpenIddict.Abstractions.OpenIddictConstants.Scopes.Email,
OpenIddict.Abstractions.OpenIddictConstants.Scopes.Profile);
// In produzione usare certificati X.509 validi
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableStatusCodePagesIntegration();
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Inizializzazione Automatica dei Client (Seed Data)
Per registrare dinamicamente le applicazioni client all’avvio del server:
using AuthServer.Data;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Abstractions;
namespace AuthServer.Worker;
public class Worker : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public Worker(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await context.Database.EnsureCreatedAsync(cancellationToken);
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
if (await manager.FindByClientIdAsync("client-mvc", cancellationToken) == null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "client-mvc",
ClientSecret = "secret-chiave-super-sicura",
ConsentType = OpenIddict.Abstractions.OpenIddictConstants.ConsentTypes.Explicit,
DisplayName = "Applicazione Client MVC",
RedirectUris = { new Uri("https://localhost:7001/signin-oidc") },
PostLogoutRedirectUris = { new Uri("https://localhost:7001/signout-callback-oidc") },
Permissions =
{
OpenIddict.Abstractions.OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddict.Abstractions.OpenIddictConstants.Permissions.Endpoints.Logout,
OpenIddict.Abstractions.OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddict.Abstractions.OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddict.Abstractions.OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
OpenIddict.Abstractions.OpenIddictConstants.Permissions.Scopes.Email,
OpenIddict.Abstractions.OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddict.Abstractions.OpenIddictConstants.Permissions.Prefixes.Scope + "api"
}
}, cancellationToken);
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
Gestione dei Flussi nel Controller di Autorizzazione
Crea un controller dedicato per gestire le richieste agli endpoint di autorizzazione e rilascio token:
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using System.Security.Claims;
namespace AuthServer.Controllers;
public class AuthorizationController : Controller
{
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("La richiesta OpenID Connect non può essere recuperata.");
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (!result.Succeeded)
{
return Challenge(
authenticationSchemes: CookieAuthenticationDefaults.AuthenticationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.QueryString
});
}
var claims = new List<Claim>
{
new(OpenIddictConstants.Claims.Subject, result.Principal.Identity!.Name!),
new(ClaimTypes.Name, result.Principal.Identity!.Name!),
new(ClaimTypes.Role, "User")
};
var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
claimsPrincipal.SetScopes(request.GetScopes());
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
[HttpPost("~/connect/token")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("La richiesta OpenID Connect non può essere recuperata.");
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
return SignIn(principal!, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("Il tipo di grant specificato non è supportato.");
}
}
Protezione dei Resource Servers (Le API)
Nei singoli progetti API dipendenti, configura il middleware di validazione di OpenIddict per leggere i token JWT:
// In Program.cs del Resource Server
builder.Services.AddAuthentication(OpenIddict.Validation.AspNetCoreOpenIddictValidationDefaults.AuthenticationScheme)
.AddOpenIddictValidation(options =>
{
options.SetIssuer("https://localhost:5001/");
options.AddAudiences("rs-api");
options.UseIntrospection();
options.UseSystemNetHttp();
options.UseAspNetCore();
});
builder.Services.AddAuthorization();In conclusione, adottare un Authorization Server centralizzato basato su OpenIddict in .NET ti permette di disaccoppiare la logica di identità e sicurezza dai singoli microservizi, garantendo un’architettura scalabile, standardizzata e pronta per qualsiasi tipo di client (Web, SPA o Mobile).
Gestire i flussi OAuth2 e OIDC in questo modo richiede un investimento iniziale di configurazione, ma ripaga ampiamente in termini di manutenibilità, flessibilità e sicurezza complessiva dell’ecosistema API.