AUTORIZZAZIONE
AUTORIZZARE IN BASE AL RUOLO CON L’ATTRIBUTO AUTHORIZE
Con Authorize possiamo chiedere oltre all’autenticazione di possedere un determinato ruolo. In questo post parleremo di autorizzazione basata sui ruoli, abbiamo visto nei post precedenti che abbiamo due macro gruppi, utenti autenticati e utenti anonimi. A sua volta tra gli utenti autenticati abbiamo fatto una suddivisione, ossia coloro che possono creare, modificare, eliminare i corsi e questo è il ruolo Teacher e gli amministratori che si occupano di assegnare i ruoli agli utenti. Vedremo inoltre le Policy, infatti ancora la nostra applicazione ha un difetto, ossia entrando con il ruolo Teacher un docente può andare a modificare il corso di un altro docente. Le policy servono a impedire ciò.
Solo l’utente amministratore che è una persona fidata scelta dalla nostra committente può autorizzare l’accesso. Se l’utente vuole accedere alla Razor Page riportata sopra in figura dovrà avere il ruolo di Administrator.
Vediamo in MVC come funziona l’autorizzazione, in particolare la creazione, modifica ed eliminazione di un corso spetta al ruolo Teacher.
L’attributo Authorize va messo sul CoursesController e su LessonsController. Vediamo come autorizzare l’accesso a due o più ruoli.
Per accedere all’action Report della figura sopra riportata bisogna avere entrambi i ruoli, questo perché gli attributi Authorize sono componibili.
USARE UNA POLICY PER LOGICHE DI AUTORIZZAZIONE PERSONALIZZATE
Ci sono situazioni in cui autorizzare per ruolo ancora non è sufficiente; infatti, adesso abbiamo il requisito di impedire ad un docente di modificare il corso di un altro docente. In questo caso dobbiamo verificare che l’autore che sta modificando o eliminando un corso ne sia l’autore. Per fare tutto questo dobbiamo usare le Policy. Una policy viene interposta tra l’utente e la risorsa, e se l’utente vuole modificare quella risorsa dovrà soddisfare tutti i requirement (mattoncini in figura) della policy.
Ogni mattoncino rappresenta un requisito che l’utente deve soddisfare per ottenere la risorsa.
Con l’ultima riga della figura sopra e il metodo Add stiamo definendo della logica personalizzata, molto importante.
Vediamo come creare il Requirement. Il codice lo trovi sotto la cartella /Model/Authorization.
Chi determina se il Requirement è soddisfatto è L’AuthorizationHandler, è lui che va a curiosare nell’identità dell’utente e che eventualmente si avvale dei servizi della Dependency Injection per stabilire se il Requirement è soddisfatto oppure no.
APPLICARE UNA POLICY
Vediamo con uno schema riassuntivo come vengono applicate le Policy.
AUTORIZZARE IN MANIERA IMPERATIVA CON IAUTHORIZATION SERVICE
La nostra committente ha stabilito che non ci sono limiti al numero di corsi che un docente può pubblicare; tuttavia se il numero è maggiore o uguale a cinque vuole essere informata con una e-mail. Questo perché pubblicando molti corsi può capitare che alcuni siano un pò tralasciati e poco accurati. Per raggiungere lo scopo ho creato una Policy sempre in /Models/Authorization che viene richiamata dalla Action Create del Controller CoursesController. Ti mostro il codice.
RIEPILOGO DELLA SEZIONE
AUTORIZZARE GLI UTENTI
In molte applicazioni web, l’accesso alle funzionalità deve essere permesso solo ad utenti che possiedono determinati requisiti. Ecco alcuni esempi che riguardano l’applicazione MyCourse:
- per inviare una domanda a un docente, l’utente deve almeno essere autenticato (cioè aver fatto il login);
- per creare un corso, l’utente deve possedere il ruolo di docente;
- per assegnare ruoli, l’utente deve possedere il ruolo di amministratore.
In casi semplici come questi, “autorizzare” vuol dire verificare che l’utente sia autenticato o che nella sua identità sia presente un certo ruolo. Se questa verifica ha successo, allora potrà accedere alla funzionalità e in questo caso si dice che l’utente è “autorizzato”.
In altri casi, quando è necessario verificare la presenza o il contenuto di altri claim oppure interrogare il database per ottenere dei valori, allora possiamo scrivere una policy per eseguire logica di autorizzazione personalizzata.
Gli attributi Authorize e AllowAnonymous con MVC
Per consentire l’accesso ad un’action MVC solo agli utenti autenticati, la decoriamo con l’attributo Authorize
.
- public CoursesController : Controller
- {
- [Authorize]
- public IActionResult Index() // Gli utenti anonimi non riusciranno ad accedere a quest’action
- {
- // …
- }
- }
Oppure lo usiamo per decorare un controller. In questo modo, tutte le sue action saranno protette da accessi anonimi.
- [Authorize]
- public CoursesController : Controller
- {
- public IActionResult Index() // Gli utenti anonimi non riusciranno ad accedere a quest’action
- {
- // …
- }
- }
Quando l’attributo Authorize
viene posto sul controller, possiamo decorare un’action con l’attributo AllowAnonymous
per renderla pubblicamente accessibile.
- [Authorize]
- public CoursesController : Controller
- {
- [AllowAnonymous]
- public IActionResult Index() // Ora gli utenti anonimi possono accedere
- {
- // …
- }
- }
Attenzione: se poniamo AllowAnonymous
su un controller, tutte le sue action saranno pubblicamente accessibili. Infatti, ogni eventuale attributo Authorize
posto sulle action o sul controller stesso verrebbe ignorato.
- [AllowAnonymous]
- public CoursesController : Controller
- {
- [Authorize] // Questo attributo Authorize viene ignorato, perciò e fuorviante
- public IActionResult Index() // Infatti qui gli utenti anonimi possono accedere
- {
- // …
- }
- }
Autorizzare con Razor Pages
L’attributo Authorize
funziona anche con le Razor Pages e deve essere usato per decorare il page model.
- [Authorize]
- public class UsersPageModel : PageModel
- {
- //
- }
Attenzione: non è possibile porre l’attributo Authorize
in corrispondenza dei page handler (ovvero i metodi della Razor Page come OnPostAsync
). Questa è una piccola limitazione di Razor Pages rispetto a MVC, che invece ammette l’attributo sulle action. In più, però, le Razor Pages permettono di configurare l’autorizzazione alle pagine in maniera centralizzata, dal metodo AddRazorPages
che poniamo in ConfigureServices
nella classe Startup
. Infatti, usando metodi come AuthorizeFolder
, in un sol colpo possiamo proteggere tutte le Razor Page presenti in una determinata directory. Ecco vari esempi:
- services.AddRazorPages(options => {
- // Tutte le Razor Page in /Pages/Admin sono protette
- options.Conventions.AuthorizeFolder(“/Admin”);
- // Tutte le Razor Page in /Areas/Identity/Pages/Account/Manage sono protette
- options.Conventions.AuthorizeAreaFolder(“Identity”, “/Account/Manage”);
- // Solo la Razor Page /Pages/Contact.cshtml è protetta
- options.Conventions.AuthorizePage(“/Contact”);
- // Analogamente, possiamo consentire l’accesso anonimo
- options.Conventions.AllowAnonymousToFolder(“/Public”);
- options.Conventions.AllowAnonymousToAreaPage(“Identity”, “/Account/Login”);
- options.Conventions.AllowAnonymousToPage(“/Privacy”);
- });
Opzionalmente, possiamo fornire anche il nome di una policy come ulteriore argomento dei metodi Authorize*
.
Autorizzare per ruolo
L’attributo Authorize
ammette il nome di uno o più ruoli attraverso la sua proprietà Roles
. In questo caso l’utente non dovrà solo essere autenticato ma anche possedere il ruolo indicato.
- public CoursesController : Controller
- {
- [Authorize(Roles = “Teacher”]
- public IActionResult Report() // Solo gli utenti con ruolo “Teacher” potranno accedere a questa action
- {
- // …
- }
- }
Possiamo indicare più ruoli separandoli con la virgola. In questo caso l’utente dovrà possederne almeno uno (i ruoli sono in “OR”).
- public CoursesController : Controller
- {
- [Authorize(Roles = “Teacher,Administrator”]
- public IActionResult Report() // Solo gli utenti con ruolo “Teacher” o “Administrator” potranno accedere
- {
- // …
- }
- }
Se invece vogliamo richiedere all’utente di possedere contemporaneamente due o più ruoli, allora usiamo più istanze dell’attributo Authorize
(i ruoli sono in “AND”):
- public CoursesController : Controller
- {
- [Authorize(Roles = “Teacher”]
- [Authorize(Roles = “Administrator”]
- public IActionResult Report() // Solo gli utenti con ruolo “Teacher” e “Administrator” potranno accedere
- {
- // …
- }
- }
La stessa cosa possiamo farla anche ponendo l’attributo Authorize
sia sul controller che sull’action. Questo dimostra come gli attributi Authorize
siano componibili per ottenere autorizzazioni complesse.
- [Authorize(Roles = “Teacher”]
- public CoursesController : Controller // Solo gli utenti con ruolo “Teacher” potranno accedere alle action
- {
- [Authorize(Roles = “Administrator”]
- public IActionResult Report() // Ma per accedere a questa action servirà anche il ruolo “Administrator”
- {
- // …
- }
- }
Autorizzare per policy
Quando autorizzare per ruolo non basta, possiamo creare una policy. Ogni policy è un contenitore di uno o più requirement, ovvero dei requisiti che l’utente dovrà soddisfare (tutti) se vuole risultare autorizzato.
Per prima cosa, nel metodo ConfigureServices
della classe Startup
, dobbiamo registrare la policy fornendo il nome e poi aggiungere dei requirement al suo interno usando l’istanza di AuthorizationPolicyBuilder
che ci viene fornita.
- services.AddAuthorization(options =>
- {
- options.AddPolicy(“NomePolicy”, builder => // builder è l’istanza di AuthorizationPolicyBuilder
- {
- // Espone vari metodi Require* per aggiungere vari tipi di requirement alla policy
- builder.RequireAuthenticatedUser();
- builder.RequireRole(“Teacher”);
- builder.RequireClaim(“Country”, “Italy”);
- });
- });
Vediamo anche l’esempio di una policy che richiede un unico requirement personalizzato, che ci permetterà di eseguire logica di autorizzazione arbitraria.
- services.AddAuthorization(options =>
- {
- options.AddPolicy(“CourseLimit”, builder =>
- {
- builder.Requirements.Add(new CourseLimitRequirement(limit: 5));
- });
- });
La classe CourseLimitRequirement
qui illustrata implementa l’interfaccia IAuthorizationRequirement
. Opzionalmente, può avere un costruttore con dei parametri che poi riespone come proprietà. In pratica, i requirement sono semplici classi che servono unicamente a trasportare dati.
- public class CourseLimitRequirement : IAuthorizationRequirement
- {
- public CourseLimitRequirement(int limit)
- {
- Limit = limit;
- }
- public int Limit { get; }
- }
Ora è il momento di realizzare un AuthorizationHandler<TRequirement>
, ovvero la classe in cui inseriremo la logica di autorizzazione personalizzata che si occuperà di verificare se l’utente soddisfa il requirement. Abbiamo il pieno supporto alla dependency injection, perciò possiamo ricevere servizi dal suo costruttore e usarli nel metodo HandleRequirementAsync
.
- public class CourseLimitRequirementHandler : AuthorizationHandler<CourseLimitRequirement>
- {
- private readonly IHttpContextAccessor httpContextAccessor;
- // Ricevo un servizio tramite dependency injection
- public CourseLimitRequirementHandler(IHttpContextAccessor httpContextAccessor)
- {
- this.httpContextAccessor = httpContextAccessor;
- }
- protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
- CourseLimitRequirement requirement)
- {
- // Il parametro AuthorizationHandlerContext contiene la proprietà User, da cui ottengo l’identità
- // Il parametro CourseLimitRequirement mi permette di accedere ai valori trasportati dal requirement
- // Se una certa condizione è verificata…
- if (condizione)
- {
- // …allora indico che l’utente soddisfa il requirement…
- context.Succeed(requirement);
- }
- else
- {
- // …altrimenti l’autorizzazione fallisce
- context.Fail();
- }
- }
- }
L’AuthorizationHandler<TRequirement>
dovrà essere registrato per la dependency injection con un idoneo ciclo di vita.
– Usiamo il ciclo di vita Singleton
se si limita a verificare la presenza o il contenuto di determinati claim nell’identità dell’utente oppure se sfrutta servizi che sono stati essi stessi registrati per la dependency injection con il ciclo di vita Singleton
;
– Usiamo il ciclo di vita Scoped
se sfrutta servizi come un DbContext
che sono stati essi stessi registrati per la dependency injection con il ciclo di vita Scoped
.
Nel metodo ConfigureServices
della classe Startup
registriamo così l’AuthorizationHandler<TRequirement>
:
- // Uso il ciclo di vita Scoped perché qui il CourseLimitRequirementHandler
- // sfrutta un DbContext per accedere al database
- services.AddScoped<IAuthorizationHandler, CourseLimitRequirementHandler>();
- // Poi qui registro altri eventuali authorization handler
Autorizzazione a livello globale
Finora abbiamo visto come autorizzare puntualmente le singole action o i controller. In più, possiamo anche autorizzare a livello globale in modo che l’intera applicazione sia accessibile solo da utenti autenticati (tranne ovviamente la pagina di login che dovrà essere accessibile anche da utenti anonimi).
Ci sono due tecniche principali per autorizzare gli utenti a livello globale:
1) Usando un AuthorizeFilter
. Questa è la tecnica “classica”, ancora supportata in .NET 5 ma consigliata per applicazioni che usano .NET Core 2. Infatti, non è ben integrata con il meccanismo dell’endpoint routing introdotto a partire da .NET Core 3. Nel metodo ConfigureServices
della classe Startup
aggiungiamo quanto segue:
- services.AddMvc(options =>
- {
- // Questo codice è efficace sia per MVC che per Razor Pages
- AuthorizationPolicyBuilder policyBuilder = new();
- AuthorizationPolicy policy = policyBuilder.RequireAuthenticatedUser().Build();
- AuthorizeFilter filter = new(policy);
- options.Filters.Add(filter);
- // …
- });
In questo esempio stiamo costruendo una policy e applicandola a livello globale, cioè ASP.NET Core verificherà se l’utente soddisfa tale policy a prescindere da quale sia il controller o il page model richiesto.
2) Usando il metodo RequireAuthorization, che si integra meglio con l’endpoint routing e che perciò è la soluzione attualmente consigliata. Integriamo così la chiamata a UseEndpoints
nel metodo Configure
della classe Startup
:
- app.UseEndpoints(routeBuilder => {
- // Aggiungiamo il RequireAuthorization subito dopo la chiamate a MapControllerRoute
- routeBuilder.MapControllerRoute(“default”, “{controller=Home}/{action=Index}/{id?}”)
- .RequireAuthorization();
- // Se usiamo anche Razor Pages, aggiungiamola anche qui, dopo MapRazorPages
- routeBuilder.MapRazorPages()
- .RequireAuthorization();
- });
Il metodo RequireAuthorization
ammette anche il nome di una policy come argomento. È utile in applicazioni sottoposte a single sign-on, quando vogliamo che solo un segmento di utenti possa accedere ad una particolare applicazione.
Autorizzazione imperativa con il servizio IAuthorizationService
Finora abbiamo usato la cosiddetta “Autorizzazione dichiarativa” perché con l’attributo Authorize
ci limitiamo a dichiarare quali sono i requisiti per autorizzare l’accesso. Sarà poi ASP.NET Core, con il suo AuthorizationMiddleware
a decidere quando è il momento opportuno di verificare che l’utente sia conforme a quei requisiti.
Esiste anche un altro modello di autorizzazione, chiamato Autorizzazione imperativa che invece ci permette di decidere il momento esatto in cui verificare se l’utente soddisfa i requisiti. Questo è utile quando vogliamo autorizzare l’esecuzione di un pezzetto di codice, come entrare in un blocco if, o mostrare un pezzetto di HTML, come un link in un menu.
Il servizio da usare si chiama IAuthorizationService
e lo possiamo ottenere grazie alla dependency injection. Contiene il metodo AuthorizeAsync
che come argomenti ci permette di fornire l’utente e il nome della policy.
- AuthorizationResult result = await authorizationService.AuthorizeAsync(User, “CourseLimit”);
- if (result.Succeeded)
- {
- // L’utente soddisfava la policy, esegui un’operazione
- }
- else
- {
- // Altrimenti ne posso eseguire un’altra
- }
Il metodo AuthorizeAsync
ha anche un overload che ci permette di fornirgli un’istanza di AuthorizationPolicy
creata come segue.
- // Usiamo l’AuthorizationPolicyBuilder già visto in precedenza per aggiungere requirement alla policy
- AuthorizationPolicy policy = new AuthorizationPolicyBuilder().RequireRole(“Teacher”).Build();
- AuthorizationResult authorizationResult = await authService.AuthorizeAsync(User, policy);
- if (authorizationResult.Succeeded)
- {
- // L’utente soddisfava la policy, esegui un’operazione
- }
Il servizio IAuthorizationService
lo possiamo sfruttare anche da una view Razor. Lo riceviamo con la direttiva @inject
e poi, in base al risultato di AuthorizeAsync
, mostrare/nascondere un elemento dell’interfaccia HTML.
LINK AL CODICE SU GITHUB
Scaricare il codice della sezione18 oppure il ramo master o clonare il repository GITHUB per avere a disposizione tutte le sezioni nel tuo editor preferito.
Scrivi un commento