Ramblings and musings of a geek

Blazor Server + Windows Auth (AD)

This blog runs through the steps required to integrate Windows AD into your Blazor Server application running on .netcore 6. I promised an example to someone who has been trying to get this up and running this week and thought I’d throw the info up here in case it is also of use to someone else. It seems that when it isnt wired up properly you get some unhelpful messages!

All of the code can be found on GitHub.

Blazor, for those who haven’t used it, has been a game changer since it arrived on the scene in 2018 (it is soooo productive). It is Microsoft’s .net based Single Page App technology. Over in Cortex Towers we have been using it to develop our own applications and products and also to deliver a number of client projects. Whilst our own projects have been hooking into Azure AD or using Identity framework for the authentication and authorisation, the client applications remain on-premise Active Directory based. Whilst not hugely complicated, you don’t get very friendly messages back when it isn’t set up properly.

This blog will:

  • Detail what needs to be added to the Program.cs class that bootstraps the application
  • Run through a few helper classes that allow you to develop the application without needing AD
  • Plumb in some auth checks
    • Secure some of the navigation items (hide them from users without authorisation)
    • Secure the page themselves from direct access (from those who know the url)

Let’s get started

The example site that we are setting up builds upon the boilerplate blazor server application you get when you create a new project.

dotnet new blazorserver -o WindowsAuth

Once we have made our changes this will look like this…

We will set up 3 security policies for the application.

  1. Application Access - we will bind this to the AD Group “Everyone” (this could be restricted to a specific group)
  2. Administrator - we will bind this to an AD Group “Sample App Admins” (you will need to change this to an AD Group name that exists within your domain)
  3. ReportViewer - we will bind this to an AD group “AD Group Name” that my user doesnt have to test the unauthorised view checks

We will secure the Counter menu to this last policy so that it renders like so for my user:

The boilerplate blazor server app (counter with weather service) for Program.cs looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var builder = WebApplication.CreateBuilder(args);

// 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;
});
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();

We need to replace line 4 with the following:

 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
builder.Services.AddAuthentication(IISDefaults.AuthenticationScheme);

var devMode = false; // This should be pulled from config
if (devMode)
{
    builder.Services.AddAuthorization(options =>
    {
        //No fallback policy in dev so that we don't need access to AD/the correct groups for testing
        options.AddPolicy(Policies.IsAdministrator, policy => policy.Requirements.Add(new Requirements.IsDeveloperRequirement()));
        options.AddPolicy(Policies.IsReportViewer, policy => policy.Requirements.Add(new Requirements.IsDeveloperRequirement()));
    });
                
    builder.Services.AddSingleton<IAuthorizationHandler, Handlers.IsDeveloperHandler>();
}
else
{
    builder.Services.AddAuthorization(options =>
    {
        //Prevents access to any part of the application without the specified role
        //https://andrewlock.net/setting-global-authorization-policies-using-the-defaultpolicy-and-the-fallbackpolicy-in-aspnet-core-3/
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireRole("Everyone")
            .Build();

        options.AddPolicy(Policies.IsAdministrator, policy => policy.RequireRole("Everyone"));
        options.AddPolicy(Policies.IsReportViewer, policy => policy.RequireRole("AD Group Name"));
    });
}

Breaking this down…

Specifiy Windows AD as the authentication method

Line 4: specifies that Windows should be used for authentication.

builder.Services.AddAuthentication(IISDefaults.AuthenticationScheme);

Define a dev mode to ignore AD

Lines 9 to 16 let us run without any AD or have our profiles set up with the relevant AD groups. This makes development easier.

IsDeveloperRequirement is a class that extends the IAuthorizationRequirement marker interface (it has no methods) that is used to obtain the correct handler for the policy.

    public class IsDeveloperRequirement : IAuthorizationRequirement
    {
        // nothing to see here :)
    }

We create a dev handler for this policy requirement that authorises all requests.

    public class IsDeveloperHandler : AuthorizationHandler<Requirements.IsDeveloperRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, Requirements.IsDeveloperRequirement requirement)
        {
            context.Succeed(requirement);

            return Task.CompletedTask;
        }
    }

Set up the AD Group to Policy bindings

We want to secure access to the application as a whole - ie. you need to be a member of the AD Group blah (Everyone in the case below) in order to use it. We also have two special types of users, Administrators and ReportViewers. We create policies for these, binding the AD Group Name to the policy so that we can use these in the application.

Lines 20 to 29 sets this up…

    //Prevents access to any part of the application without the specified role
    //https://andrewlock.net/setting-global-authorization-policies-using-the-defaultpolicy-and-the-fallbackpolicy-in-aspnet-core-3/
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireRole("Everyone")
        .Build();

    // In the github repo the constants below have been added to the Policies class
    options.AddPolicy("Administrators", policy => policy.RequireRole("Everyone"));
    options.AddPolicy("ReportViewers", policy => policy.RequireRole("AD Group Name"));

Applying the policies

You can apply policies to both:

  • Portions of content within of a page
  • Entire pages

Content

Line 8 applies the authorisation check. If the user is authorised (satisfies the policy ReportViewers) then contents of the <Authorized> are rendered.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <AuthorizeView Policy="@Policies.IsReportViewer">  @* ie. "ReportViewers" *@
            <Authorized>
                <div class="nav-item px-3">
                    <NavLink class="nav-link" href="counter">
                        <span class="oi oi-plus" aria-hidden="true"></span> Counter
                    </NavLink>
                </div>
            </Authorized>
        </AuthorizeView>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </div>
    </nav>
</div>

Page level

You can apply policies via the Authorize attribute.

1
2
3
@page "/counter"
@attribute [Authorize(Policy = Policies.IsReportViewer)]
...

You can apply auth checks in a number of different places. If you want to dig deeper into this then Matt Champion covers it very nicely on his blog.

Wrapping up

That is pretty much it. This was intended to be a intro (simple) to getting Windows AD integrated into a blazor app. It gives you a taste of some of the custom policies which let you apply some weird and wonderful rules. This is using policy based authorisation. You can use role based auth too. That will be a topic for another post.

The sample application can be found on the CortexGG github.