@ -0,0 +1,250 @@ |
|||
# Using Hangfire Dashboard in ABP API Website 🚀 |
|||
|
|||
## 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. |
|||
|
|||
```bash |
|||
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 `AuthServer` project. |
|||
|
|||
> **Note:** Replace `44371` with your `API` project's port. |
|||
|
|||
```json |
|||
"OpenIddict": { |
|||
"Applications": { |
|||
//... |
|||
"AbpHangfireDemoApp_Hangfire": { |
|||
"ClientId": "AbpHangfireDemoApp_Hangfire", |
|||
"RootUrl": "https://localhost:44371/" |
|||
} |
|||
//... |
|||
} |
|||
} |
|||
``` |
|||
|
|||
2. Update the `OpenIddictDataSeedContributor`'s `CreateApplicationsAsync` method in the `Domain` project to seed the new Hangfire application. |
|||
|
|||
```csharp |
|||
//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" |
|||
); |
|||
} |
|||
``` |
|||
|
|||
3. 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: |
|||
|
|||
```bash |
|||
<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" /> |
|||
``` |
|||
|
|||
```cs |
|||
typeof(AbpBackgroundJobsHangfireModule), |
|||
typeof(AbpAspNetCoreAuthenticationOpenIdConnectModule) |
|||
``` |
|||
|
|||
2. Add the `HangfireClientId` and `HangfireClientSecret` to the `appsettings.json` file in the `API` project: |
|||
|
|||
```csharp |
|||
"AuthServer": { |
|||
"Authority": "https://localhost:44358", |
|||
"RequireHttpsMetadata": true, |
|||
"MetaAddress": "https://localhost:44358", |
|||
"SwaggerClientId": "AbpHangfireDemoApp_Swagger", |
|||
"HangfireClientId": "AbpHangfireDemoApp_Hangfire", |
|||
"HangfireClientSecret": "1q2w3e*" |
|||
} |
|||
``` |
|||
|
|||
3. Add the `ConfigureHangfire` method to the `API` project to configure Hangfire: |
|||
|
|||
```csharp |
|||
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")); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
4. Modify the `ConfigureAuthentication` method to add new `Cookies` and `OpenIdConnect` authentication schemes: |
|||
|
|||
```csharp |
|||
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; |
|||
}); |
|||
|
|||
//... |
|||
} |
|||
``` |
|||
|
|||
5. Add a custom middleware and `UseAbpHangfireDashboard` after `UseAuthorization` in the `OnApplicationInitialization` method: |
|||
|
|||
```csharp |
|||
//... |
|||
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. |
|||
|
|||
 |
|||
|
|||
## 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. |
|||
|
|||
```csharp |
|||
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. |
|||
|
|||
```csharp |
|||
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 📚 |
|||
|
|||
- [ABP Hangfire Background Job Manager](https://abp.io/docs/latest/framework/infrastructure/background-jobs/hangfire) |
|||
- [Use cookie authentication in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-9.0) |
|||
|
After Width: | Height: | Size: 4.9 MiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 33 KiB |