question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Feature: Allow custom authorization

See original GitHub issue

Hiya,

Right now i keep having to add my token into the log view - this can be a bit tedious. Would it be possible to use authorization the same way hangfire does, by allowing custom Authorization filters?

See: https://docs.hangfire.io/en/latest/configuration/using-dashboard.html

The code looks simple enough from a first glance:

public interface IDashboardAsyncAuthorizationFilter
{
Task<bool> AuthorizeAsync([NotNull] DashboardContext context);
}

Hangfire.Dashboard.AspNetCoreDashboardMiddleware

foreach (IDashboardAuthorizationFilter authorizationFilter in this._options.Authorization)
{
  if (!authorizationFilter.Authorize((DashboardContext) context))
  {
    httpContext.Response.StatusCode = AspNetCoreDashboardMiddleware.GetUnauthorizedStatusCode(httpContext);
    context = (AspNetCoreDashboardContext) null;
    findResult = (Tuple<IDashboardDispatcher, Match>) null;
    return;
  }
}
foreach (IDashboardAsyncAuthorizationFilter authorizationFilter in this._options.AsyncAuthorization)
{
  if (!await authorizationFilter.AuthorizeAsync((DashboardContext) context))
  {
    httpContext.Response.StatusCode = AspNetCoreDashboardMiddleware.GetUnauthorizedStatusCode(httpContext);
    context = (AspNetCoreDashboardContext) null;
    findResult = (Tuple<IDashboardDispatcher, Match>) null;
    return;
  }
}

Startup:

endpoints.MapHangfireDashboard(new DashboardOptions
            {
                AsyncAuthorization = new[] {
                    new MyHangfireAuthenticationFilter(
                        new(
                            authApiUrl,
                            publicKeyUrl
                        ),
                        services.GetRequiredService<ILogger<MYHangfireAuthenticationFilter>>()
                    )
                }
            });

Custom filter:

public class MyHangfireAuthenticationFilter : IDashboardAsyncAuthorizationFilter
{
     ....
   public async Task<bool> AuthorizeAsync(DashboardContext context)
    {
        var httpContext = context.GetHttpContext();

        if (_options.ForceSsl && httpContext.Request.Scheme != "https")
        {

            var redirectUri = new UriBuilder("https", httpContext.Request.Host.ToString(), 443, httpContext.Request.Path).ToString();
            httpContext.Response.Redirect(redirectUri, true);

            return false;
        }

        // If cookie has a JWT token we don't have to send a request to the auth api, we can just validate the token
        if (httpContext.Request.Cookies.ContainsKey("token"))
        {
            try
            {
                _ = new JwtSecurityTokenHandler()
                    .ValidateToken(httpContext.Request.Cookies["token"],
                        new()
                        {
                            ClockSkew = TimeSpan.Zero,
                            ValidateAudience = false,
                            ValidateIssuer = false,
                            ValidateIssuerSigningKey = true,
                            ValidateLifetime = true,
                            IssuerSigningKey = await GetSecurityKey()
                        }, out _);

                return true;
            }
            catch(Exception ex)
            {
                // Validation failed
                _logger.LogError(ex, ex.Message);
            }
        }

        // No token or invalid token, check for basic auth headers to validate
        var authHeader = httpContext.Request.Headers["Authorization"];
        if (!string.IsNullOrWhiteSpace(authHeader))
        {
            var authValues = AuthenticationHeaderValue.Parse(authHeader);

            if (authValues.Scheme.Equals("Basic", StringComparison.InvariantCultureIgnoreCase))
            {
                if (string.IsNullOrWhiteSpace(authValues.Parameter))
                    throw new("string.IsNullOrWhiteSpace(authValues.Parameter)");

                var authString = Encoding.UTF8.GetString(Convert.FromBase64String(authValues.Parameter));

                var parts = authString.Split(':', 2);
                if (parts.Length == 2)
                {
                    var body = new
                    {
                        Email = parts[0],
                        Password = parts[1]
                    };
                    try
                    {
                        var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8,
                            "application/json");

                        // Some custom auth logic with an external server
                        var result = await HttpClient.PostAsync($"{_options.AuthUrl}/api/authorize/login", content);
                        if (result.IsSuccessStatusCode)
                        {
                            var response = await result.Content.ReadAsStringAsync();
                            var jsonResponse = JsonSerializer.Deserialize<JsonDocument>(response) ?? throw new NullReferenceException("JsonSerializer.Deserialize<JsonDocument>(response)");
                            var token = jsonResponse.RootElement.GetProperty("token").GetString() ?? throw new NullReferenceException("jsonResponse.RootElement.GetProperty(\"token\").GetString()");
                            httpContext.Response.Cookies.Append("token", token);


                            return true;
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, ex.Message);
                    }

                }
            }
        }

        await ShowChallenge(httpContext);

        return false;
    }
        
    private async Task ShowChallenge(HttpContext context)
    {
        context.Response.StatusCode = 401;
        context.Response.Headers.Append("WWW-Authenticate", "Basic realm=\"Hangfire Dashboard\"");

        await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("Authentication is required."));
    }

    private async ValueTask<SecurityKey> GetSecurityKey()
    {
        if (_key == null)
        {
            _key = ParsePublicKey(await HttpClient.GetStringAsync(_options.CertificateUrl));
        }

        return _key;
    }

    private RsaSecurityKey ParsePublicKey(string key)
    {
        var rsa = RSA.Create();
        var cert = new X509Certificate(Encoding.UTF8.GetBytes(key));

        rsa.ImportRSAPublicKey(cert.GetPublicKey(), out _);
        return new(rsa);
    }

}

public readonly struct HangfireAuthenticationOptions
{
    public string AuthUrl { get; }
    public string CertificateUrl { get; }
    public bool ForceSsl { get; }


    public HangfireAuthenticationOptions(string url, string certificateUrl, bool forceSsl = true)
    {
        AuthUrl = url;
        CertificateUrl = certificateUrl;
        ForceSsl = forceSsl;
    }

}

This way we’ll get the native browser login modal, which makes it easy to log in (especially since the browser can remember my account details)

image

I’d prefer:

  • An Async interface
  • DI so that i can inject an ILogger

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:1
  • Comments:7 (7 by maintainers)

github_iconTop GitHub Comments

1reaction
mo-esmpcommented, Jan 16, 2022

I followed the Swashbuckle Swagger approach for authentication, simple and easy to implement. I don’t think there is a problem with storing token in session storage, however, there is a PR for UI and not merged yet and after merging that, it can be implemented and you are very welcome to help.

0reactions
mo-esmpcommented, Aug 29, 2022

@sommmen, you’re welcome to add a description for this use case to the Readme file.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Custom Authorization Policy Providers in ASP.NET Core
Learn how to use a custom IAuthorizationPolicyProvider in an ASP.NET Core app to dynamically generate authorization policies.
Read more >
Authorization servers - Okta Developer
Okta allows you to create multiple custom authorization servers within a single Okta org that you can use to protect your own resource...
Read more >
Sample Use Cases: Rules with Authorization - Auth0
With rules, you can modify or complement the outcome of the decision made by the pre-configured authorization policy to handle more complicated cases...
Read more >
Use API Gateway Lambda authorizers - AWS Documentation
A Lambda authorizer (formerly known as a custom authorizer) is an API Gateway feature that uses a Lambda function to control access to...
Read more >
Authentication and Authorization - ServiceStack Docs
The Auth Feature also allows you to specify your own custom IUserAuthSession type where you can capture additional metadata with your users session...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found