dRBACs.Client 4.1.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 a 0 disattiva il backoff. Anche Rbac:CacheTtlMinutes a 0 disattiva 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.

Cache dei ruoli e invalidazione

I ruoli risolti restano in cache in memoria per Rbac:CacheTtlMinutes (default 10). Ogni scrittura fatta con IRbacClient — AddRoleToUserAsync, RemoveRoleFromUserAsync — scarta la cache di quel codice fiscale non appena il servizio conferma: l'utente vede i ruoli nuovi alla richiesta successiva, senza aspettare il TTL e senza rifare login. Si invalida solo dopo un esito positivo, perché se la scrittura non e' passata i ruoli in cache sono ancora quelli giusti.

Un'app che modifica i ruoli per una strada propria — una API amministrativa che parla direttamente al servizio RBAC invece di passare da IRbacClient — puo' iniettare IRbacRoleCache e invalidare da se':

public class RuoliController(IRbacRoleCache cache)
{
    public async Task Assegna(string cf, string roleId)
    {
        await _mioServizio.AssegnaAsync(cf, roleId);
        cache.Invalidate(cf);   // maiuscole e spazi non contano
    }
}

Limite da conoscere: la cache vive dentro un solo processo. Una scrittura fatta dall'app A non invalida la cache dell'app B, che continuera' a servire i ruoli vecchi fino alla scadenza del TTL. Per l'utente modificato dall'interno della propria applicazione — il caso normale — l'aggiornamento e' invece immediato. Se serve che la modifica si propaghi subito ovunque, l'unica leva oggi e' abbassare Rbac:CacheTtlMinutes.

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.

v.4.1.0 - La cache dei ruoli viene invalidata dopo ogni scrittura andata a buon fine con AddRoleToUserAsync o RemoveRoleFromUserAsync: l'utente vede i ruoli nuovi alla richiesta successiva, senza attendere la scadenza del TTL ne' rifare login. Nuovo servizio pubblico IRbacRoleCache per le app che modificano i ruoli senza passare da IRbacClient. La cache resta per processo: una scrittura fatta da un'app non invalida quella di un'altra. v.4.0.0 - Sicurezza, cambio di default: Iam:ValidationMode passa da Compatible a Strict. La firma dell'id_token viene ora verificata contro il JWKS dell'IAM, e issuer e nonce sono validati. Il default precedente accettava un id_token contraffatto e nasceva dall'assunto che l'IAM non pubblicasse un jwks_uri: lo pubblica, e ogni gradino e' stato verificato con login reali contro l'IAM di produzione. Per tornare al comportamento precedente impostare Iam:ValidationMode a Compatible. Inoltre: nonce piu' corto (43 caratteri base64url, 256 bit) perche' l'IAM non digerisce quello del framework; RequireState forzato a false ovunque, perche' OpenIdConnectHandler non popola mai lo State del contesto di validazione e con true il login fallisce con IDX21329 contro qualunque IdP; RS256 come unico algoritmo ammesso; cookie di nonce e correlazione con SameSite=None piu' Secure espliciti. Il codice fiscale viene ora letto anche dall'id_token (claim codice_fiscale): lo userinfo dell'IAM risponde una sola volta per token, quindi un suo guasto non e' ritentabile e non fa piu' fallire il login — si prosegue senza i soli claim di profilo. v.3.3.0 - Nuovo lookup opzionale sull'anagrafica dipendenti (Iam:EmployeeDirectory, disattivato per default): dato il codice fiscale risolve lo username aziendale e lo emette come claim preferred_username, cosi' un'app che passa all'IAM continua a identificare l'utente con la stessa chiave con cui ha scritto i propri dati. Disponibile sia sul login a cookie sia, con AddIamEmployeeDirectory, sugli host a token. Le chiamate all'anagrafica sono autenticate con un token di servizio ottenuto in Basic da AuthTokenUrl, tenuto in cache e rinnovato prima della scadenza dichiarata dal servizio. Il DTO completo dell'anagrafica e' esposto agli augmenter tramite IamClaimsAugmentationContext.Employee. v.3.2.0 - Sicurezza: i claim di ruolo emessi dall'IdP vengono scartati prima di emettere il cookie quando i ruoli li decide il servizio RBAC (nuova opzione IgnoreIdentityProviderRoles, attiva per default). Un IdP che emette lo stesso ruolo amministrativo a tutti gli utenti non puo' piu' concedere privilegi, e un'indisponibilita' dell'RBAC non lascia piu' attivi ruoli residui nel cookie. v.3.1.3 - Correzione: un ruolo gia' presente sotto un solo claim type impediva l'emissione dello stesso ruolo sotto RoleClaimType. v.3.1.2 - Correzione: il base url impostato con Configure<RbacOptions> dopo AddRbacClient veniva ignorato. v.3.1.1 - Correzione: ricorsione infinita in RemoteRolesClaimsTransformation.

Version Downloads Last updated
4.3.2 56 09/17/2026
4.3.1 1 09/17/2026
4.3.0 1 09/16/2026
4.2.0 23 09/01/2026
4.1.0 15 09/01/2026
4.0.0 5 08/28/2026
3.3.0 15 08/28/2026
3.2.0 3 08/27/2026
3.1.2 3 08/27/2026
3.1.1 3 08/26/2026
3.1.0 1 08/26/2026