2

My application is an Angular 12 application running on ASP.Net Core 5.

I am currently trying to lock down Hangfire so that it will only work for people with the Admin role.

It uses Microsoft Identity to log in - specifically Single Sign-on, set up in Azure.

public void ConfigureServices(IServiceCollection services)
{
...
   services.AddHangfire(x =>
   {
      x.UseSqlServerStorage(sqlServerConnectionString);
   });
...
   services
      .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
      .AddMicrosoftIdentityWebApi(Configuration);
...
}


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
   app.UseAuthentication();
   app.UseRouting();
   app.UseAuthorization();

   app.UseHangfireDashboard("/hangfire", new DashboardOptions
   {
       Authorization = new[] {
          new HangfireAuthorisationFilter()
          },
          AppPath = "/"
   });

...
   app.UseEndpoints(endpoints => {
   ...
   });

   app.UseSpa(spa=>{
      ...
   });

}

This works in my dot net core controllers.

All I need to do to get it to work is add the Authorize attribute:

namespace MyAppName.Controllers
{
    [Produces("application/json")]
    [Route("api/MyRoute")]
    [Authorize(Roles="Role1,Role2,Administrator")]
    public class MyControllerController: MyBaseApiController
    {
...
    }
}

But when I want to Authorise in Hangfire, the User object is missing a whole lot of its properties.

Here is the HangfireAuthorisationFilter:

public class HangfireAuthorisationFilter : IDashboardAuthorizationFilter
{

   public HangfireAuthorisationFilter()
   {
   }

   public bool Authorize(DashboardContext context)
   {
      var httpContext = context.GetHttpContext();
      // the next line always fails. The User object is set. The Identity object is set
      // but there are no claims and the User.Name is null. There are also no roles set.
      return httpContext.User.Identity.IsAuthenticated;
   }
}

There is, however, cookie information, containing the msal cookie: httpContext.Request.Cookies

How can I pass authentication information into the Hangfire Authorize method? How can I access the role information so that I can lock it down to just the Admin role? Is there a way I can decode the msal cookie server-side?

tone
  • 1,374
  • 20
  • 47

2 Answers2

4

Assuming you have an AzureAd configuration block that looks like below:

"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]",
    "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]",
    "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]"
}

I think a better approach to avoid manual validation of the token is to change your code to the following:

public void ConfigureServices(IServiceCollection services)
{

   services.AddHangfire(x =>
   {
      x.UseSqlServerStorage(sqlServerConnectionString);
   });

   services
      .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
      .AddMicrosoftIdentityWebApi(Configuration);

   services.
       .AddAuthentication(AzureADDefaults.AuthenticationScheme)
       .AddAzureAD(options => Configuration.Bind("AzureAd", options));

   services.AddAuthorization(options =>
        {
            options.AddPolicy("Hangfire", builder =>
            {
                builder
                    .AddAuthenticationSchemes(AzureADDefaults.AuthenticationScheme)
                    .RequireRole("Admin")
                    .RequireAuthenticatedUser();
            });
        });
}


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   app.UseAuthentication();
   app.UseRouting();
   app.UseAuthorization();

   app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapHangfireDashboard("/hangfire", new DashboardOptions()
            {
                Authorization = Enumerable.Empty<IDashboardAuthorizationFilter>()
            })
            .RequireAuthorization("Hangfire");
        });
 }

To break this down, the following changes have been made:

  • Add authentication for AzureADDefaults.AuthenticationScheme so we can create a policy requiring the "Admin" role.
  • Add a policy named "Hangfire" that requires the "Admin" role against a user. See the AddAuthorization call.
  • Instead of calling UseHangfireDashboard we call MapHangfireDashboard inside UseEndpoints and protect the hangfire dashboard endpoint using our "Hangfire" policy through the call to RequireAuthorization("Hangfire")
  • Removal off the HangfireAuthorisationFilter which is not needed and instead we pass an empty collection of filters in the MapHangfireDashboard call.

The key takeaway is that we are now relying on the security provided by the middleware rather than the implementation of IDashboardAuthorizationFilter which comes with huge risk around the token being invalid and/or a mistake is made in the logic.

Kusterbeck
  • 108
  • 8
0

Ok I have figured out how to decode the msal cookie to get my list of claims and roles, and authorise successfully with Hangfire

using Hangfire.Dashboard;
using System.IdentityModel.Tokens.Jwt;


namespace MyApp.Filters
{
    public class HangfireAuthorisationFilter : IDashboardAuthorizationFilter
    {

        public HangfireAuthorisationFilter()
        {
        }

        public bool Authorize(DashboardContext context)
        {

            var httpContext = context.GetHttpContext();
            var cookies = httpContext.Request.Cookies;
            var msalIdToken = cookies["msal.{your app client id goes here}.idtoken"];
            var token = new JwtSecurityTokenHandler().ReadJwtToken(msalIdToken);
            foreach(var claim in token.Claims)
            {
                if (claim.Type=="roles" && claim.Value == "Admin")
                {
                    return true;
                }
            }
            return false;
        }

    }
}
tone
  • 1,374
  • 20
  • 47
  • I don't think this a good approach. Following your logic, you would have to validate the token (the token may not be valid here) Have you tried the ValidateAppRoles HttpContext extension method instead ? https://learn.microsoft.com/en-us/dotnet/api/microsoft.identity.web.resource.rolesrequiredhttpcontextextensions.validateapprole?view=azure-dotnet-preview – jbl Sep 08 '21 at 09:17
  • I doubt that would work. The roles aren't set inside the http context. They are only set inside the cookie. If there was a way to get it to populate the http context object from the cookie, then that might work. But I don't know a way to do that. – tone Sep 08 '21 at 10:28
  • maybe you should give it a try. The method does not seem to check the User claims as you do with Identity https://github.com/AzureAD/microsoft-identity-web/blob/f6c31104c5e5432228ff1a987206766c713ad6a8/src/Microsoft.Identity.Web/Resource/RolesRequiredHttpContextExtensions.cs#L46 – jbl Sep 08 '21 at 10:47
  • It errors out with System.UnauthorizedAccessException: 'IDW10204: The user is unauthenticated. The HttpContext does not contain any claims.' – tone Sep 08 '21 at 11:17
  • I do believe that I need to Validate the Token. There is a method called ValidateToken on the JwtSecurityTokenHandler, but I have still been unable to get it to validate, even setting the Public key found at https://login.microsoftonline.com/{my tenent id}/discovery/v2.0/keys?clientid={my client id} – tone Sep 09 '21 at 05:43