11 aprile 2022 09:37
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();