dRBACs.Client 4.0.0
dRBACs.Client
Libreria client per l'integrazione del sistema RBAC (Role-Based Access Control) nei progetti di ASLRoma1.
Installazione
Per utilizzare questa libreria, aggiungi il riferimento al progetto o installa il package NuGet (quando disponibile).
Compatibilità
Il pacchetto è multi-target sulle sole release LTS:
| TFM | Note |
|---|---|
net6.0 |
LTS scaduta (nov 2024), inclusa per i progetti non ancora migrati |
net8.0 |
LTS |
net10.0 |
LTS |
NuGet seleziona automaticamente l'assembly giusto e non serve alcuna configurazione nel progetto consumer. Un progetto su una release STS funziona comunque: risolve all'LTS compatibile più vicina — per esempio un progetto net9.0 usa lib/net8.0.
Il codice sorgente è identico per tutti i target — nessuna #if di compilazione condizionale, quindi il comportamento non cambia da un framework all'altro.
Aggiornamento a v3.3.0 — anagrafica dipendenti (opzionale)
Novità additiva e disattivata per default: chi non la configura non deve cambiare nulla.
L'IAM identifica l'utente con il codice fiscale, mentre le applicazioni preesistenti lo
identificano con lo username aziendale — quello con cui hanno scritto anni di dati. Senza un
ponte fra i due, ogni scrittura fatta dopo il passaggio all'IAM finisce sotto una chiave utente
diversa da tutte quelle già a database. Il lookup risolve il problema chiedendo a un servizio di
anagrafica, dato il codice fiscale, la posizione del dipendente, e ne emette lo USERNAME come
claim preferred_username.
{
"Iam": {
"EmployeeDirectory": {
"Enabled": true,
"BaseUrl": "https://url-del-servizio-anagrafica",
"LookupPath": "api/dipendenti/{cf}",
"CacheTtlMinutes": 60
}
}
}
Token di servizio
L'anagrafica non accetta il token dell'utente: autentica l'applicazione, con un token proprio che
si chiede in POST autenticandosi in Basic. Il token e' opaco e ha una scadenza assoluta, che il
pacchetto legge dalla risposta e usa per rinnovarlo — con un margine, perche' un token scaduto per
una frazione di secondo si manifesta come un 401 intermittente.
{
"Iam": {
"EmployeeDirectory": {
"AuthTokenUrl": "https://admin-mia.aslroma1.it/auth/token",
"AuthBasicCredentials": "<gia' codificate in base64>",
"TokenExpiryMarginSeconds": 60
}
}
}
In alternativa a AuthBasicCredentials si possono passare AuthClientId e AuthClientSecret in
chiaro. Lasciando AuthTokenUrl vuoto si inoltra all'anagrafica il token dell'utente, che resta il
comportamento adatto ai servizi che lo accettano.
Il token e' dell'applicazione e non dell'utente: sta in un singleton, condiviso da tutte le richieste, e il rinnovo e' serializzato — alla scadenza tutte le richieste in volo ripartono insieme, ed e' il momento in cui conviene meno moltiplicare le chiamate.
Il claim viene aggiunto prima degli augmenter e prima dei ruoli, quindi un augmenter lo
trova già fra i claim. Il pacchetto emette solo lo username: il resto dell'anagrafica —
nome, cognome, recapiti, struttura — è a disposizione dell'app in
IamClaimsAugmentationContext.Employee, perché quali altri dati diventino claim è una decisione
applicativa e non del pacchetto.
⚠️ Il campo GRUPPO dell'anagrafica è mappato ma deliberatamente inutilizzato, e non va
trasformato in un ruolo: i ruoli li decide il servizio RBAC, e una seconda sorgente
riaprirebbe esattamente il problema che IgnoreIdentityProviderRoles esiste per chiudere.
Un'anagrafica irraggiungibile non fa fallire il login: si prosegue senza username, coerentemente con quanto già fa la risoluzione dei ruoli. Esiti positivi e finestre di backoff sono in cache come per i ruoli.
Host a token (API, microservizi)
Un'app a cookie risolve lo username una volta sola al login e se lo porta nel cookie. Un'API
quel momento non ce l'ha: deve ricavarlo dal token a ogni richiesta, e per questo si registra
diversamente — senza OIDC, con una IClaimsTransformation:
builder.Services.AddIamEmployeeDirectory(builder.Configuration);
builder.Services.AddRbacClient(builder.Configuration); // se servono anche i ruoli
Richiede SaveToken = true sull'handler JWT: senza, non c'è access token da inoltrare né
all'anagrafica né al servizio RBAC.
Aggiornamento a v3.2.0 — i ruoli dell'IdP non sono più attendibili per default
Quando i ruoli li decide il servizio RBAC (UseRbacRoles = true, il default), i claim di ruolo
presenti nel token vengono scartati prima che l'identity finisca nel cookie.
Il motivo è di sicurezza, non di igiene dei claim. Un IdP può emettere ruoli non pertinenti
all'applicazione — o, come osservato sull'IAM ASLROMA1, lo stesso ruolo admin a ogni
utente che effettua l'accesso. E c'è un secondo motivo, meno evidente: un ruolo lasciato nel
cookie sopravvive a un'indisponibilità del servizio RBAC, perché
RemoteRolesClaimsTransformation restituisce il principal intatto quando il servizio non
risponde. Senza lo scarto, un'interruzione del servizio ruoli diventerebbe una promozione.
Lo scarto copre identity.RoleClaimType e i tipi noti (role, roles, URI WS-Federation), ed
è verificato a posteriori: se un claim di ruolo sopravvive, il login fallisce invece di
proseguire con un cookie potenzialmente privilegiato.
Gli IIamClaimsAugmenter girano dopo lo scarto: un ruolo aggiunto da un innesto applicativo
è una decisione dell'app, non un'affermazione dell'IdP, e resta in piedi.
// Per tornare al comportamento precedente — cioè fidarsi dei ruoli dell'IdP
// quanto di quelli RBAC:
builder.Services.AddIamAuthentication(builder.Configuration, o =>
{
o.IgnoreIdentityProviderRoles = false;
});
L'opzione non ha effetto con UseRbacRoles = false: senza una sorgente alternativa, svuotare i
ruoli lascerebbe ogni utente senza permessi.
Aggiornamento a v3.1.3
Un ruolo già presente nel principal sotto un solo claim type non veniva normalizzato sugli
altri. La verifica di presenza era globale — "questo ruolo c'è già da qualche parte?" — e un
riscontro positivo saltava l'emissione di tutti i tipi, incluso identity.RoleClaimType, che
è quello su cui IsInRole e [Authorize(Roles = ...)] decidono.
Si manifesta con un host che ha MapInboundClaims = true: il claim role del token viene
rimappato sull'URI WS-Federation, mentre RoleClaimType resta "role". Il ruolo si vede
benissimo fra i claim e IsInRole risponde comunque false.
Ora la verifica è per singolo claim type: si aggiungono i tipi mancanti senza duplicare quelli presenti.
Nota di configurazione, indipendente da questa correzione: RoleClaimType e NameClaimType
vanno tenuti coerenti con MapInboundClaims. Con la mappatura inbound attiva i claim del token
arrivano in forma WS-Federation, quindi i valori giusti sono ClaimTypes.Role e
ClaimTypes.Name, non "role" ed "email".
Aggiornamento a v3.1.2
Configure<RbacOptions> chiamata dopo AddRbacClient (o dopo AddIamAuthentication, che
la chiama internamente) non aveva effetto sull'indirizzo usato dal client: il base url veniva
letto subito da Rbac:BaseUrl e catturato nella closure di AddHttpClient. Un'app che tiene
la configurazione in variabili d'ambiente piatte finiva così per chiamare il default
https://localhost:7171.
Sintomo: a log l'indirizzo giusto, e a runtime
Risoluzione ruoli RBAC non riuscita ...: Connection refused (localhost:7171).
Ora il base url si risolve da IOptions<RbacOptions> alla creazione del client, quindi
entrambe le forme funzionano:
// sezione di configurazione
builder.Services.AddRbacClient(builder.Configuration); // legge Rbac:BaseUrl
// oppure valore da variabile d'ambiente piatta
builder.Services.Configure<RbacOptions>(o => o.BaseUrl = Environment.GetEnvironmentVariable("RBAC_BASE_URL")!);
Un BaseUrl esplicitamente vuoto ora fallisce con un messaggio chiaro invece di ripiegare in
silenzio su localhost.
Aggiornamento a v3.1.1
Correzione obbligatoria per chi usa AddIamAuthentication o comunque la risoluzione ruoli:
in v3.1.0 RemoteRolesClaimsTransformation entrava in ricorsione infinita alla prima
richiesta autenticata.
Il token per il servizio RBAC si legge con HttpContext.GetTokenAsync("access_token"), che
internamente chiama AuthenticateAsync, che rinvoca la claims transformation: senza una
guardia il giro non finisce mai. Il risultato è una StackOverflowException — non
intercettabile — quindi il processo terminava subito dopo il login senza lasciare nulla nei
log, e il container ripartiva.
Sintomo tipico: il login sull'IAM va a buon fine, il browser viene rediretto all'applicazione
e l'applicazione si riavvia. Nessun aggiornamento di configurazione è necessario: basta
passare a 3.1.1.
Migrazione a v3.0.0
IRbacClient restituisce RbacResult<T> invece di liste e bool. Il motivo: prima un
servizio irraggiungibile e un utente senza ruoli restituivano entrambi una lista vuota, e la
claims transformation metteva quell'esito in cache — un blip di rete lasciava l'utente senza
permessi per l'intera durata della cache, e una revoca era indistinguibile da un guasto.
// prima (v2.x)
List<RbacRoleDto> roles = await client.GetUserRolesAsync(cf);
// una lista vuota poteva voler dire "nessun ruolo" oppure "servizio giù": impossibile saperlo
// ora (v3.0.0)
RbacResult<IReadOnlyList<RbacRoleDto>> result = await client.GetUserRolesAsync(cf);
if (!result.IsSuccess)
{
// result.Error, result.StatusCode
return;
}
IReadOnlyList<RbacRoleDto> roles = result.Value!;
Altri cambiamenti, senza impatto sul codice consumer:
- il token di accesso viaggia sulla singola richiesta e non più su
HttpClient.DefaultRequestHeaders; - la claims transformation non muta più il principal ricevuto e non emette il claim
RBAC_TRANSFORMED, che se persistito in un cookie avrebbe congelato i ruoli fino al logout; - nuova opzione
Rbac:FailureRetrySeconds(default 15): attesa prima di riprovare dopo un guasto. Impostarla a0disattiva il backoff. AncheRbac:CacheTtlMinutesa0disattiva la cache dei ruoli.
Migrazione da AslRoma1.Rbac.Client (v1.x → v2.0.0)
Il pacchetto è stato rinominato. Per aggiornare un progetto consumer:
| Prima (v1.x) | Ora (v2.0.0) |
|---|---|
PackageReference Include="AslRoma1.Rbac.Client" |
PackageReference Include="dRBACs.Client" |
using AslRoma1.Rbac.Client.Extensions; |
using dRBACs.Client.Extensions; |
using AslRoma1.Rbac.Client.Security; |
using dRBACs.Client.Security; |
using AslRoma1.Rbac.Client.Services; |
using dRBACs.Client.Services; |
AddAslRoma1RbacClient(configuration) |
AddRbacClient(configuration) |
Nessuna chiave di configurazione è cambiata: la sezione Rbac resta identica.
Configurazione
Aggiungi la configurazione RBAC nel file appsettings.json:
{
"Rbac": {
"BaseUrl": "https://url-del-servizio-rbac",
"CacheTtlMinutes": 10,
"FailureRetrySeconds": 15
}
}
Autenticazione IAM (v3.1.0)
Per autenticarsi contro l'IAM ASLROMA1 (LDAP, SPID, CIE) e risolvere i ruoli in un colpo solo:
using dRBACs.Client.Extensions;
builder.Services.AddIamAuthentication(builder.Configuration);
Restano a carico dell'app app.UseAuthentication() e app.UseAuthorization(): l'ordine dei
middleware è una decisione della pipeline, non del pacchetto.
{
"Iam": {
"Authority": "https://iam.aslroma1.it",
"ClientId": "...",
"ClientSecret": "...",
"Scopes": "openid profile",
"ValidationMode": "Strict"
}
}
Al termine del login l'utente ha: anagrafica da SPID/CIE, codice fiscale su
ClaimTypes.SerialNumber, email e PEC estratte dall'assertion SAML, e i ruoli dal servizio
RBAC — quindi IsInRole(...) e [Authorize(Roles = "...")] funzionano senza altro codice.
Modalità di validazione
| Modalità | Discovery | Firma verificata | Issuer / nonce |
|---|---|---|---|
Strict (default) |
sì | sì, via JWKS | validati |
Discovery |
sì | sì, via JWKS | rilassati |
Compatible |
no, endpoint cablati | no | rilassati |
Strict è il default perché è stato verificato con login reali contro l'IAM di produzione,
un gradino per volta: il jwks_uri esiste e pubblica una chiave RSA 2048 con kid, le firme
RS256 verificano, l'iss coincide con l'authority, l'aud è il client_id e il nonce torna
riflesso nell'id_token.
Discovery verifica la firma ma lascia rilassati issuer e nonce: serve a isolare un guasto,
non come destinazione. Compatible non verifica nulla — un id_token contraffatto verrebbe
accettato — ed esiste solo come via di fuga se l'IAM smettesse di pubblicare il JWKS. In quel
caso la cosa giusta è segnalarlo all'IAM, non restare lì.
Nota su RequireState: resta disattivato in tutte e tre le modalità, e non è una deroga. Il
default del framework è true, ma OpenIdConnectHandler non assegna mai lo State del
contesto di validazione: lasciato a true il login fallisce con IDX21329 contro qualunque
IdP. La protezione CSRF sul callback la fa il cookie di correlazione, che il handler valida
per conto suo.
Durata della sessione
Il cookie eredita la scadenza del token e non si rinnova (UseTokenLifetime = true,
SlidingExpiration = false): lasciando i default del framework durerebbe 14 giorni
rinnovandosi a ogni richiesta, e sopravviverebbe per settimane a un token scaduto o revocato.
L'id_token dell'IAM vive un'ora, e quella diventa la durata della sessione.
Il discovery dichiara solo authorization_code, ma il token endpoint restituisce anche un
refresh token. Il pacchetto non lo usa: nulla qui rinnova la sessione.
Iam:CookieLifetimeFollowsToken = false disattiva il vincolo, ma è una responsabilità che
ci si assume consapevolmente.
Claim applicativi
Per i claim che il pacchetto non può conoscere — il codice struttura di un'app, un profilo
amministrativo — si implementa IIamClaimsAugmenter:
builder.Services.AddIamAuthentication(builder.Configuration)
.WithClaimsAugmenter<CirCodeAugmenter>();
Adattare i default a un'app esistente
I default seguono l'integrazione collaudata (MapInboundClaims = false, nome da email,
ruoli da role). Un'app che si aspetta la forma WS-Federation li ribalta:
builder.Services.AddIamAuthentication(builder.Configuration, o =>
{
o.MapInboundClaims = true;
o.NameClaimType = "preferred_username";
});
Altre opzioni: ApiPathPrefixes (default ["/api"], sotto cui si risponde 401/403 invece di
redirigere al login), UseRbacRoles (per chi si autorizza diversamente),
EmployeeDirectory (vedi sopra), AccessDeniedPath,
e LogUserInfoPayload — che registra il payload grezzo dello userinfo per riallineare i DTO
quando lo schema IAM cambia. ⚠️ Quel payload contiene dati personali (codice fiscale,
indirizzo, telefono, estremi del documento): va acceso solo il tempo necessario.
Registrazione dei servizi
Nel file Program.cs, utilizza il metodo di estensione per registrare i servizi necessari:
using dRBACs.Client.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Registra i servizi RBAC
builder.Services.AddRbacClient(builder.Configuration);
var app = builder.Build();
// ...
Utilizzo
IUserContext
Puoi iniettare IUserContext per ottenere informazioni sull'utente corrente:
public class MyService
{
private readonly IUserContext _userContext;
public MyService(IUserContext userContext)
{
_userContext = userContext;
}
public void DoSomething()
{
var cf = _userContext.CodiceFiscale;
var isAdmin = _userContext.IsAdmin;
// ...
}
}
IRbacClient
Puoi iniettare IRbacClient per interagire direttamente con il servizio RBAC:
public class MyAdminService
{
private readonly IRbacClient _rbacClient;
public MyAdminService(IRbacClient rbacClient)
{
_rbacClient = rbacClient;
}
public async Task<bool> AssignRole(string cf, string roleId)
{
var result = await _rbacClient.AddRoleToUserAsync(cf, roleId);
return result.IsSuccess;
}
}
No packages depend on dRBACs.Client.