Il blog di Sandro Rizzetto

Blazor: gestire la Role Based Authorization insieme alla Windows Authentication

 

In rete si trovano molti esempi di come gestire in Blazor Server Side la Role Based Authorization tramite gli indivual accounts, ovvero l'infrastruttura base di Asp.Net Identity con le tabelle ASPNET* create dal template di Visual Studio oppure dalla prima migration se portiamo dentro il codice necessario tramite New Scaffolded Item...

Non ho trovato nulla però a riguardo quando invece usiamo la "vetusta" Windows/NTLM authentication magari per una intranet o una blazor webapp che giri nella nostra LAN e dove gli utenti sono gli employees autenticati nel dominio. Sembra che per MS ormai il mondo faccia login solo su Azure AAD oppure con forms o provider esterni... Mah!

Con l'aiuto di un paio di colleghi che stanno sbattendo il naso con Auth0 e altre faccende di security sono comunque riuscito a venirne a capo e riporto qui i passaggi necessari per proteggere la nostre page o component a seconda di un Role scritto in una nostra tabella sql custom.

Quello che vogliamo sfruttare è ovviamente l'infrastruttura base già presente in Blazor che ci consente di:

  • proteggere parte di un componente/page tramite il tag <AuthorizeView Roles="xyz">
  • proteggere tutto un componente/page con @attribute [Authorize(Roles = "xyz")] nel codice razor oppure con [Authorize(Roles ="ADMIN")] nella partial class facendo venire fuori il messaggi presente in app.razor nel tag <NotAuthorized>

 Esempi di pages/components:

<AuthorizeView Roles="ADMIN">
    <Authorized>
        <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
    </Authorized>
    <NotAuthorized>
        <p>Sorry, solo gli admin possono incrementare il counter</p>
     </NotAuthorized>
</AuthorizeView>
@page "/fetchdata"
@attribute [Authorize(Roles = "ADMIN")]
[Authorize(Roles = "ADMIN")]
public partial class FetchData

app.razor:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
       <Found Context="routeData">
           <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                        <p>Sorry, il tuo ruolo non prevede l'accesso a questa pagina!</p>
                </NotAuthorized>
           </AuthorizeRouteView>
           <FocusOnNavigate RouteData="@routeData" Selector="h1" />
         </Found>
        <NotFound>
            <PageTitle>Not found</PageTitle>
            <LayoutView Layout="@typeof(MainLayout)">
                   <p role="alert">Sorry, there's nothing at this address.</p>
            </LayoutView>
         </NotFound>
    </Router>  
</CascadingAuthenticationState>

La cosa è banale (quando si sa!) in quanto è sufficiente crearsi una classe che implementi IClaimsTransformation e che nel suo metodo TransformAsync aggiunga i ruoli dell'utente allo user ricavato dal ClaimsPrincipal.

Ovviamente i ruoli andranno presi dal proprio DB o da altre fonti a piacimento (nel mio caso lo fa un servizio _roleService iniettato con Dependency Injection)

public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
 {
    var currentAuthenticatedUser = (ClaimsIdentity)principal.Identity;

    //tolgo il domainname dallo username che viene ritornato nella forma DOMAIN\userName)
    var userName = currentAuthenticatedUser?.Name.Replace(@"YOURDOMAIN\", "");

   // uso diretto del dbContext con la classica struttura a 3 tabelle many-to-many Users, Roles, UsersRoles
    //var roles = await _dbContext.Roles
    // .Where(r => r.Users.Any(u => u.DomainAccount == userName))
    // .ToListAsync();
   var roles = await _roleService.GetRolesByEmployee(userName);    foreach (var role in roles)     //decidiamo noi se aggiungere l'Id -se stringa parlante- o qualche altro campo della tabella Ruoli       var roleClaim = new Claim(currentAuthenticatedUser.RoleClaimType, role.Id);       currentAuthenticatedUser.AddClaim(roleClaim);    }    return principal;

Una volta fatto questo non ci resterà che aggiungere il nostro servizio nel program.cs o nella ServiceExtension che ci saremmo fatti. Lascio anche per reference le righe che servono per "accendere" la windows Authentication


// Add services to the container.
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();

builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy.
options.FallbackPolicy = options.DefaultPolicy;
});

...

// Roles enricher per attaccare i ruoli al claim
builder.Services.AddSingleton<IClaimsTransformation, RolesEnricherService>();

...

app.UseAuthentication();
app.UseAuthorization();

 

Commenti (3) -

  • Daniele

    04/05/2022 15:02:46 | Rispondi

    Tutto molto bello tranne la necessità di fare il replace di YOURDOMAIN per avere lo username!  :o  Smile

    • Sandro

      04/05/2022 15:39:20 | Rispondi

      Perché sei stato più furbo e nel db hai tenuto gli user nella forma domain\username ?

  • Danilo

    11/03/2023 09:43:20 | Rispondi

    Grazie mille dell'articolo, ci stavo sbattendo da un po'.

Aggiungi Commento

Copyright © 1997-2024 Sandro Rizzetto | All Rights Reserved | Riproduzione delle fotografie vietata | Powered by me