diff --git a/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/POST.md b/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/POST.md new file mode 100644 index 0000000000..e49b81c17a --- /dev/null +++ b/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/POST.md @@ -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 //Hybrid flow + { + OpenIddictConstants.GrantTypes.AuthorizationCode, OpenIddictConstants.GrantTypes.Implicit + }, + scopes: commonScopes, + redirectUris: new List { $"{hangfireClientRootUrl}signin-oidc" }, + postLogoutRedirectUris: new List { $"{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 + + + +``` + +```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("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. + +![Hangfire Dashboard](gif.gif) + +## 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) \ No newline at end of file diff --git a/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/gif.gif b/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/gif.gif new file mode 100644 index 0000000000..e4fa879eb4 Binary files /dev/null and b/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/gif.gif differ diff --git a/docs/en/framework/infrastructure/background-jobs/hangfire.md b/docs/en/framework/infrastructure/background-jobs/hangfire.md index f5991ee19b..da0e3953ab 100644 --- a/docs/en/framework/infrastructure/background-jobs/hangfire.md +++ b/docs/en/framework/infrastructure/background-jobs/hangfire.md @@ -190,18 +190,20 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi options.Authority = configuration["AuthServer:Authority"]; options.RequireHttpsMetadata = configuration.GetValue("AuthServer:RequireHttpsMetadata"); options.Audience = "MyProjectName"; - }); - context.Services.AddAuthentication() - .AddCookie("Cookies") - .AddOpenIdConnect("oidc", options => + 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 = configuration.GetValue("AuthServer:RequireHttpsMetadata"); - options.ResponseType = OpenIdConnectResponseType.CodeIdToken; + options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); + options.ResponseType = OpenIdConnectResponseType.Code; - options.ClientId = configuration["AuthServer:ClientId"]; - options.ClientSecret = configuration["AuthServer:ClientSecret"]; + options.ClientId = configuration["AuthServer:HangfireClientId"]; + options.ClientSecret = configuration["AuthServer:HangfireClientSecret"]; options.UsePkce = true; options.SaveTokens = true; @@ -211,6 +213,8 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi options.Scope.Add("email"); options.Scope.Add("phone"); options.Scope.Add("MyProjectName"); + + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; }); } ``` @@ -218,26 +222,27 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi ```csharp app.Use(async (httpContext, next) => { - if (httpContext.Request.Path.StartsWithSegments("/hangfire")) + if (httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase)) { - var result = await httpContext.AuthenticateAsync("Cookies"); - if (result.Succeeded) + var authenticateResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); + if (!authenticateResult.Succeeded) { - httpContext.User = result.Principal; - await next(httpContext); + await httpContext.ChallengeAsync( + OpenIdConnectDefaults.AuthenticationScheme, + new AuthenticationProperties + { + RedirectUri = httpContext.Request.Path + httpContext.Request.QueryString + }); return; } - - await httpContext.ChallengeAsync("oidc"); - } - else - { - await next(httpContext); } + await next.Invoke(); }); - app.UseAbpHangfireDashboard("/hangfire", options => { - options.AsyncAuthorization = new[] {new AbpHangfireAuthorizationFilter()}; + options.AsyncAuthorization = new[] + { + new AbpHangfireAuthorizationFilter() + }; }); ``` diff --git a/docs/en/studio/images/solution-runner/csharp-application-context-menu-build.png b/docs/en/studio/images/solution-runner/csharp-application-context-menu-build.png index 7f20236e84..f046bfc5a6 100644 Binary files a/docs/en/studio/images/solution-runner/csharp-application-context-menu-build.png and b/docs/en/studio/images/solution-runner/csharp-application-context-menu-build.png differ diff --git a/docs/en/studio/images/solution-runner/csharp-application-context-menu-monitor.png b/docs/en/studio/images/solution-runner/csharp-application-context-menu-monitor.png index fc6692c866..4517bd9188 100644 Binary files a/docs/en/studio/images/solution-runner/csharp-application-context-menu-monitor.png and b/docs/en/studio/images/solution-runner/csharp-application-context-menu-monitor.png differ diff --git a/docs/en/studio/images/solution-runner/csharp-application-context-menu.png b/docs/en/studio/images/solution-runner/csharp-application-context-menu.png index 885512b940..08b79c0787 100644 Binary files a/docs/en/studio/images/solution-runner/csharp-application-context-menu.png and b/docs/en/studio/images/solution-runner/csharp-application-context-menu.png differ diff --git a/docs/en/studio/images/solution-runner/docker-container-example-add-dialog.png b/docs/en/studio/images/solution-runner/docker-container-example-add-dialog.png index 3320b70d03..020e5261be 100644 Binary files a/docs/en/studio/images/solution-runner/docker-container-example-add-dialog.png and b/docs/en/studio/images/solution-runner/docker-container-example-add-dialog.png differ diff --git a/docs/en/studio/images/solution-runner/docker-container-example-add.png b/docs/en/studio/images/solution-runner/docker-container-example-add.png index d9afede07c..ba405f3982 100644 Binary files a/docs/en/studio/images/solution-runner/docker-container-example-add.png and b/docs/en/studio/images/solution-runner/docker-container-example-add.png differ diff --git a/docs/en/studio/images/solution-runner/docker-container-example-rabbitmq.png b/docs/en/studio/images/solution-runner/docker-container-example-rabbitmq.png index 71a362e5f1..d4aa7791a7 100644 Binary files a/docs/en/studio/images/solution-runner/docker-container-example-rabbitmq.png and b/docs/en/studio/images/solution-runner/docker-container-example-rabbitmq.png differ diff --git a/docs/en/studio/images/solution-runner/docker-container-properties.png b/docs/en/studio/images/solution-runner/docker-container-properties.png index 75459db02b..6f34fe660e 100644 Binary files a/docs/en/studio/images/solution-runner/docker-container-properties.png and b/docs/en/studio/images/solution-runner/docker-container-properties.png differ diff --git a/docs/en/studio/images/solution-runner/docker-container-stack.png b/docs/en/studio/images/solution-runner/docker-container-stack.png index e9c079b5f3..f5e40fafce 100644 Binary files a/docs/en/studio/images/solution-runner/docker-container-stack.png and b/docs/en/studio/images/solution-runner/docker-container-stack.png differ diff --git a/docs/en/studio/images/solution-runner/docker-container-warning.png b/docs/en/studio/images/solution-runner/docker-container-warning.png index 04eae58b86..abb00242e2 100644 Binary files a/docs/en/studio/images/solution-runner/docker-container-warning.png and b/docs/en/studio/images/solution-runner/docker-container-warning.png differ diff --git a/docs/en/studio/images/solution-runner/folder-context-menu.png b/docs/en/studio/images/solution-runner/folder-context-menu.png index 44677da97d..47f56995f0 100644 Binary files a/docs/en/studio/images/solution-runner/folder-context-menu.png and b/docs/en/studio/images/solution-runner/folder-context-menu.png differ diff --git a/docs/en/studio/images/solution-runner/profile-root-context-menu.png b/docs/en/studio/images/solution-runner/profile-root-context-menu.png index 5615789a0f..355869511c 100644 Binary files a/docs/en/studio/images/solution-runner/profile-root-context-menu.png and b/docs/en/studio/images/solution-runner/profile-root-context-menu.png differ diff --git a/docs/en/studio/images/solution-runner/solution-runner.png b/docs/en/studio/images/solution-runner/solution-runner.png index 53a1a11e75..5a8b123d6d 100644 Binary files a/docs/en/studio/images/solution-runner/solution-runner.png and b/docs/en/studio/images/solution-runner/solution-runner.png differ diff --git a/docs/en/tutorials/microservice/images/abp-studio-solution-runner-play-all.png b/docs/en/tutorials/microservice/images/abp-studio-solution-runner-play-all.png index 96f8f614c8..4372a48c70 100644 Binary files a/docs/en/tutorials/microservice/images/abp-studio-solution-runner-play-all.png and b/docs/en/tutorials/microservice/images/abp-studio-solution-runner-play-all.png differ