Hangfire: Upgrade to ASP.NET Core 2.0 authentication not working for Hangfire.AspNetCore Dashboard

Created on 3 Nov 2017  路  15Comments  路  Source: HangfireIO/Hangfire

Since upgrading to core 2.0, authentication has stopped working for the hangfire dashboard.

The implementation of the Authorize method, now may return false, despite being logged in. This means that calls to eg. /hangfire no longer works in some instances.

    public class HangfireDashboardAuthorizationFilter : IDashboardAuthorizationFilter
    {
        public bool Authorize(DashboardContext context)
        {
            var httpContext = context.GetHttpContext();
            return httpContext.User.Identity.IsAuthenticated;
        }
    }

This is due to changes introduced with asp.net core 2.0 authentication and supporting multiple authentication schemes. Discussions with the author of the excellent Basic Authentication package Odachi.AspNetCore.Authentication.Basic proposed a solution, found here: https://github.com/Kukkimonsuta/Odachi/issues/11 . This highlights the problem issue and solution very clearly.

As a workaround, I have implemented our own UseHangfireDashboard method. Below is the code for anyone else struggling in this regard, until a fix is in place.

       private IApplicationBuilder UseHangfireDashboard(IApplicationBuilder app, string pathMatch = "/hangfire", DashboardOptions options = null, JobStorage storage = null)
        {
            var services = app.ApplicationServices;
            storage = storage ?? services.GetRequiredService<JobStorage>();
            options = options ?? services.GetService<DashboardOptions>() ?? new DashboardOptions();
            var routes = app.ApplicationServices.GetRequiredService<RouteCollection>();

            // Use our custom middleware.
            app.Map(new PathString(pathMatch), x => x.UseMiddleware<HangfireDashboardMiddleware>(storage, options, routes));
            return app;
        }

The middleware created to fix this issue is below. Feel free to use this, although some of the null and config checks are missing from your original source.

This would replace the code here: https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.AspNetCore/Dashboard/AspNetCoreDashboardMiddleware.cs

    public class HangfireDashboardMiddleware
    {
        private readonly RequestDelegate nextRequestDelegate;
        private readonly JobStorage jobStorage;
        private readonly DashboardOptions dashboardOptions;
        private readonly RouteCollection routeCollection;

        public HangfireDashboardMiddleware(RequestDelegate nextRequestDelegate, JobStorage storage, DashboardOptions options, RouteCollection routes)
        {
            this.nextRequestDelegate = nextRequestDelegate;
            this.jobStorage = storage;
            this.dashboardOptions = options;
            this.routeCollection = routes;
        }

        public async Task Invoke(HttpContext httpContext)
        {
            var aspNetCoreDashboardContext = new AspNetCoreDashboardContext(this.jobStorage, this.dashboardOptions, httpContext);
            var findResult = this.routeCollection.FindDispatcher(httpContext.Request.Path.Value);
            if (findResult == null)
            {
                await this.nextRequestDelegate.Invoke(httpContext);
                return;
            }

            // attempt to authenticate against default auth scheme (this will attempt to authenticate using data in request, but doesn't send challenge)
            var result = await httpContext.AuthenticateAsync();
            if (httpContext.User.Identity.IsAuthenticated == false)
            {
                // request was not authenticated, send challenge and do not continue processing this request
                await httpContext.ChallengeAsync();
            }

            foreach (var filter in this.dashboardOptions.Authorization)
            {
                if (filter.Authorize(aspNetCoreDashboardContext) == false)
                {
                    var isAuthenticated = httpContext.User?.Identity?.IsAuthenticated;
                    httpContext.Response.StatusCode = isAuthenticated == true ? (int)HttpStatusCode.Forbidden : (int)HttpStatusCode.Unauthorized;
                    return;
                }
            }

            aspNetCoreDashboardContext.UriMatch = findResult.Item2;
            await findResult.Item1.Dispatch(aspNetCoreDashboardContext);
        }
    }

Hope that helps and any questions, please feel free to ask.

help wanted

Most helpful comment

Got the same problem

my problem was specifying app.UseAuthentication(); after the useHangfire

All 15 comments

Be nice if the dependency on Owin could be removed from the Hangfire.Core project as well. If I recall correctly, when using ASP.NET Core, Owin is not used, regardless if it's .NET Core or the full .NET Framework.

I don't actually get why you call httpContext.AuthenticateAsync() here, because it is the job for AuthenticationMiddleware to do.

Does someone else have such a problem on ASP.NET Core 2.0? Authorization filters should rely on the available information and general and shouldn't authenticate anything. They are authorization filters, not authentication ones.

@osbornesoftware, have you tried to play a bit with the order of your middleware, i.e. placing your authentication middleware before the dashboard's one?

@odinserj Yes ordering all tried. In fact I spent a significant amount of time looking into this before raising the issue, initially with the auth library guys.
Feel free to look at https://github.com/Kukkimonsuta/Odachi/issues/11
There is a Zip with some sample code to demonstrate the issue.
The AuthCheckMiddleware can be replaced for the hangfire code. The results are the same.
If I am doing something idiotic please let me know.

This is probably only relevant for challenge-based authentication and doesn't affect other types (e.g. cookie-based authentication).

Also, it would be better to do something like this:
```c#
public class HangfireDashboardMiddleware
{
private readonly RequestDelegate _next;
private readonly JobStorage _storage;
private readonly DashboardOptions _options;
private readonly RouteCollection _routes;

public HangfireDashboardMiddleware(RequestDelegate next, JobStorage storage, DashboardOptions options, RouteCollection routes)
{
    _next = next;
    _storage = storage;
    _options = options;
    _routes = routes;
}

public async Task Invoke(HttpContext httpContext)
{
    var findResult = _routes.FindDispatcher(httpContext.Request.Path.Value);
    if (findResult == null)
    {
        await _next.Invoke(httpContext);
        return;
    }

    // attempt to authenticate against default auth scheme (this will attempt to authenticate using data in request, but doesn't send challenge)
    var result = await httpContext.AuthenticateAsync();
    if (!result.Succeeded)
    {
        // request was not authenticated, send challenge and do not continue processing this request
        await httpContext.ChallengeAsync();
        return;
    }

    var aspNetCoreDashboardContext = new AspNetCoreDashboardContext(_storage, _options, httpContext);

    foreach (var filter in _options.Authorization)
    {
        if (filter.Authorize(aspNetCoreDashboardContext) == false)
        {
            var isAuthenticated = httpContext.User?.Identity?.IsAuthenticated;
            httpContext.Response.StatusCode = isAuthenticated == true ? (int)HttpStatusCode.Forbidden : (int)HttpStatusCode.Unauthorized;
            return;
        }
    }

    aspNetCoreDashboardContext.UriMatch = findResult.Item2;
    await findResult.Item1.Dispatch(aspNetCoreDashboardContext);
}

}
```

I just upgraded our project to 2.0 and I'm having the same issue. We use WindowsAuthentication.. so services.AddAuthentication(IISDefaults.AuthenticationScheme); is added before hangfire. I get a 403 when navigating to the Dashboard

So.. false alarm (sorry). During the upgrade we commented a lot of not compiling code and something was left behind. On net core 1 we had this before:

app.UseClaimsTransformation(new ClaimsTransformationOptions
{
    Transformer = new CustomAdGroupClaimTransformer()
});

So this added especial claims.. and we used the same claims to protect the Hangfire Dashboard when configuring the IDashboardAuthorizationFilter.
So.. now just needed to add the ClaimTransformation as a service:

services.AddTransient<IClaimsTransformation, CustomAdGroupClaimTransformer>();
All works :)

I'm having this issue as well. HttpContext.User.Identity is an empty ClaimsPrincipal in the auth filter. It does come after app.AddAuthentication(). I guess for the time being I'll add my own custom middleware to check if the route is /hangfire and do the authentication myself.

I'm having this issue as well. (DashboardContext)context.GetHttpContext().User.IsAuthenticated returns false. The same worked in .net core 1.1

I'm having this issue as well. (DashboardContext)context.GetHttpContext().User.IsAuthenticated returns false. The same worked in .net core 1.1

Did you ever get to the bottom of this? I'm having the same issue.

Got the same problem

my problem was specifying app.UseAuthentication(); after the useHangfire

For anyone still struggling with User.IsAuthenticated returning false even though you are logged in, this is how I fixed it.

The key was getting the IsAuthenticated property from the AuthenticateResult instead of directly out HttpContext.User.IsAuthenticated.

```C#
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.Dashboard;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;

namespace Hangfire
{
public class CustomHangfireDashboardMiddleware
{
private readonly RequestDelegate _nextRequestDelegate;
private readonly JobStorage _jobStorage;
private readonly DashboardOptions _dashboardOptions;
private readonly RouteCollection _routeCollection;

    public CustomHangfireDashboardMiddleware(RequestDelegate nextRequestDelegate,
                                             JobStorage storage,
                                             DashboardOptions options,
                                             RouteCollection routes)
    {
        _nextRequestDelegate = nextRequestDelegate;
        _jobStorage = storage;
        _dashboardOptions = options;
        _routeCollection = routes;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        var aspNetCoreDashboardContext = new AspNetCoreDashboardContext(_jobStorage, _dashboardOptions, httpContext);
        var findResult = _routeCollection.FindDispatcher(httpContext.Request.Path.Value);
        if (findResult == null)
        {
            await _nextRequestDelegate.Invoke(httpContext);
            return;
        }

        // Attempt to authenticate against Cookies scheme.
        // This will attempt to authenticate using data in request, but doesn't send challenge.
        var result = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        if (!result.Succeeded)
        {
            // Request was not authenticated, send challenge and do not continue processing this request.
            await httpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme);
            return;
        }

        if (_dashboardOptions.Authorization.Any(filter => filter.Authorize(aspNetCoreDashboardContext) == false))
        {
            var isAuthenticated = result.Principal?.Identity?.IsAuthenticated ?? false;
            if (isAuthenticated == false)
            {
                httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
            }
            else
            {
                httpContext.Response.StatusCode = (int) HttpStatusCode.Forbidden;
            }

            return;
        }

        aspNetCoreDashboardContext.UriMatch = findResult.Item2;
        await findResult.Item1.Dispatch(aspNetCoreDashboardContext);
    }
}

}

```

Thank you so much, works great!

Was this ever resolved in Hangfire itself? I am using 1.7.11 with ASP.Net Core 2.2 on full .NET 4.7.2 and in the custom auth filter, there seems to be no way to get the username from httpcontext so I can do AD group membership checks (using Win authentication under IIS).

Problem seems to be still there

Was this page helpful?
0 / 5 - 0 ratings