.NET Core WebApi Secure Hangfire Dashboard with Azure AD Login

Viewed 780

I am currrently trying to secure the access to Hangfire Dashboard.

As I am using a .NET Core WebApi I do not really know how to secure the dashboard with Azure AD.

I tried to use a Policy, but without success:

services.AddAuthorization(options =>
            {
                // Policy to be applied to hangfire endpoint
                options.AddPolicy(AppConstants.HangfirePoilicyName, builder =>
                {
                    builder
                        .AddAuthenticationSchemes(AzureADDefaults.AuthenticationScheme)
                        .RequireAuthenticatedUser();
                });
            });

And here the Configure method:

app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                        name: "default",
                        pattern: "{controller}/{action=Index}/{id?}")
                    .RequireAuthorization();
                
                endpoints.MapHangfireDashboard("/hangfire", new DashboardOptions()
                    {
                        Authorization = new List<IDashboardAuthorizationFilter> { }
                    })
                    .RequireAuthorization(AppConstants.HangfirePoilicyName);
            });

What I expect is when I go to the Hangfire Dashboard that I see the Microsoft Login through Azure AD and then login to my tenant and gain access to the dashboard.

Maybe worth to mention: Default authentication is JWTBearerDefaults, but for the Hangfire Dashboard I need AzureAdDefaults

2 Answers

Some preamble information:

  • The solution I have is for implementations that follow the MSAL 2 flows, using .NET Core 6.
  • For the statement:

Default authentication is JWTBearerDefaults, but for the Hangfire Dashboard I need AzureAdDefaults

AzureAdDefaults is now obsolete, so you would need to use something else, like the JWTBearerDefaults, but read on to see how I got my implementation working.

  • Another confusing thing for me was having a WebApi which also acted as a WebApp, since you're enabling application-like authentication on certain endpoints, but on others, not. Turns out this is possible.

Solution:

I came across this nugget after quite a lot of browsing through code samples:

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
          .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
              .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                  .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
                  .AddInMemoryTokenCaches();

services.AddAuthentication()
        .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"),
                                    JwtBearerDefaults.AuthenticationScheme)
        .EnableTokenAcquisitionToCallDownstreamApi();

Essentially, this is saying you can have 2 separate calls to .AddAuthentication(...), with 1 chaining the AddMicrosoftIdentityWebApp call, and another the AddMicrosoftIdentityWebApi call, specifying which authentication to use in both cases.

For me, I setup my Api to authenticate with JwtBearerDefaults.AuthenticationScheme, and the WebApp portion uses OpenIdConnectDefaults.AuthenticationScheme, making it look like this:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"),
                                  JwtBearerDefaults.AuthenticationScheme);

services.AddAuthentication()
            .AddMicrosoftIdentityWebApp(Configuration, "AzureAd", OpenIdConnectDefaults.AuthenticationScheme);

Next, setup a policy for Hangfire to use, which matches the scheme previously setup for the WebApp portion.

services.AddAuthorization(options =>
        {
            options.AddPolicy("azureAdPolicy", builder =>
            {
                builder
                    .AddAuthenticationSchemes(OpenIdConnectDefaults.AuthenticationScheme)
                    .RequireAuthenticatedUser();
            });
        });

I then created some extension methods for setting up the Hangfire services and routes.

public static IServiceCollection AddHangfireService(
        this IServiceCollection services, 
        IConfiguration configuration)
    {
        services.AddHangfireServer();
        services.AddHangfire(cfg =>
        {
            // Additional setup code
        });
        return services;
    }

Called like so:

 services.AddHangfireService(Configuration);

For the routing:

public static IEndpointRouteBuilder AddHangfireRoute(this IEndpointRouteBuilder endpoints,
        HangfireDBOptions hangfireDbOptions)
    {
        endpoints.MapHangfireDashboard("/hangfire", new DashboardOptions
        {
            DashboardTitle = "Your App Title",
            AppPath = hangfireDbOptions.BackToSiteUrl,
            Authorization = new[]
            {
                new HangfireDashboardAuthFilter() // if you need a filter
            }
        })
        .RequireAuthorization("azureAdPolicy"); // Matches your policy name
        return endpoints;
    }

Which is called like so:

app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers(); // Call this first

            endpoints.AddHangfireRoute(hangfireOptions);
        });

The only thing left to do is properly configure the routes for your Hangfire routes in your Azure AD environment.

On your App Registration, add a Web application type, and add the following routes:

  • (your base application URL)/signin-oidc
  • (your base application URL)
  • (your base application URL)/hangfire

And make sure to have the ID Token checkbox selected

Screenshot of Azure AD ID Token checkbox

That should be it :) Hope this helps, your application might be using other auth flows or older library versions etc - this is what I got working which is currently used in production. I would recommend you upgrade your packages anyway as there are a lot of 'better' auth flows to use, and as I mentioned, some features are now obsolete.

Humble apologies again for the delayed answer, was quite a lot of information to compile

I think you are missing an AddAuthentication call for the AzureAdDefaults:

services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
        .AddAzureAD(options => Configuration.Bind("AzureAd", options));

With this call the rest of code should work. As you mentioned your policy must be against AzureAdDefaults so you need to add authentication for the scheme.

Where AzureAd is the following configuration:

"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]",
    "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]",
    "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]"
}
Related