Articoli

Autenticazione e protezione in Blazor WebAssembly – Parte 2

da |

Proseguiamo a parlare di autenticazione e autorizzazione. Nell’articolo precedente avevamo utilizzato ASP.NET Core Identity all’interno di un progetto API per fornire due endpoint: uno di registrazione e uno di signin. In caso di successo, l’endpoint restituiva un token che abbiamo utilizzato in un progetto Blazor WASM.

Diamo un’occhiata all’interno del progetto Blazor WASM alla pagina FetchData. Il codice è ancora quello del template originario fornito da Microsoft. Possiamo modificarlo in maniera tale da andare a recuperare i dati dalla nostra API dove c’è un endpoint chiamato WeatherForecast che restituisce gli stessi dati visualizzabili nella pagina. 

protected override async Task OnInitializedAsync() 
{ 
        string url = "https://localhost:6001/weatherforecast"; 
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>(url); 
} 

Come potete verificare, il codice continua a funzionare tranquillamente. Torniamo al progetto DemoSicurezza.API e aggiungiamo al controller WeatherForecast un’annotazione [Authorize()]

[ApiController] 
[Authorize()] 
[Route("[controller]")] 
public class WeatherForecastController : ControllerBase 
{ 
…. 
}; 

Se nel client WASM eseguiamo il sign out e proviamo ad aprire la pagina FetchData troviamo un errore: il server ci ha risposto con uno status 401. Ossia la richiesta non è autorizzata.   

Se eseguiamo il sign in e ricarichiamo la pagina FetchData torna di nuovo a funzionare tutto.  

Un’applicazione reale ha però bisogno di gestire uno o più ruoli in maniera tale da offrire diverse funzionalità ai suoi utenti in base al loro ruolo di appartenenza. 

Il progetto DemoSicurezza.API è già stato configurato  per utilizzare i ruoli attraverso l’aggiunta di AddRoles() 

builder.Services.AddDefaultIdentity<IdentityUser>() 
.AddRoles<IdentityRole>() 
.AddEntityFrameworkStores<ApplicationDbContext>(); 

Come si può notare, la creazione e la gestione dei ruoli dipende da come andiamo a persistere i dati del processo di autorizzazione. I ruoli vengono esposti attraverso il metodo IsInRole sulla classe ClaimsPrincipal. Con ASP.NET Core Identity possiamo creare quanti ruoli desideriamo e assegnarli agli utenti grazie alla classe RoleManager<T> dove T è la classe che rappresenta i ruoli nel nostro Database (nel nostro caso è IdentityRole, ossia quella base offerta dalla libreria).  Ricordiamo che tutte le classi base  possono essere customizzate per rispondere alle esigenze del progetto, ad esempio il vostro utente potrebbe avere delle proprietà che mancano nella classe base IdentityUser. Nel nostro caso abbiamo scelto SQLite: se apriamo il database (mydb.db) contenuto all’interno del folder Data  del progetto DemoSicurezza.API, troveremo le due seguenti tabelle. La prima conterrà i Ruoli e la seconda le associazioni di un ruolo a un utente. 

Potremmo inserire manualmente i dati in queste due tabelle ma preferisco a scopo didattico farvi vedere come creare una classe che all’avvio della API andrà a verificare l’esistenza di ruoli e la loro assegnazione.

public static class SeedRuoliEUtenti 
{ 
     public async static Task Seed( 
            RoleManager<IdentityRole> roleManager, 
            UserManager<IdentityUser> userManager, 
            string utente, 
            string ruolo) 
     { 
            await CreaRuolo(roleManager, ruolo); 
            await AssegnaRuoloAdUtente(userManager, utente, ruolo); 
     } 
     
     private async static Task CreaRuolo( 
            RoleManager<IdentityRole> roleManager, string ruolo) 
     { 
            bool esisteRuolo = 
                await roleManager.RoleExistsAsync(ruolo); 
            if(!esisteRuolo) 
            { 
                var role = new IdentityRole 
                { 
                    Name = ruolo 
                }; 
                await roleManager.CreateAsync(role); 
            } 
     } 

     private async static Task AssegnaRuoloAdUtente( 
            UserManager<IdentityUser> userManager, 
            string utente, string ruolo) 
     { 
            bool esisteUtente = await userManager.FindByEmailAsync 
                (utente) != null; 
            if(esisteUtente == false) 
            { 
                var nuovoUtente = new IdentityUser 
                { 
                    UserName = utente, 
                    Email = utente 
                }; 
                var result = await userManager.CreateAsync(nuovoUtente,          "MiaPassword1!"); 

                if(result.Succeeded == true) 
                { 
                    await userManager.AddToRoleAsync(nuovoUtente, 
                                       ruolo); 
                } 
           }  
     } 
 } 

Nel Program.cs all’interno dell’if relativo all’ambiente di sviluppo possiamo invocare il metodo Seed.

if (app.Environment.IsDevelopment()) 
{ 
    app.UseSwagger(); 
    app.UseSwaggerUI(); 
    var roleManager = 
            builder.Services.BuildServiceProvider() 
                   .GetService<RoleManager<IdentityRole>>(); 
    var userManager = 
            builder.Services.BuildServiceProvider() 
                   .GetService<UserManager<IdentityUser>>(); 

    await SeedRuoliEUtenti.Seed(roleManager, userManager, 
        "admin@admin.com", "admin"); 

    await SeedRuoliEUtenti.Seed(roleManager, userManager, 
        "user@user.com", "user"); 
} 

Quando l’utente user@user.com si collega vedremo la situazione seguente 

Quindi ai claim dell’utente è stato aggiunto anche quello del ruolo user. 

Proviamo ad aggiungere il ruolo user anche all’utente admin@admin.com ( che è già appartenente al ruolo admin). Ciò che vedremo è 

Ora, per esempio, possiamo usare il ruolo per autorizzare l’accesso alla pagina FetchData solo a chi appartenga al ruolo admin. 

@page "/fetchdata" 
@using Microsoft.AspNetCore.Authorization 
@inject HttpClient Http 
@attribute [Authorize(Roles = "admin")] 
<PageTitle>Weather forecast</PageTitle> 

Possiamo anche impostare il menu in base al ruolo dell’utente andando a modificare il componente NavMenu.

<AuthorizeView> 
    <Authorized> 
        @if (context.User.IsInRole("admin")) 
        { 
            <div class="nav-item px-3"> 
                 <NavLink class="nav-link" href="fetchdata"> 
                            <span class="oi oi-list-rich" aria-hidden="true">.    </span> Fetch data 
                 </NavLink> 
            </div> 
        } else 
        { 
            <div class="nav-item px-3"> 
                 <NavLink class="nav-link" href="counter"> 
                      <span class="oi oi-list-rich" aria-hidden="true"></span> Menu non admin 
                 </NavLink> 
            </div> 
        } 
     </Authorized> 
</AuthorizeView> 

In ogni caso è sempre buona norma proteggere anche il lato server e quindi andremo a restringere l’accesso alla API WeatherForecast solo agli admin. Perché? Non dimentichiamo mai che il nostro codice lato client viene eseguito nel browser e può essere modificato da un utente malevolo. 

[ApiController] 
[Authorize(Roles ="admin")] 
[Route("[controller]")] 
public class WeatherForecastController : ControllerBase 
{ 
}; 

E se volessimo un controllo ancora più granulare? Possiamo creare nel nostro codice lato server una policy che richieda sia l’appartenenza a un ruolo che il possesso di un claim specifico. La policy va aggiunta nel Program.cs all’interno di AddAuthorization()

builder.Services.AddAuthorization(opts => { 
    opts.AddPolicy("BlazorAdmin", policy => {
        policy.RequireRole("admin"); 
        policy.RequireClaim("blazor", "maestro"); 
    }); 
}); 

Modifichiamo il controller WeatherForecast richiedendo l’uso della policy 

[ApiController] 
[Authorize(Policy = "BlazorAdmin")] 
[Route("[controller]")] 
public class WeatherForecastController : ControllerBase 
{ 
        ….. 
}; 

Se provate a collegarvi anche come admin riceverete il seguente messaggio d’errore: 

Per risolverlo, quindi, dobbiamo assegnare al nostro utente admin il claim richiesto. 

Il passo successivo consistere nell’aggiungere al token jwt anche il nostro Claim customizzato. Se ricordate lo scorso articolo, il token viene creato nel metodo GeneraJSONWebToken del controller AccountsController. I Claim aggiunti alla tabella AspNetUserClaims sono disponibili attraverso la classe UserManager<IdentityUser> 

var userclaims = await userManager.GetClaimsAsync(identityUser); 
var blazorClaim = userclaims.FirstOrDefault(x => x.Type == "blazor"); 

Modifichiamo quindi la lista di claim da aggiungere al token jwt: 

var claims = new List<Claim>() 
           { 
                    new Claim("blazor", blazorClaim?.Value ?? ""),  
                    new Claim(ClaimTypes.NameIdentifier, identityUser.Id), 
                    new Claim(ClaimTypes.Name, identityUser.Email), 
                    new Claim(ClaimTypes.Email, identityUser.Email), 
                    new Claim(JwtRegisteredClaimNames.Sub, identityUser.Email), 
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), 
           }.Union(roleNames.Select(role => new Claim(ClaimTypes.Role, role))); 

JwtSecurityToken jwtSecurityToken = new JwtSecurityToken( 
                configuration["Jwt:Issuer"], 
                configuration["Jwt:Audience"],
                claims, DateTime.UtcNow, DateTime.UtcNow.AddMinutes(10), 
               credentials ); 
….. 

Fatte queste modifiche, il nostro utente admin@admin.com tornerà ad accedere alla pagina FetchData. Modifichiamo anche il componente NavMenu per tenere conto della nuova richiesta 

<AuthorizeView> 
            <Authorized> 
                @if (context.User.Claims.FirstOrDefault 
                    (x => x.Type == "blazor" && x.Value == "maestro") != null) 
                { 
                    <div class="nav-item px-3"> 
                        <NavLink class="nav-link" href="fetchdata"> 
                            <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data 
                        </NavLink> 
                    </div> 
                } else 
                { 
                     <div class="nav-item px-3"> 
                        <NavLink class="nav-link" href="counter"> 
                            <span class="oi oi-list-rich" aria-hidden="true"></span> Menu non admin 
                        </NavLink> 
                    </div> 
                } 
            </Authorized> 
</AuthorizeView> 

Siamo giunti al termine di questa mini serie dove abbiamo mostrato come usare l’autenticazione e l’autorizzazione in un progetto Blazor WASM utilizzando ASP.Net Core Identity sul fronte server e gestendo cosa visualizzare su quello client analizzando il token ricevuto al momento dell’autenticazione. Lo abbiamo fatto dunque usando solo librerie esistenti da anni e ormai consolidate. Spero di aver attirato la vostra attenzione e attendo qualche vostro spunto e commento per eventualmente estendere la serie.

Il codice è disponibile sulla seguente repository github nel branch parte2

Alla prossima

Scritto da: