I've downloaded the nu-get package Hangfire.Dashboard.Authorization
I'm trying configure the OWIN based authorization as per the docs as follows but I get intellisense error DashboardOptions.AuthorizationFilters is obsolete please use Authorization property instead
I also get intellisense error
The type or namespace AuthorizationFilter and ClaimsBasedAuthorizationFilterd not be found
using Hangfire.Dashboard;
using Hangfire.SqlServer;
using Owin;
using System;
namespace MyApp
{
public class Hangfire
{
public static void ConfigureHangfire(IAppBuilder app)
{
GlobalConfiguration.Configuration
.UseSqlServerStorage(
"ApplicationDbContext",
new SqlServerStorageOptions
{ QueuePollInterval = TimeSpan.FromSeconds(1) });
var options = new DashboardOptions
{
AuthorizationFilters = new[]
{
new AuthorizationFilter { Users = "admin, superuser", Roles = "advanced" },
new ClaimsBasedAuthorizationFilter("name", "value")
}
};
app.UseHangfireDashboard("/hangfire", options);
app.UseHangfireServer();
}
}
}
* UPDATE *
Since the above nuget package doesnt work I've attempted to create my own custom filter:
public class HangfireAuthorizationFilter : IAuthorizationFilter
{
public bool Authorize(IDictionary<string, object> owinEnvironment)
{
// In case you need an OWIN context, use the next line,
// `OwinContext` class is the part of the `Microsoft.Owin` package.
var context = new OwinContext(owinEnvironment);
// Allow all authenticated users to see the Dashboard (potentially dangerous).
return context.Authentication.User.Identity.IsAuthenticated;
}
}
How do I restrict to only Admin roles i.e what is the syntax?
You need to make sure the Configure(app) method is called in your Startup.cs class before configuring your hangfire dashboard.
public partial class Startup
{
private static readonly ILog log =
LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod
().DeclaringType);
public void Configuration(IAppBuilder app)
{
//Hangfire Config
GlobalConfiguration.Configuration.UseSqlServerStorage
("HangFireJobs");
app.UseHangfireServer();
log.Debug("Application Started");
ConfigureAuth(app);
//this call placement is important
var options = new DashboardOptions
{
Authorization = new[] { new CustomAuthorizationFilter() }
};
app.UseHangfireDashboard("/hangfire", options);
}
}
Then in your auth config class you can do something as simple as this :
public class CustomAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
if (HttpContext.Current.User.IsInRole("Admin"))
{
return true;
}
return false;
}
}
Defining the dashboard options in this way worked for me -
var options = new DashboardOptions
{
AuthorizationFilters = new List<IAuthorizationFilter>
{
new Hangfire.Dashboard.AuthorizationFilter { Users = "admin, superuser", Roles = "advanced" },
new Hangfire.Dashboard.ClaimsBasedAuthorizationFilter("name", "value")
}
};
I have imported the following namespaces -
using System;
using Owin;
using Hangfire;
using Hangfire.Dashboard;
using System.Collections.Generic;
using Hangfire.SqlServer;
Yes it is showing me the deprecated warning for AuthorizationFilters and suggest to use Authorization, basically the IAuthorizationFilter interface is going to removed in version 2.0, and IDashboardAuthorizationFilter interface has to be used.
For this you can create your own custom filter implementing IDashboardAuthorizationFilter and use this instead.
public class MyAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
//Implement
//Netcore example
return dashboardContext.GetHttpContext().User.Identity.IsAuthenticated;
}
}
Related
I have an Azure SQL DB that initially had the following columns:
user name
password hash
password salt
This DB serves a .NET Core C# API that checks username and password to return a JWT token.
The API had a User object that comprised all three columns with the correct types, a DbContext with a DbSet<User>, and an IServiceCollection that used said DbContext.
The API worked fine, returning a JWT token as needed.
I have since needed to add an extra parameter to check and pass to the JWT creation - the relevant column has been created in the DB, the User object in the API has been updated to include the extra parameter and that extra parameter is observed in the Intellisense throughout the API code.
The issue is that when the API is deployed to Azure, the extra parameter isn't being recognised and populated; how do I make the API correctly update to use the new DbContext and retrieve the User with the extra parameter?
(I've omitted the interfaces for brevity, as they're essentially the corresponding classes)
User, UserRequest and MyApiDbContext Classes:
using Microsoft.EntityFrameworkCore;
namespace MyApi.Models
{
// Basic user model used for authentication
public class User
{
public string UserId { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
public string ExtraParam { get; set; } // newly added parameter
}
public class UserRequest
{
public string UserId { get; set; }
public string password { get; set; }
}
public class MyApiDbContext : DbContext
{
public MyApiDbContext(DbContextOptions<MyApiDbContext> options)
: base(options)
{
}
public DbSet<User> Users { get; set; }
}
}
The AuthRepository that retrieves the user:
using Microsoft.EntityFrameworkCore;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;
namespace MyApi.Services
{
public class AuthRepository : IAuthRepository
{
private readonly MyApiDbContext _context;
public AuthRepository(MyApiDbContext context)
{
_context = context;
}
public async Task<User> Login(string username, string password)
{
// my test user gets returned
User returnedUser = await _context.Users.FirstOrDefaultAsync(x => x.UserId == username);
if (returnedUser == null)
{
return null;
}
// the password get verified
if (!VerifyPasswordHash(password, returnedUser.PasswordHash, returnedUser.PasswordSalt))
{
return null;
}
// this does not get changed, but the value set in the DB is definitely a string
if (returnedUser.ExtraParam == null || returnedUser.ExtraParam == "")
{
returnedUser.ExtraParam = "placeholder"
}
return returnedUser;
}
}
}
The AuthService that calls the AuthRepository for the user then "creates the JWT token" (just returning a string for this example), currently set up to return the user details:
using Microsoft.Extensions.Options;
using MyApi.Interfaces;
using MyApi.Models;
using System;
using System.Threading.Tasks;
namespace MyApi.Services
{
public class AuthService : IAuthService
{
private readonly IOptions<MyApiBlobStorageOptions> _settings;
private readonly IAuthRepository _repository;
public AuthService(IOptions<MyApiBlobStorageOptions> settings, IAuthRepository repository)
{
_repository = repository;
_settings = settings;
}
public async Task<string> Login(string username, string password)
{
User returnedUser = await _repository.Login(username, password);
if (returnedUser != null)
{
// currently returns "UserIdInDB,ProvidedPasswordFromLogin,"
return $"{returnedUser.UserId},{password},{returnedUser.ExtraParam}";
}
return null;
}
}
}
The controller that calls the AuthService:
using Microsoft.AspNetCore.Mvc;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace MyApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly MyApiDbContext _context;
private readonly IAuthService _authService;
public AuthController(MyApiDbContext context, IAuthService authService)
{
_context = context;
_authService = authService;
}
[HttpPost("login")]
public async Task<IActionResult> Login(UserRequest loginUser)
{
string token = await _authService.Login(loginUser.UserId, loginUser.Password);
if (token != null)
{
return Ok(token);
}
return Unauthorized("Access Denied!!");
}
}
}
The startup class that registers everything:
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using MyApi.Interfaces;
using MyApi.Models;
using MyApi.Services;
using Microsoft.Extensions.Azure;
using Azure.Storage.Queues;
using Azure.Storage.Blobs;
using Azure.Core.Extensions;
using System;
namespace MyApi
{
public class Startup
{
public IConfiguration Configuration { get; }
private readonly ILogger<Startup> _logger;
private readonly IConfiguration _config;
public Startup(ILogger<Startup> logger, IConfiguration config)
{
_logger = logger;
_config = config;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add dBContext for DB
services.AddDbContextPool<MyApiDbContext>(options => options.UseSqlServer(_config.GetConnectionString("MyAzureDb")));
// Add DI Reference for Repository
services.AddScoped<IAuthRepository, AuthRepository>();
// Add DI Reference for Azure Blob Storage Processes
services.AddScoped<IBlobService, AzureBlobService>();
// DI Reference for AuthService
services.AddScoped<IAuthService, AuthService>();
// Add configuration section for Constructor Injection
services.Configure<ApiBlobStorageOptions>(_config.GetSection("MyApiBlobStorage"));
services.AddMvc(mvcOptions => mvcOptions.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Latest);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII
.GetBytes(_config.GetSection("MyApiBlobStorage:Secret").Value)),
ValidateIssuer = false,
ValidateAudience = false
};
options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = context =>
{
_logger.LogWarning("Token authentication failed whilst attempting to upload file");
return Task.CompletedTask;
}
};
});
services.AddAzureClients(builder =>
{
builder.AddBlobServiceClient(Configuration["ConnectionStrings:MyApiBlobStorage/AzureBlobStorageConnectionString:blob"], preferMsi: true);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCors(x => x.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseAuthentication();
app.UseMvc();
}
}
internal static class StartupExtensions
{
public static IAzureClientBuilder<BlobServiceClient, BlobClientOptions> AddBlobServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
{
if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
{
return builder.AddBlobServiceClient(serviceUri);
}
else
{
return builder.AddBlobServiceClient(serviceUriOrConnectionString);
}
}
public static IAzureClientBuilder<QueueServiceClient, QueueClientOptions> AddQueueServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
{
if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
{
return builder.AddQueueServiceClient(serviceUri);
}
else
{
return builder.AddQueueServiceClient(serviceUriOrConnectionString);
}
}
}
}
Let me know if there is anything else required for understanding: the only difference between before and now is the addition of ExtraParam and the corresponding references throughout for the API, and the DB getting the identically named column.
I tried adding the parameter and deploying it to Azure and making the POST request as normal, starting and stopping the app service, deploying the API while the app service was stopped and starting it again, and restarting the app service. I don't know how much I could try changing up what I'm doing, I'm trying to do exactly the same as before, but with an extra parameter getting requested from the DB.
I can also confirm that the DB contains the ExtraParam column, and that it contains values against the existing data rows, as viewed using the Azure Portal's DB Query Editor.
I've resolved the issue, partially because of posting this question and sanitising the code for public discussion.
In the Login Controller, in my development code the request for the user to be returned was subsequently ignored, passing through the user request details which had a null ExtraParam, not the returned user which had the ExtraParam populated.
The moral of the story is to confirm which objects are being used at which points in the code, or have one object that is passed into, updated by, then returned from functions to maintain consistency.
I am trying to improve my projects openapi definition by adding security requirements to endpoints that require authentication.
An endpoint requires authentication if it is a method of a class with the [Authorize] decorator, and lacks a [AllowAnonymous] decorator.
I am trying to use IOperationFilter to add security requirements based on this, but i am struggling with filter descriptors - I am not able to find any documentation on how these work whatsoever.
This is my current filter that i based on another StackOverflow thread:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace OpenData.Filters {
public class AddAuthHeaderOperationFilter : IOperationFilter
{
private readonly IHttpContextAccessor httpContextAccessor;
public AddAuthHeaderOperationFilter(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var filterDescriptor = context.ApiDescription.ActionDescriptor.FilterDescriptors;
var isAuthorized = filterDescriptor.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter);
var allowAnonymous = filterDescriptor.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter);
if (/*isAuthorized && */!allowAnonymous)
{
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
//Add JWT bearer type
operation.Security.Add(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "Bearer",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
}
}
}
}
My problem is the filterDescriptor part. How do i use this to detect if what i am currently applying is a method of a class with an [Authorize] decorator, if it is, if it has a [AllowAnonymous] decorator? I might be misusing terms as well, it is a method like this i want to detect:
[Authorize]
[Route("/api/[controller]")]
public class CommentController : Controller
{
[AllowAnonymous]
[HttpGet("metadata/{metadataUuid}")]
public async Task<IActionResult> GetCommentsForMetadataAsync(Guid metadataUuid)
{
// ...
}
Researching this has been a pain, there doesn't seem to be any easily available documentation for this API on the web, and i haven't been able to find any examples for this version of dotnetcore/swagger either.
My versions:
.netcore 3.1
Swashbuckle.AspNetCore 5.0.0
a method of a class with an [Authorize] decorator,if it (a action) has a [AllowAnonymous] decorator? I might be misusing terms as well
To achieve above requirement, you can try the following code snippet:
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var authAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
.Union(context.MethodInfo.GetCustomAttributes(true))
.OfType<AuthorizeAttribute>();
var allowAnonymousAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
.Union(context.MethodInfo.GetCustomAttributes(true))
.OfType<AllowAnonymousAttribute>();
if (authAttributes.Any() && !allowAnonymousAttributes.Any())
{
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
}
//...
}
For more information about operation filters, please check this documentation:
https://github.com/domaindrivendev/Swashbuckle.AspNetCore#operation-filters
I need to render a view to a string (to send as email). I'm using this implementation.
I want to unit test it, without needing a full ASP.NET Core environment. So I must create an instance of IRazorViewEngine.
The default implementation is RazorViewEngine. I has a mega constructor because each argument needs to be created and each one has a mega constructor, etc., etc. (I can't mock it, I need a live instance.)
Surely there is a simpler way to get an instance?
(Before Core, I could use ViewEngines.Engines. Maybe Core has something similar?)
I tried to do this as well with a unit test similar to this and ran into various issues:
var services = new ServiceCollection();
services.AddMvc();
... ended up needing to add random things into the service collection ...
var serviceProvider = services.BuildServiceProvider();
var razorViewEngine = serviceProvider.GetRequiredService<IRazorViewEngine>();
I ended up going with more of a component test approach using Microsoft.AspNetCore.Mvc.Testing:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
public class ComponentTestStartup : IStartup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
return services.BuildServiceProvider();
}
public void Configure(IApplicationBuilder app)
{
}
}
public class ComponentTestServerFixture : WebApplicationFactory<ComponentTestStartup>
{
public TService GetRequiredService<TService>()
{
if (Server == null)
{
CreateDefaultClient();
}
return Server.Host.Services.GetRequiredService<TService>();
}
protected override IWebHostBuilder CreateWebHostBuilder()
{
var hostBuilder = new WebHostBuilder();
return hostBuilder.UseStartup<ComponentTestStartup>();
}
// uncomment if your test project isn't in a child folder of the .sln file
// protected override void ConfigureWebHost(IWebHostBuilder builder)
// {
// builder.UseSolutionRelativeContentRoot("relative/path/to/test/project");
// }
}
public class RazorViewToStringRendererTests
{
private readonly RazorViewToStringRenderer viewRenderer;
public RazorViewToStringRendererTests()
{
var server = new ComponentTestServerFixture();
var serviceProvider = server.GetRequiredService<IServiceProvider>();
var viewEngine = server.GetRequiredService<IRazorViewEngine>();
var tempDataProvider = server.GetRequiredService<ITempDataProvider>();
viewRenderer = new RazorViewToStringRenderer(viewEngine, tempDataProvider, serviceProvider);
}
[Fact]
public async Task CanRenderViewToString()
{
// arrange
var model = "test model";
// act
var renderedView = await viewRenderer.RenderViewToStringAsync("/Path/To/TestView.cshtml", model);
// assert
Assert.NotNull(renderedView);
Assert.Contains(model, renderedView, StringComparison.OrdinalIgnoreCase);
}
}
TestView.cshtml:
#model string
<h1>#Model</h1>
We have this custom Authorization scheme which I'm trying to solve with the ability to unit test and use dependency injection in .NET core. Let me explain the setup:
I created an interface IStsHttpClient and class StsHttpClient. This class connects to a internal web service that creates & decodes tokens. This has exactly 1 method "DecodeToken(string token)" and the constructor is very simple - it takes in an option object that is loaded from DI.
Then my AuthorizationHandler would in theory just use the IStsHttpClient to call and decode the token. My question is, based on the examples online I don't know how to properly specify/build the Authorization Handler (see code below).
Auth Code here:
public class MyAuthorizationRequirement : AuthorizationHandler<MyAuthorizationRequirement >, IAuthorizationRequirement
{
const string Bearer = "Bearer ";
readonly IStsHttpClient _client;
public BuzzStsAuthorizationRequirement([FromServices]IStsHttpClient client)
{
_client = client;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyStsAuthorizationRequirement requirement)
{
/* remaining code omitted - but this will call IStsHttpClient.Decode() */
My Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.Configure<StsHttpOptions>(Configuration.GetSection("StsConfigurationInfo"));
services.AddScoped<IStsHttpClient , StsHttpClient >();
services.AddAuthorization(options =>
{
options.AddPolicy("Authorize", policy =>
{
/* initialize this differently?? */
policy.AddRequirements(new MyStsAuthorizationRequirement( /* somethign is needed here?? */));
});
});
Nicholas,
You have to separate your handler and requirements here. In addition to that keep your DI stuff in the handler. Requirement itself is going to be either a DTO or an empty class with marker interface IAuthorizationRequirement.
Requirement:
public class MyAuthorizationRequirement : IAuthorizationRequirement
{
}
Handler:
public class MyAuthorizationHandler : AuthorizationHandler<MyAuthorizationRequirement>
{
const string Bearer = "Bearer ";
readonly IStsHttpClient _client;
public BuzzStsAuthorizationRequirement([FromServices]IStsHttpClient client)
{
_client = client;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyAuthorizationRequirement requirement)
{
...
}
}
Configuration:
services.Configure<StsHttpOptions>(Configuration.GetSection("StsConfigurationInfo"));
services.AddScoped<IStsHttpClient , StsHttpClient >();
services.AddAuthorization(options =>
{
options.AddPolicy("Authorize", policy =>
{
policy.AddRequirements(new MyAuthorizationRequirement());
});
});
For other people looking to wrap authorization around an existing permission handler in C#9 NetCore5, the I found the following solution which allowed me to make use of the stock dependency injection container to inject a service into an AuthorizationHandler.
For me this required 5 new classes and some changes to Startup.cs
The following is my PermissionPolicyProvider.cs, this will represent a generic permission, and not a policy (I filter for permissions later)
using System.Data;
using System.Threading.Tasks;
using App.Models;
using App.Services.Permissions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
namespace App.Permissions
{
class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly AppUserManager<AppUser> _appUserManager;
public PermissionAuthorizationHandler(UserManager<AppUser> userManager)
{
_appUserManager = (AppUserManager<AppUser>)userManager;
}
#nullable enable
// public virtual async Task HandleAsync(AuthorizationHandlerContext context)
// {
// foreach (var req in context.Requirements.OfType<TRequirement>())
// {
// await HandleRequirementAsync(context, req);
// }
// }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
var permissionsService = (PermissionService?) _appUserManager.Services.GetService(typeof(PermissionService))
?? throw new NoNullAllowedException("Null found when accessing PermissionService");
if (await permissionsService.Permitted(requirement.Permission))
{
context.Succeed(requirement);
}
}
#nullable disable
}
}
Next is my PermissionPolicyProvider.cs, this code allows us to filter out policies and to dynamically build a permission when received.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
namespace App.Permissions
{
internal class PermissionPolicyProvider : IAuthorizationPolicyProvider
{
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public PermissionPolicyProvider(IOptions<AuthorizationOptions> options) =>
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
public Task<AuthorizationPolicy> GetFallbackPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
// Dynamically creates a policy with a requirement that contains the permission.
// The policy name must match the permission that is needed.
/// <summary>
///
/// </summary>
/// <param name="policyName"></param>
/// <returns></returns>
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (! policyName.StartsWith("Permission", StringComparison.OrdinalIgnoreCase))
{
// If it doesn't start with permission, then it's a policy.
// pass policies onward to default provider
return FallbackPolicyProvider.GetPolicyAsync(policyName);
}
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new PermissionRequirement(policyName));
return Task.FromResult(policy.Build());
}
}
}
Next up is the PermissionAuthorizationHandler.cs, this is where microsoft wants you to custom db checks, so if you don't want to separate your service layer you can stop after this. Note that you can handle one permission at a time or all at once (note the commented out code).
using System.Data;
using System.Threading.Tasks;
using App.Models;
using App.Services.Permissions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
namespace App.Permissions
{
class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly AppUserManager<AppUser> _appUserManager;
public PermissionAuthorizationHandler(UserManager<AppUser> userManager)
{
_appUserManager = (AppUserManager<AppUser>)userManager;
}
#nullable enable
// public virtual async Task HandleAsync(AuthorizationHandlerContext context)
// {
// foreach (var req in context.Requirements.OfType<TRequirement>())
// {
// await HandleRequirementAsync(context, req);
// }
// }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
var permissionsService = (PermissionService?) _appUserManager.Services.GetService(typeof(PermissionService))
?? throw new NoNullAllowedException("Null found when accessing PermissionService");
if (await permissionsService.Permitted(requirement.Permission))
{
context.Succeed(requirement);
}
}
#nullable disable
}
}
If you don't want the service layer separation, this is the last step for you. You just need to properly register all the services. Add the following to your Startup.cs
services.AddDbContext<PokeflexContext>
(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<PokeflexContext>()
.AddUserManager<UserManager<IdentityUser>>()
.AddDefaultTokenProviders();
To separate out the service layer, we need to extend the UserManager. UserManager actually gets access to the entire service layer injected into your app, but it hides it under a private modifier. Our solution is simple: extend the UserManager and override the constructor to pass on our service to a public variable instead. Here is my custom version as AppUserManager
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace App.Permissions
{
public class AppUserManager<TUser> : UserManager<TUser> where TUser : class
{
public IServiceProvider Services;
public AppUserManager(IUserStore<TUser> store,
IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<TUser> passwordHasher,
IEnumerable<IUserValidator<TUser>> userValidators,
IEnumerable<IPasswordValidator<TUser>> passwordValidators,
ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors,
IServiceProvider services, ILogger<UserManager<TUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators,
passwordValidators, keyNormalizer, errors, services, logger)
{
Services = services;
}
}
}
Last step here, we need to update Startup.cs again to reference our custom type. We also add another line here to ensure that if someone requests our service within an endpoint and not as an attribute they will get our custom AppUserManager. My final resulting ConfigureServices contents is as follows
services.AddDbContext<PokeflexContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddTransient<PermissionService>();
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddIdentity<AppUser, IdentityRole>()
.AddEntityFrameworkStores<PokeflexContext>()
.AddUserManager<AppUserManager<AppUser>>()
.AddDefaultTokenProviders();
services.AddScoped(s => s.GetService<AppUserManager<AppUser>>());
If you are already comfortable with service configuration then you probably don't need the following, but here is a simple service I created that the authorization handler can access via DI.
using System.Threading.Tasks;
using App.Data;
using Microsoft.EntityFrameworkCore;
namespace App.Services.Permissions
{
public class PermissionService
{
private PokeflexContext _dbContext;
public PermissionService(PokeflexContext dbContext)
{
_dbContext = dbContext;
}
public virtual async Task<bool> Permitted(string permission)
{
return await _dbContext.AppUsers.AnyAsync();
}
}
}
For some more information on permissioning, visit: https://github.com/iammukeshm/PermissionManagement.MVC
I'm using Visual Studio 2013 MVC, and I installed "Hangfire" to perform scheduled tasks. (http://hangfire.io/)
How can I protect the Web Monitoring UI page (http://localhost/Hangfire) with a password?
Thanks
Please take a look to the documentation
In short.
You can use already created authorization filters or implement your own
using Hangfire.Dashboard;
public class MyRestrictiveAuthorizationFilter : IAuthorizationFilter
{
public bool Authorize(IDictionary<string, object> owinEnvironment)
{
// In case you need an OWIN context, use the next line.
var context = new OwinContext(owinEnvironment);
return false;
}
}
Additional information:
Also you can take a look to the special package Hangfire.Dashboard.Authorization which contains the logic which you needed
Let me give the entire code for a RestrictiveAuthorizationFilter:
This way you can handle authorization however you desire.
Assuming you have the OWINStartup class added.
OWINStartup.cs
using Owin;
using Hangfire;
using Hangfire.Dashboard;
public class OWINStartup
{
public void Configuration(IAppBuilder app)
{
GlobalConfiguration.Configuration.UseSqlServerStorage("String");
DashboardOptions options = new DashboardOptions()
{
AuthorizationFilters = new IAuthorizationFilter[]
{
new MyRestrictiveAuthorizationFilter()
}
};
app.UseHangfireDashboard("/hangfire", options);
}
}
RestrictiveAuthorizationFilter.cs
using Hangfire.Dashboard;
using System.Collections.Generic;
using Microsoft.Owin;
public class MyRestrictiveAuthorizationFilter : IAuthorizationFilter
{
public bool Authorize(IDictionary<string, object> owinEnvironment)
{
var context = new OwinContext(owinEnvironment);
return context.Authentication.User.Identity.IsAuthenticated;
}
}
Notice: using System.Collections.Generic;
References:
https://github.com/HangfireIO/Hangfire/issues/202
https://media.readthedocs.org/pdf/hangfire/latest/hangfire.pdf (page 20)
Hangfire.Dashboard.Authorization version: 2.1.0
Set this up in your Startup.Cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//TODO
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new MyAuthorizationFilter() }
});
app.UseHangfireDashboard();
var options = new BackgroundJobServerOptions { WorkerCount = 1 };
app.UseHangfireServer(options); }
Create this class, it allows authenticated users to see the Dashboard
public class MyAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
// Allow all authenticated users to see the Dashboard (potentially dangerous).
return httpContext.User.Identity.IsAuthenticated;
}
}