Windows authentication/authorization - asp.net-core

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")].

Related

How to add custom authorization in .NET5?

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

Blazor - B2C authentication - What is the proper way to persist user data on login?

I'm building a Blazor app to see how I can persist user data after a B2C AD login.
I want to persist claim data to sql database (ef 6 core) when the user logs in to the app.
I'm trying to capture a Tenant for the user for use in filtering on the app.
I is custom middleware a good way to go with this?
This is a Blazor Server Side app
I have something like this for testing.
public class PersistUserChangesMiddleware
{
private readonly RequestDelegate _next;
public PersistUserChangesMiddleware(RequestDelegate next)
{
_next = next;
}
[Authorize]
public Task Invoke(HttpContext httpContext, MyContext context)
{
try
{
var user = httpContext.User;
var claims = user.Claims;
var tenant = claims?.FirstOrDefault(c => c.Type.Equals("extension_CompanyId", StringComparison.OrdinalIgnoreCase));
if(tenant != null)
{
context.Tenants.Add(new Models.Tenant()
{
TenantName = tenant.Value
});
context.SaveChanges();
}
}
catch (Exception)
{
throw;
}
return _next(httpContext);
}
}
}
I'm not getting the user back from this call in the middleware. Do I need to do it a different way for Blazor? I set [Authorize] but still no user.
I can't see which AuthenticationStateProvider is configured, but it's likely to be ServerAuthenticationStateProvider.
Create a custom AuthenticationStateProvider which is essentially a pass through provider that just grabs the ClaimsPrincipal user and does whatever you want with it. (Let me know if you're using a different provider).
public class MyAuthenticationStateProvider : ServerAuthenticationStateProvider
{
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var authstate = await base.GetAuthenticationStateAsync();
if (authstate.User is not null)
{
ClaimsPrincipal user = authstate.User;
// do stuff with the ClaimsPrincipal
}
return authstate;
}
}
And then register it in Program:
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// sequence is crucial - Must be after AddServerSideBlazor
builder.Services.AddScoped<AuthenticationStateProvider, MyAuthenticationStateProvider>();
builder.Services.AddSingleton<WeatherForecastService>();
Test it with a break point on the first line of GetAuthenticationStateAsync.

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);
}

Create Role-Based Authorization with Windows Authentication

I have an ASP.NET Core application where I want to add role-based authentication. I'm using Windows Authentication because it's an intranet app. I already have a custom database that contains the users/roles that frankly doesn't map to the fields in the IdentityFramework. I can easily get the logged-in user's name via the Context.User.Identity.Name. I then want to look up the user in the custom user/roles table in order to get the available roles for that user. Then I want to use an annotation-based authentication filter decorated at the Controller or Action method level. For example, [Authorize(roles="admin")].
I was able to get this working by turning off Windows Authentication and using Forms Authentication with Cookies. In the AccountController I ran code like this:
using(LDAPConnection connection = new LDAPConnection(loginModel.UserName,loginModel.Password))
{
List<Claim> claims = new List<Claim> {
new Claim(ClaimTypes.Name, loginModel.UserName),
new Claim(ClaimTypes.Role, "admin")
};
ClaimsIdentity userIdentity = new ClaimsIdentity(claims,"login");
ClaimsPrincipal principal = new ClaimsPrincipal(userIdentity);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(principal),
new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.Now.AddDays(200)
});
return Redirect("/");
}
I would then store the claims in a cookie. Then when I decorate the Controller with [Authorize(roles="admin")], I'm able to retrieve the View without issues. The authorization works. I would like to replicate this same functionality for WindowsAuthentication without logging the user in. I have tried using a ClaimsTransformer and implementing Policy-based authorization, which works. But if I decorate it with [Authorize(roles="admin")] it bombs when I navigate to the action method. Here is the ClaimsTransformer:
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = (ClaimsIdentity)principal.Identity;
List<Claim> claims = new List<Claim> {
new Claim(ClaimTypes.Name, identity.Name),
new Claim(ClaimTypes.Role, "admin")
};
identity.AddClaims(claims);
return Task.FromResult(principal);
}
What piece am I missing in order to use the [Authorize(Roles="admin")] working? BTW, I'm currently using ASP.NET Core 2.2.
You could write a custom Policy Authorization handlers in which you get all User's Roles and check if they contains your desired role name.
Refer to following steps:
1.Create CheckUserRoleRequirement(accept a parameter)
public class CheckUserRoleRequirement: IAuthorizationRequirement
{
public string RoleName { get; private set; }
public CheckUserRoleRequirement(string roleName)
{
RoleName = roleName;
}
}
2.Create CheckUserRoleHandler
public class CheckUserRoleHandler : AuthorizationHandler<CheckUserRoleRequirement>
{
private readonly IServiceProvider _serviceProvider;
public CheckUserRoleHandler(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
CheckUserRoleRequirement requirement)
{
var name = context.User.Identity.Name;
using (var scope = _serviceProvider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<YourDbContext>();
//your logic to look up the user in the custom user/roles table in order to get the available roles for that user
List<string> roles = dbContext.UserRoles.Where(...;
if (roles != null && roles.Contains(requirement.RoleName))
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
3.Register Handler in ConfigureServices
services.AddAuthorization(options =>
{
options.AddPolicy("AdminRole", policy =>
policy.Requirements.Add(new CheckUserRoleRequirement("Admin")));
});
services.AddSingleton<IAuthorizationHandler, CheckUserRoleHandler>();
4.Usage
[Authorize(Policy = "AdminRole")]
I know this is a bit of a late answer, but I've been troubleshooting the same issue today and none of the answers I've seen on similar posts have fixed my issue.
Here are the steps I took to be able to use [Authorize(Roles = "Admin")] on my controller with Windows authentication.
Double check that UseAuthentication() comes before UseAuthorization() in the Configure() method of Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication(); // <--- this needs to be before
app.UseAuthorization(); // <----this
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Hccc}/{action=Index}/");
});
}
Have a claims transformer to handle the necessary roles. For example,
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var ci = (ClaimsIdentity)principal.Identity;
var user = UserAuth.GetUserRole(ci.Name); // gets my user from db with role
// handle your roles however you need.
foreach(var role in user.Roles)
{
var roleClaim = new Claim(ci.RoleClaimType, role.RoleName);
ci.AddClaim(roleClaim);
}
return Task.FromResult(principal);
}
Set up the ConfigureServices() method in Startup.cs to handle authorization
services.AddSingleton<IClaimsTransformation, ClaimsTransformer>();
// Implement a policy called "AdminOnly" that uses "Windows" authentication
// The policy requires Role "Admin"
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
{
policy.AddAuthenticationSchemes("Windows");
policy.RequireRole("Admin");
});
});
services.AddMvc();
services.AddControllersWithViews();
Use the [Authorize] tag to implement the policy. For my case, I wanted to block access to a controller unless the user was an "Admin".
[Authorize(Policy = "AdminOnly")]
public class UsersController : Controller
{
}

Why do I need to set a DefaultForbidScheme

In a WebAPI .net core project I have created a Middleware class that validates an api key. From validating it, it retrieves the permissions that the key has (user or admin) within the invoke method.
I pass it through a switch to set the principle like so
GenericIdentity identity = new GenericIdentity("API");
GenericPrincipal principle = null;
//we have a valid api key, so set the role permissions of the key
switch (keyValidatorRes.Role)
{
case Roles.User:
principle = new GenericPrincipal(identity, new[] { "User" });
context.User = principle;
break;
case Roles.Admin:
principle = new GenericPrincipal(identity, new[] { "Admin" });
context.User = principle;
break;
default:
principle = new GenericPrincipal(identity, new[] { "Other" });
context.User = principle;
break;
}
On controllers methods I have
[Authorize(Roles = "Admin")]
to validate the roles of an authenticated api key
If the user has the admin principle it goes through as expected. However, if it has a user or other principle then I get an error about
not having a DefaultForbidScheme
I googled around and added Authentication to my startup.cs with a customer scheme
services.AddAuthentication(options=> {
options.DefaultForbidScheme = "forbidScheme";
options.AddScheme<AuthSchemeHandle>("forbidScheme", "Handle Forbidden");
});
and created the AuthSchemeHandle
public class AuthSchemeHandle : IAuthenticationHandler
{
private HttpContext _context;
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.NoResult());
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.FromResult(AuthenticateResult.Fail("Failed Auth"));
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
}
Now if the principle does not have Admin it fails without the error but the response that is returned on the API is 200 with no content. I was expecting a 4xx response with the message "Failed Auth"
I am just trying to work out why it is not as expected as although it seems "fixed" I do not understand how it has fixed it.
Is there a better way that I should be doing this?
why it is not as expected as although it seems "fixed" I do not understand how it has fixed it.
There's no dark magic when the authentication handler calls IAuthenticationHandler.ForbidAsync() method. We have to do relevant things ourself. In short, setting the StatusCode=403 as your need.
public async Task ForbidAsync(AuthenticationProperties properties)
{
properties = properties ?? new AuthenticationProperties();
_context.Response.StatusCode = 403;
// ...
return Task.CompletedTask;
}
As a side note, you don't need return a Task.FromResult() as it doesn't care about the result.
Is there a better way that I should be doing this?
The ASP.NET Core Team provides us an abstract class AuthenticationHandler to handle authentication. This abstract class has a built-in implementation for ForbidAsync(AuthenticationProperties properties) (and also for other public methods). So it's much easy to extends this abstract class as below:
public class MyAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public MyAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
return AuthenticateResult.NoResult();
}
}
Finally, add a configuration for authentication service:
services
.AddAuthentication(options=>{
options.DefaultAuthenticateScheme = "forbidScheme";
options.DefaultForbidScheme = "forbidScheme";
options.AddScheme<MyAuthenticationHandler>("forbidScheme", "Handle Forbidden");
});
It should work as expected.