Search code examples
blazorblazor-server-sidewindows-authentication.net-9.0

How to setup Windows AD authentication in Blazor Server on .NET 9?


I have a Blazor Server (using .NET 9 Blazor web app template with render mode set to server). I want to set Windows AD authentication, but it always outputs a null username on the user page, and no roles as well. I am part of domain though.

If I use

var user = System.Security.Principal.WindowsIdentity.GetCurrent().Name;

then I was able to get the user name. But I also need to be able to get the roles of the user.

What am I missing in my code shown below?

null username

Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
    .AddNegotiate();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminsOnly", policy =>
        policy.RequireRole("DOMAIN\\AdminGroup"));
});

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

builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

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

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

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

app.Run();

User page

@page "/user"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider

<h3>User Information</h3>

@if (user != null)
{
    <p>Username: @user.Identity.Name</p>
    <ul>
        @foreach (var claim in user.Claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}
else
{
    <p>Loading...</p>
}

@code {
    private ClaimsPrincipal? user;

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        user = authState.User;
        var username = user.Identity?.Name;
        var roles = user.Claims.Where(c => c.Type == ClaimTypes.Role);
        Console.WriteLine("Roles: " + string.Join(", ", roles.Select(r => r.Value)));
    }
}

LaunchSettings.json:

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": true,
    "anonymousAuthentication": false,
    "iisExpress": {
      "applicationUrl": "http://localhost:28505",
      "sslPort": 44325
    }
  },
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5050",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "windowsAuthentication": true,
      "anonymousAuthentication": false
    },
    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:7150;http://localhost:5050",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "windowsAuthentication": true,
      "anonymousAuthentication": false
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "windowsAuthentication": true,
      "anonymousAuthentication": false
    }
  }
}

Solution

  • I may be totally wrong, but I don't think you get Roles through NTLM. There's no mapping from NT Groups to Roles.

    What you get is a set of groupsid:

    Claim Type: http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid; Claim Value: S-1-5-4
    

    Which you need to map manually into your roles.

    Here's a simple example using a CustomAuthenticationStateProvider which just checks the claims on the user and adds a new ClaimsIdentity with the appropriate roles.

    public class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider
    {
        private readonly record struct AdGroupMap(string ClaimType, string Value, string Role);
     
        private const string AdGroupClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid";
    
        private readonly List<AdGroupMap> _adGroupMaps = new()
        {
            new AdGroupMap(AdGroupClaimType, "S-1-5-32-545", "Admin")
        };
    
        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var authState = await base.GetAuthenticationStateAsync();
            var user = authState.User;
            List<Claim> claims = new();
    
            // Adds a new Identity to the ClaimsPrincipal with the Group pset
            foreach (var adGroupMap in _adGroupMaps)
            {
                if (user.Claims.Any(claim => claim.Type == adGroupMap.ClaimType && claim.Value == adGroupMap.Value))
                     claims.Add(new Claim(ClaimTypes.Role, adGroupMap.Role)) ;
            }
    
            if (claims.Any())
                user.AddIdentity(new ClaimsIdentity(claims));
    
            // return the modified principal
            return await Task.FromResult(new AuthenticationState(user));
        }
    }
    

    The demo repo is here: https://github.com/ShaunCurtis/SO79394377