Using Hangfire Dashboard in ABP API Website 🚀

cover

Introduction

In this article, I'll show you how to integrate and use the Hangfire Dashboard in an ABP API website.

Typically, API websites use JWT Bearer authentication, but the Hangfire Dashboard isn't compatible with JWT Bearer authentication. Therefore, we need to implement Cookies and OpenIdConnect authentication for the Hangfire Dashboard access.

Creating a New ABP Demo Project 🛠️

We'll create a new ABP Demo Tiered project that includes AuthServer, API, and Web projects.

abp new AbpHangfireDemoApp -t app --tiered

Now let's add the Hangfire Dashboard to the API project and configure it to use Cookies and OpenIdConnect authentication for accessing the dashboard.

Adding a New Hangfire Application 🔧

We need to add a new Hangfire application to the appsettings.json file in the DbMigrator project:

Note: Replace 44371 with your API project's port.

"OpenIddict": {
    "Applications": {
        //...
        "AbpHangfireDemoApp_Hangfire": {
            "ClientId": "AbpHangfireDemoApp_Hangfire",
            "RootUrl": "https://localhost:44371/"
        }
        //...
    }
}
  1. Update the OpenIddictDataSeedContributor's CreateApplicationsAsync method in the Domain project to seed the new Hangfire application.
 //Hangfire Client
var hangfireClientId = configurationSection["AbpHangfireDemoApp_Hangfire:ClientId"];
if (!hangfireClientId.IsNullOrWhiteSpace())
{
    var hangfireClientRootUrl = configurationSection["AbpHangfireDemoApp_Hangfire:RootUrl"]!.EnsureEndsWith('/');

    await CreateApplicationAsync(
        applicationType: OpenIddictConstants.ApplicationTypes.Web,
        name: hangfireClientId!,
        type: OpenIddictConstants.ClientTypes.Confidential,
        consentType: OpenIddictConstants.ConsentTypes.Implicit,
        displayName: "Hangfire Application",
        secret: configurationSection["AbpHangfireDemoApp_Hangfire:ClientSecret"] ?? "1q2w3e*",
        grantTypes: new List<string> //Hybrid flow
        {
            OpenIddictConstants.GrantTypes.AuthorizationCode, OpenIddictConstants.GrantTypes.Implicit
        },
        scopes: commonScopes,
        redirectUris: new List<string> { $"{hangfireClientRootUrl}signin-oidc" },
        postLogoutRedirectUris: new List<string> { $"{hangfireClientRootUrl}signout-callback-oidc" },
        clientUri: hangfireClientRootUrl,
        logoUri: "/images/clients/aspnetcore.svg"
    );
}
  1. Run the DbMigrator project to seed the new Hangfire application.

Adding Hangfire Dashboard to the API Project 📦

  1. Add the following packages and modules dependencies to the API project:
<PackageReference Include="Volo.Abp.BackgroundJobs.HangFire" Version="9.2.0" />
<PackageReference Include="Volo.Abp.AspNetCore.Authentication.OpenIdConnect" Version="9.2.0" />
<PackageReference Include="Hangfire.SqlServer" Version="1.8.20" />
typeof(AbpBackgroundJobsHangfireModule),
typeof(AbpAspNetCoreAuthenticationOpenIdConnectModule)
  1. Add the HangfireClientId and HangfireClientSecret to the appsettings.json file in the API project:
"AuthServer": {
    "Authority": "https://localhost:44358",
    "RequireHttpsMetadata": true,
    "MetaAddress": "https://localhost:44358",
    "SwaggerClientId": "AbpHangfireDemoApp_Swagger",
    "HangfireClientId": "AbpHangfireDemoApp_Hangfire",
    "HangfireClientSecret": "1q2w3e*"
}
  1. Add the ConfigureHangfire method to the API project to configure Hangfire:
public override void ConfigureServices(ServiceConfigurationContext context)
{
    var configuration = context.Services.GetConfiguration();
    var hostingEnvironment = context.Services.GetHostingEnvironment();

    //...

    //Add Hangfire
    ConfigureHangfire(context, configuration);
    //...
}

private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
{
    context.Services.AddHangfire(config =>
    {
        config.UseSqlServerStorage(configuration.GetConnectionString("Default"));
    });
}
  1. Modify the ConfigureAuthentication method to add new Cookies and OpenIdConnect authentication schemes:
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
    context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddAbpJwtBearer(options =>
        {
            options.Authority = configuration["AuthServer:Authority"];
            options.RequireHttpsMetadata = configuration.GetValue<bool>("AuthServer:RequireHttpsMetadata");
            options.Audience = "AbpHangfireDemoApp";

            options.ForwardDefaultSelector = httpContext => httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase)
                ? CookieAuthenticationDefaults.AuthenticationScheme
                : null;
        })
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddAbpOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            options.Authority = configuration["AuthServer:Authority"];
            options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
            options.ResponseType = OpenIdConnectResponseType.Code;

            options.ClientId = configuration["AuthServer:HangfireClientId"];
            options.ClientSecret = configuration["AuthServer:HangfireClientSecret"];

            options.UsePkce = true;
            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;

            options.Scope.Add("roles");
            options.Scope.Add("email");
            options.Scope.Add("phone");
            options.Scope.Add("AbpHangfireDemoApp");

            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        });

    //...
}
  1. Add a custom middleware and UseAbpHangfireDashboard after UseAuthorization in the OnApplicationInitialization method:
//...
app.UseAuthorization();

app.Use(async (httpContext, next) =>
{
    if (httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase))
    {
        var authenticateResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        if (!authenticateResult.Succeeded)
        {
            await httpContext.ChallengeAsync(
                OpenIdConnectDefaults.AuthenticationScheme,
                new AuthenticationProperties
                {
                    RedirectUri = httpContext.Request.Path + httpContext.Request.QueryString
                });
            return;
        }
    }
    await next.Invoke();
});
app.UseAbpHangfireDashboard("/hangfire", options =>
{
    options.AsyncAuthorization = new[]
    {
        new AbpHangfireAuthorizationFilter()
    };
});

//...

Perfect! 🎉 Now you can run the AuthServer and API projects and access the Hangfire Dashboard at https://localhost:44371/hangfire.

Note: Replace 44371 with your API project's port.

The first time you access the Hangfire Dashboard, you'll be redirected to the login page of the AuthServer project. After you log in, you'll be redirected back to the Hangfire Dashboard.

Hangfire Dashboard

Key Points 🔑

1. Authentication Scheme Selection

The default authentication scheme in API websites is JWT Bearer. We've implemented Cookies and OpenIdConnect specifically for the Hangfire Dashboard.

We've configured the JwtBearerOptions's ForwardDefaultSelector to use CookieAuthenticationDefaults.AuthenticationScheme for Hangfire Dashboard requests.

This means that if the request path starts with /hangfire, the request will be authenticated using the Cookies authentication scheme; otherwise, it will use the JwtBearer authentication scheme.

options.ForwardDefaultSelector = httpContext => httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase)
    ? CookieAuthenticationDefaults.AuthenticationScheme
    : null;

2. Custom Middleware for Authentication

We've also implemented a custom middleware to handle Cookies authentication for the Hangfire Dashboard. If the current request isn't authenticated with the Cookies authentication scheme, it will be redirected to the login page.

app.Use(async (httpContext, next) =>
{
    if (httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase))
    {
        var authenticateResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        if (!authenticateResult.Succeeded)
        {
            await httpContext.ChallengeAsync(
                OpenIdConnectDefaults.AuthenticationScheme,
                new AuthenticationProperties
                {
                    RedirectUri = httpContext.Request.Path + httpContext.Request.QueryString
                });
            return;
        }
    }
    await next.Invoke();
});

References 📚