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 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.

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.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