How to add custom authorization in .NET5? - asp.net-core

I have ASP.NET Core MVC application using NET 5. Only authenticated users are allowed to access the application. The authorization policy below takes care of it.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options =>
{
var authorizationPolicy = new AuthorizationPolicyBuilder()
.RequireClaim(ClaimTypes.Email)
.RequireClaim(ClaimTypes.NameIdentifier)
.RequireClaim(ClaimTypes.Name)
.RequireClaim(IdentityClaimTypes.IdToken)
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(authorizationPolicy));
})
}
The controllers are also using AuthorizeRoles attribute to check access based on roles.
public class AuthorizeRolesAttribute : AuthorizeAttribute
{
public AuthorizeRolesAttribute(params string[] roles) : base()
{
if (roles.Length > 0)
{
Roles = string.Join(",", roles);
}
}
}
[AuthorizeRoles("ClientAdmin")]
public class WorkItemClientsController : BaseController
{
private readonly IClientWorkItemService _clientWorkItemService;
public WorkItemClientsController(IClientWorkItemService clientWorkItemService)
{
_clientWorkItemService = clientWorkItemService;
}
[HttpGet]
[Route("workitems/{workItemID}/clients")]
public async Task<ActionResult> Index([FromRoute(Name = "workItemID")] int workItemID)
{
}
}
The application has few actions that need to be further authorized based on the user's data in the database. I have the following
public class WorkItemRequirement : IAuthorizationRequirement
{
}
public class WorkItemAuthorizationHandler : AuthorizationHandler<WorkItemRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, WorkItemRequirement requirement)
{
//check if logged in user can access this route based on workitemid from the route, if true then return context.Succeed(requirement);
}
}
public class WorkItemAuthorizeAttribute : AuthorizeAttribute
{
public WorkItemAuthorizeAttribute()
{
Policy = "WorkItemPolicy"
}
}
I will add WorkItemAuthorizeAttribute to require action methods.
What I am missing here is how WorkItemAuthorizeAttribute will know which handler to invoke. In this case its WorkItemAuthorizationHandler.
What do I need to change/add in AuthorizationPolicyBuilder in startup.cs to make this association?

Pretty much everything you can find in official docs here
basically as you said you need to modify your policy to include your WorkItemRequirement like that:
.Requirements.Add(new WorkItemRequirement());
That will 'glue' Policy in your Attribute with your AuthorizationHandler

Related

Resource based authorization in SignalR

I have web API with custom policies and authorization handlers.
I wanted to reuse authorization handlers but HttpContext is null when attribute is used on signalr's hub.
For example this is my controller.
[Authorize]
public sealed class ChatsController : ControllerBase
{
[HttpPost("{chatId}/messages/send")]
[Authorize(Policy = PolicyNames.ChatParticipant)]
public Task SendMessage() => Task.CompletedTask;
}
And this my my authorization handler. I can extract "chatId" from HttpContext and then use my custom logic to authorize user.
internal sealed class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ChatParticipantRequirementHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement)
{
if(_httpContextAccessor.HttpContext != null)
{
// Logic
}
return Task.CompletedTask;
}
}
However this won't work with Azure SignalR because I don't have access to HttpContext. I know that I can provide custom IUserIdProvider but I have no idea how to access "chatId" from "Join" method in my custom authorization handler.
[Authorize]
public sealed class ChatHub : Hub<IChatClient>
{
[Authorize(Policy = PolicyNames.ChatParticipant)]
public async Task Join(Guid chatId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, chatId.ToString());
}
Is it possible to reuse my authorization handlers?
I would like to avoid copypasting my code.
One solution is to extract my authorization code to separate services but then I have to manually call those from my hubs and abandon [Authorize] way.
Your chat is a resource, and you want to use resource based authorization. In this case declarative authorization with an attribute is not enough, because chat id is known at runtime only. So you have to use imperative authorization with IAuthorizationService.
Now in your hub:
[Authorize]
public sealed class ChatHub : Hub<IChatClient>
{
private readonly IAuthorizationService authService;
public ChatHub(IAuthorizationService authService)
{
this.authService = authService;
}
public async Task Join(Guid chatId)
{
// Get claims principal from authorized hub context
var user = this.Context.User;
// Get chat from DB or wherever you store it, or optionally just pass the ID to the authorization service
var chat = myDb.GetChatById(chatId);
var validationResult = await this.authService.AuthorizeAsync(user, chat, PolicyNames.ChatParticipant);
if (validationResult.Succeeded)
{
await Groups.AddToGroupAsync(Context.ConnectionId, chatId.ToString());
}
}
}
Your authorization handler should look different, because it needs the chat resource in its signature to do this kind of evaluation:
internal sealed class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement, Chat>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ChatParticipantRequirementHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement, Chat chat)
{
// You have both user and chat now
var user = context.User;
if (this.IsMyUserAuthorizedToUseThisChat(user, chat))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
Edit: there is actually another option I didn't know about
You can make use of HubInvocationContext that SignalR Hub provides for authorized methods. This can be automatically injected into your AuthorizationHandler, which should look like this:
public class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement, HubInvocationContext>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement, HubInvocationContext hubContext)
{
var chatId = Guid.Parse((string)hubContext.HubMethodArguments[0]);
}
}
Hub method will be decorated normally with [Authorize(Policy = PolicyNames.ChatParticipant)]
You still will have two authorization handlers, AuthorizationHandler<ChatParticipantRequirement> and AuthorizationHandler<ChatParticipantRequirement, HubInvocationContext>, no way around it. As for code dublication, you can however just get the Chat ID in the handler, either from HttpContext or HubInvocationContext, and than pass it to you custom written MyAuthorizer that you could inject into both handlers:
public class MyAuthorizer : IMyAuthorizer
{
public bool CanUserChat(Guid userId, Guid chatId);
}

.NET Core Middleware - access IApplicationBuilder in a controller?

I need to access IApplicationBuilder inside a controller.
What I have tried :-
I have written middleware (app.UseMyMiddleware) as follows
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IHttpContextAccessor httpContextAccessor)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMyMiddleware();
app.UseAuthentication();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
///TODO - Pass IApplicationBuilder to HttpContext
await _next(context);
}
}
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
but I can't figure out how I can pass IApplicationBuilder to HttpContext in Invoke method. so, that I can use it in a controller.
I have also referred following stackoverflow question-answer
how to access IApplicationBuilder in a controller?
.Net Core Middleware - Getting Form Data from Request
Question(s) :-
How can pass IApplicationBuilder to HttpContext in Invoke method to use it in controller?
Is there any better way to access IApplicationBuilder inside controller apart from middleware?
IApplicationBuilder was not designed to work the way you want it to. Instead, if you have some data created at build time that you want to be available to middleware add a Singleton to the services and inject the singleton into the middleware.
You cannot access IApplicationBuilder anywhere later after completing the application building phase (after running Configure method). It's not available for injection at all.
However for the purpose of plugging-in or configuring middlewares at runtime based on request data (from HttpContext), you can use .UseWhen. Another one for terminal middleware is .MapWhen but I think that's not for your case. Here is an example of .UseWhen:
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
var allOptions = new [] {"option 1","option 2"};
foreach(var option in allOptions){
var currentOption = option;
builder.UseWhen(context => {
//suppose you can get the user's selected option from query string
var selectedOption = context.Request.Query["option_key"];
return selectedOption == currentOption;
}, app => {
//your MyMiddleware is supposed to accept one argument
app.UseMiddleware<MyMiddleware>(currentOption);
});
}
return builder;
}
}
To simplify it I suppose your options are just strings, you must know beforehand all possible options that the user can select via UI. Each one will be an exact match for the condition to plug-in a middleware and they must be all exclusive (so just one of them can enable one corresponding middleware), otherwise there will be duplicate middlewares, which may cause some issue.
By expressing the foreach above more clearly, it may represent something as follows:
//kind of pseudo code
if(selectedOption1){
app.UseMiddleware<MyMiddleware>("option 1");
} else if(selectedOption2){
app.UseMiddleware<MyMiddleware>("option 2");
}
...
You must decide how you get the selected option from the user (in the example above I get it from query string). You can get it from Cookie as well (to remember the user's selection) or from other sources such as route data, headers, form, request body. I think that's another issue, so if you have problem with that, please ask in another question.
First up all thanks to #Kingking and #GlennSills for there solution and valuable comments.
I have solved this problem as
Created one class which inherit from Hangfire.JobStorage as follows
public class HangfireSqlServerStorageExtension : Hangfire.JobStorage
{
private readonly HangfireSqlServerStorage _hangfireSqlServerStorage = new HangfireSqlServerStorage();
public HangfireSqlServerStorageExtension(string nameOrConnectionString)
{
_hangfireSqlServerStorage.SqlServerStorageOptions = new SqlServerStorageOptions();
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public HangfireSqlServerStorageExtension(string nameOrConnectionString, SqlServerStorageOptions options)
{
_hangfireSqlServerStorage.SqlServerStorageOptions = options;
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public void UpdateConnectionString(string nameOrConnectionString)
{
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public override IStorageConnection GetConnection()
{
return _hangfireSqlServerStorage.SqlServerStorage.GetConnection();
}
public override IMonitoringApi GetMonitoringApi()
{
return _hangfireSqlServerStorage.SqlServerStorage.GetMonitoringApi();
}
}
HangfireSqlServerStorage.cs
Used in HangfireSqlServerStorageExtension class above
public class HangfireSqlServerStorage
{
public SqlServerStorage SqlServerStorage { get; set; }
public SqlServerStorageOptions SqlServerStorageOptions { get; set; }
}
Startup.cs
In Startup file add singleton service for HangfireSqlServerStorageExtension instance and configure hangfire dashboard as follows
public class Startup
{
///Other necessary code here
public static HangfireSqlServerStorageExtension HangfireSqlServerStorageExtension { get; private set; }
public void ConfigureServices(IServiceCollection services)
{
///Other necessary code here
HangfireSqlServerStorageExtension = new HangfireSqlServerStorageExtension("DBConnecttionString"));
services.AddSingleton<HangfireSqlServerStorageExtension>(HangfireSqlServerStorageExtension);
services.AddHangfire(configuration => configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_170));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IHttpContextAccessor httpContextAccessor)
{
//Other necessary code here
app.UseHangfireDashboard("/Dashboard", new DashboardOptions(), HangfireSqlServerStorageExtension);
//Other necessary code here
}
}
Inside controller I have used it as follows
HangfireController.cs
public class HangfireController : Controller
{
protected readonly HangfireSqlServerStorageExtension
hangfireSqlServerStorageExtension;
public HangfireController(HangfireSqlServerStorageExtension hangfireSqlServerStorageExtension)
{
this.hangfireSqlServerStorageExtension = hangfireSqlServerStorageExtension;
}
public IActionResult DisplayHangfireDashboard()
{
// Update connString as follows
hangfireSqlServerStorageExtension.UpdateConnectionString(connString);
var hangfireDashboardUrl = $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}" + "/Dashboard";
return Json(new { url = hangfireDashboardUrl });
}
}

declarative resource based requirement authorization not working with API Controller

I am trying setup an authoriztion policy that I can use to decorate actions in API Controllers in .net core 3.1. I have been following these examples :
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-3.1
If i have an API action decorated like below my code does not hit the handlerequirementAsync method of the handler and I get a 403 Forbidden response from swagger. If i remove the document model from the handler/requirement it does work. Am I doing something wrong or is this not supported for api requests?
here is the other relevant code :
public class DocumentAuthorizationHandler :
AuthorizationHandler<SameAuthorRequirement, Document>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
SameAuthorRequirement requirement,
Document resource)
{
if (context.User.Identity?.Name == resource.Author)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
public class SameAuthorRequirement : IAuthorizationRequirement { }
[Authorize(Policy = "EditPolicy")]
public async Task<FileResult> RetreiveFile([FromRoute]Document model)
{
}
services.AddAuthorization(options =>
{
options.AddPolicy("EditPolicy", policy =>
policy.Requirements.Add(new SameAuthorRequirement()));
});
services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();
You should have to inject IAuthorizationService in the controller constructor
public class AbcController : Controller
{
private readonly IAuthorizationService _authorizationService;
public AbcController(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
// No need to add this [Authorize(Policy = "EditPolicy")]
public async Task<FileResult> RetreiveFile([FromRoute]Document model)
{
//add this below line which shall call the handler
var authorizationResult = await _authorizationService.AuthorizeAsync(User, model, "EditPolicy");
}

Windows authentication/authorization

I am working on a website where I need to authorize the user through a service. I have managed to get windows authentication working if I use the AuthorizeAttribute (User.Identities will be set). My plan is to create a custom middleware that sets the roles/claims for the user but context.User is not set in the middleware. User.Identities will also not be set in the controllers where I don't add the AuthorizeAttribute.
My goal is to write a middleware that gets the windows username and calls a service with the username to get the roles the user has access to and then set the roles or claims for the user.
public class RoleMiddleware
{
private readonly RequestDelegate _next;
public RoleMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (!rolesSet)
{
var result = _service.GetRoles(context.User.Identity.Name);
//set roles
//set claims
}
await _next.Invoke(context);
}
}
Would a middleware be the correct place to do this and what do I need to do to get access to the username in the same way as I do when I use the AuthorizeAttribute in a controller?
In my opinion that's not the right way to do it. ASP.NET Identity provide rich set of classes which you can override and extend to fit your requirements.
If you want to inject roles bases on some custom service then you should override RoleStore (and maybe RoleManager too) and inject there your custom roles.
It will be also worth to take a look here: Using Role Claims in ASP.NET Identity Core
I solved it by using requirements
public class CustomFunctionRequirement : IAuthorizationRequirement
{
public CustomFunctionRequirement(string function)
{
Function = function;
}
public string Function { get; }
}
The handler
public class CustomFunctionHandler : AuthorizationHandler<CustomFunctionRequirement>
{
private readonly Service _service;
public CustomFunctionHandler(Service service)
{
_service = service;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomFunctionRequirement requirement)
{
var functions = _service.GetFunctions(context.User.Identity.Name);
if (functions.Any(x => x == requirement.Function))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Setup in ConfigureServices in Startup
services.AddMvc(
config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
services.AddAuthorization(
options =>
{
options.AddPolicy("User", policy => policy.Requirements.Add(new CustomRequirement("User")));
});
I can now in my controller specify the requirement by adding the authorize attribute [Authorize(Policy = "User")].

How do Implement authorization in MVC4 Web API?

I Had create a MVC4 Web API. But People without authorization also can use it.
Example: people type in address bar "/api/product/1" also can get the result.
So, How to implement Security and allow authorize person to use the WEB API only ?
How to give authorize to the person that allow login to web api ?
More info about Authentication and Authorization
Simply adding the annotation to your controller will do:
// Require authorization for all actions on the controller.
[Authorize]
public class ValuesController : ApiController
{
public HttpResponseMessage Get(int id) { ... }
public HttpResponseMessage Post() { ... }
}
// Restrict by user:
[Authorize(Users="Alice,Bob")]
public class ValuesController : ApiController
{
}
// Restrict by role:
[Authorize(Roles="Administrators")]
public class ValuesController : ApiController
{
}
you can use MVC4 AspNet.identiy.Usermanager and Microsoft.Owin.Security to authenticate user..
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.Current.GetOwinContext().Authentication;
}
}
public HttpResponseMessage Login(string username, string password)
{
UserManager<TenantUser> userManager=new new UserManager<TenantUser>(new UserStore<TenantUser>(YOUR DBCONTEXT));
var user = UserManager.Find(username, password);
if (user != null)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicatioCookie);
ClaimsIdentity identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, identity);
}
else
new HttpResponseMessage(HttpStatusCode.Forbidden) { Content = new ObjectContent<object>(new { Error = "You are not authorized to perform this action" }, Configuration.Formatters.JsonFormatter) };
}
it is working for me....