Search code examples
asp.net-coreauthenticationblazor-server-side

Blazor Server .NET 8 with Windows Authentication and role authorization


I am migrating a Blazor server project from .NET 7 to .NET 8 and I am struggling with the Authorization. When I click on a link to my Admin page it is working fine but when I just go directly to the URL I get a 403 forbidden. I am guessing this has something to do with my routing. What I am missing?

Github sample

My simple AuthenticationStateProvider implementation:

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CustomAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor)
    {
        this._httpContextAccessor = httpContextAccessor;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var identity = _httpContextAccessor.HttpContext.User.Identity;
        var windowsAccountName = identity.Name.Split('\\').Last();

        var ci = identity as ClaimsIdentity;
        var user = new ClaimsPrincipal(identity);

        user.AddIdentity(new ClaimsIdentity(new List<Claim>() { new Claim(ClaimTypes.Role, "Admin") }));
        user.AddIdentity(new ClaimsIdentity(new List<Claim>() { new Claim(ClaimTypes.Role, "Manager") }));
        return await Task.FromResult(new AuthenticationState(user));
    }
}

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

//Windowes authentication setup
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
       .AddNegotiate();
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});

builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
builder.Services.AddHttpContextAccessor();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

My Admin.razor

@page "/admin"
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization

@attribute [Authorize(Roles = "Admin")]

<h3>Admin Page</h3>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

My Routes.razor

<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
            <NotAuthorized>
                <h1>Unauthorized</h1>
                <p>Sorry, you are not authorized to view this page!</p>
            </NotAuthorized>
            <Authorizing>
                <h1>Please wait</h1>
                <p>You are being authorized...</p>
            </Authorizing>
        </AuthorizeRouteView>
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
</Router>

Solution

  • After debugging the issue, I found a solution for this issue.

    BlazorAuthorizationMiddlewareResultHandler

    using Microsoft.AspNetCore.Authorization.Policy;
    using Microsoft.AspNetCore.Authorization;
    
    namespace BlazorAppWindowsAuth
    {
        public class BlazorAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
        {
            public Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
            {
                return next(context);
            }
        }
    }
    

    Register it

    using BlazorAppWindowsAuth;
    using BlazorAppWindowsAuth.Components;
    using Microsoft.AspNetCore.Authentication.Negotiate;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Components.Authorization;
    using System.Security.Claims;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddRazorComponents()
        .AddInteractiveServerComponents();
    builder.Logging.SetMinimumLevel(LogLevel.Debug);
    //Windowes authentication setup
    builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
           .AddNegotiate();
    builder.Services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });
    
    
    builder.Services.AddCascadingAuthenticationState();
    builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
    builder.Services.AddHttpContextAccessor();
    builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, BlazorAuthorizationMiddlewareResultHandler>();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error", createScopeForErrors: true);
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    
    app.UseStaticFiles();
    app.UseAntiforgery();
    
    app.MapRazorComponents<App>()
        .AddInteractiveServerRenderMode();
    
    app.Run();
    

    Got inspiration from this link.

    One more thing, your CustomAuthenticationStateProvider method seems not correct. We may find the error User is not in the Admin role. Here is my working sample code.

    using Microsoft.AspNetCore.Components.Authorization;
    using System.Security.Claims;
    
    namespace BlazorAppWindowsAuth;
    
    public class CustomAuthenticationStateProvider : AuthenticationStateProvider
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
    
        public CustomAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }
    
        public override Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var originalIdentity = _httpContextAccessor.HttpContext.User.Identity;
    
            if (originalIdentity == null || !originalIdentity.IsAuthenticated)
            {
                return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
            }
    
    
            var claims = _httpContextAccessor.HttpContext.User.Claims.ToList();
            var claimsIdentity = new ClaimsIdentity(claims, originalIdentity.AuthenticationType);
    
    
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "Manager"));
    
            var user = new ClaimsPrincipal(claimsIdentity);
            return Task.FromResult(new AuthenticationState(user));
        }
    }