Search code examples
c#authenticationblazorblazor-server-side

How to customize Default Windows AutheticationStateProvider in Blazor server app?


I have a Blazor Server app that uses Windows authentication to identify users. The app "detects" the user currently browsing the app and can display domain/username correctly.

I wrote a class that checks logged in username and fetches roles from a database, then updates the current principal so that the UI can properly take advantage of the given roles for the user.

I am avoiding EntityFramework and core identity scaffolding code for reasons I can't share here.

Here is the class I wrote

public class CompanyAuthentication
{
    public CompanyAuthentication(AuthenticationStateProvider auth)
    {
        var result = auth.GetAuthenticationStateAsync().Result;
        // here I have access to the current username:
        string username = result.User.Identity.Name;
        // now I can fetch roles from file, but for simplicity, I will use the following:
        if(username.BeginsWith("admin"))
        {
            var claims = new ClaimsIdentity(new Claim[] {new Claim(ClaimType.Role, "admin")});
            result.User.AddIdentity(claims);  // this will have an effect on UI
        }
     
    }
}

I also added the above class as a service in Startup.cs

services.AddScoped<CompanyAuthentication>();

Now, in any razor page, I simply do:

@page "/counter"
@inject CompanyAuthentication auth

<AuthorizeView Roles="admin">
    <Authorized> Welcome admin </Authorized>
    <NotAuthorized> You are not authorized </NotAuthorized>
</AuthorizeView>

That all works fine, as long as I don't forget to inject CompanyAuthentication in each page I intend to use.

Is there a way to automatically inject my class into every page without having to do @inject ? I am aware that I can write a custom authentication class that inherits AuthenticationStateProvider and then add it as service, but if I do that, I lose access to the currently logged in username, and thus, I can't fetch roles from the database.

I tried to use HttpContext.Current.User.Identity.Name but that is not in the scope of any part in a Balzor server app.

How can I take advantage of Windows AuthenticationStateProvider and at the same time, customize its roles, without having to inject the customization everywhere ?


Solution

  • You should be able to write a custom AuthenticationStateProvider. I've set up a Blazor Server site with authentication set to Windows.

    The custom AuthenticationStateProvider. Note it inherits from ServerAuthenticationStateProvider.

    public class MyAuthenticationStateProvider : ServerAuthenticationStateProvider
    {
        private bool _isNew = true;
    
        public async override Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var state = await base.GetAuthenticationStateAsync();
            // Add your code here to get the user info to use in the logic of what roles you add
    
            // only add the extra Identity once
            if (_isNew)
                state.User.AddIdentity(AdminIdentity);
            
            _isNew = false;
            return state;
        }
    
        private ClaimsIdentity AdminIdentity
            => new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, "admin") }, "My Auth Type");
    }
    

    Register it in services - note the order:

    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
       .AddNegotiate();
    
    builder.Services.AddAuthorization(options =>
    {
        options.FallbackPolicy = options.DefaultPolicy;
    });
    
    builder.Services.AddRazorPages();
    builder.Services.AddServerSideBlazor();
    // Add custom AuthenticationStateProvider after AddServerSideBlazor will overload the existing registered service
    builder.Services.AddScoped<AuthenticationStateProvider, MyAuthenticationStateProvider>();
    builder.Services.AddSingleton<WeatherForecastService>();
    
    var app = builder.Build();
    

    And here's a test page to show you the claims:

    @page "/"
    @inject AuthenticationStateProvider Auth;
    @using System.Security.Claims
    <PageTitle>Index</PageTitle>
    
    <h1>Hello, world!</h1>
    
    Welcome to your new app.
    
    <SurveyPrompt Title="How is Blazor working for you?" />
    
    @if(user is not null)
    {
        @foreach(var claim in user.Claims)
        {
            <div>
                @claim.Type : @claim.Value
            </div>
        }
    }
    
    @code{
        private ClaimsPrincipal user = default!;
    
        protected async override Task OnInitializedAsync()
        {
            var state = await Auth.GetAuthenticationStateAsync();
            user = state.User;
        }
    }
    

    Here's a screen capture showing both the user windows security info and the added Role.

    enter image description here