In my asp.net core Web API i want to be sure that Users actually exist and are not deactivated when calling some Actions. We use JwtBearer authentication what works pretty fine. Our problem is that using only the Microsoft.AspNetCore.Authorization.AuthorizeAttribute it won't matter, if a user was deleted or deactivated after the token was issued (as long as the token it self is valid).
In my Startup.cs i configured a policy to solve this issue:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = AuthenticationHandler.TokenValidationParameters;
});
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
options.AddPolicy(nameof(ExistingUserRequirement),
policy => policy.Requirements.Add(new ExistingUserRequirement()));
});
services.AddTransient<IAuthorizationHandler, ExistingUserHandler>();
There i query the DataContext if the user is existing and active.
What works:
If I configure the Authorization attribute on an action everything works fine:
[HttpPost]
[Authorize(Policy = nameof(ExistingUserRequirement))]
public JsonResult Post([FromBody]CreateStoryViewModel viewModel) { ...}
I can access the requests user identity using HttpContextAccessor.HttpContext.User.Identity.Name.
What not works: If I configure the attribute on my BaseController the Identity-context seems the be missing when the ExistingUserHandler fires up:
[Authorize(Policy = nameof(ExistingUserRequirement))]
public abstract class BaseController : Controller { ... }
Now Identity.Name returns always NULL:
public class ExistingUserHandler : AuthorizationHandler<ExistingUserRequirement>
{
private IHttpContextAccessor HttpContextAccessor { get; }
private SaycleContext SaycleContext { get; }
public ExistingUserHandler(IHttpContextAccessor httpContextAccessor, SaycleContext saycleContext)
{
HttpContextAccessor = httpContextAccessor;
SaycleContext = saycleContext;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext authorizationContext, ExistingUserRequirement requirement)
{
var exists = false;
if (Guid.TryParse(HttpContextAccessor.HttpContext.User.Identity.Name, out var currentUserId))
{
exists = SaycleContext.Users.Any(u => Equals(u.Id, currentUserId));
}
if (!exists)
{
throw new NonExistingUserException();
}
authorizationContext.Succeed(requirement);
return Task.CompletedTask;
}
}
Am I missing anything? Why can the identity-context not be resolved using the Authorize attribute on my parent BaseController?
Related
I am trying to add in authorization middleware which sets the user in a dependency injected interface. I have tested this and is successful.
I then wanted to add a authorization handler which checks the users role vs the role which is expected, for example to limit certian actions of the api to superusers.
I've created the authorization handler which is here, as you can see I'm dependency injecting the IUserProvider, the authenticated user is set within the middleware of this method.
public class RoleHandler : AuthorizationHandler<RoleRequirement>, IAuthorizationHandler
{
private readonly IUserProvider _userProvider;
public RoleHandler(IUserProvider userProvider)
{
_userProvider = userProvider;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RoleRequirement requirement)
{
var currentUser = _userProvider.GetAuthenticatedUser();
if (requirement.Roles.Contains(currentUser.RoleId))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
AuthenticationMiddleware:
public async Task InvokeAsync(HttpContext context, IUserRepository userRepository, IUserProvider userProvider, ApplicationContext applicationContext)
{
var email = context.User.Claims.FirstOrDefault(claim => claim.Type == "preferred_username");
if (email == null)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
var user = await userRepository.FindByEmailAsync(email.Value);
if (user == null)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
userProvider.SetAuthenticatedUser(user);
await _next.Invoke(context);
}
I can see that the AuthorizationHandler is getting called before the AuthorizationMiddleware which is why this is occuring.
However, I've tried to check the context within the HandleRequirementAsync and the user in here is also null.
Here is my user provider, as you can see very basic:
public class UserProvider : IUserProvider
{
private static User AuthenticatedUser { get; set; } = default!;
public User GetAuthenticatedUser()
{
return AuthenticatedUser;
}
public void SetAuthenticatedUser(User user)
{
AuthenticatedUser = user;
}
}
Is there anything I can do to alter the execution order?
EDIT:
Forgot to add in the controller where I am using this:
[Authorize(Policy = "Superuser")]
[Route("{id}"]
public async Task<User> UpdateAsync([FromBody] User user, int id)
{
return await _userService.UpdateAsync(user);
}
And the Program.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Web;
using System.IdentityModel.Tokens.Jwt;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDataAccessLayer();
builder.Services.AddHttpContextAccessor();
builder.Services.AddDbContext<ApplicationContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("Database")).EnableSensitiveDataLogging().EnableDetailedErrors();
});
builder.Services.AddScoped<IAuthorizationHandler, RoleHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Superuser", policy => policy.Requirements.Add(new RoleRequirement(Role.SUPER_USER)));
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<AuthenticationMiddleware>();
app.MapControllers();
app.Run();
I had to solve the opposite problem; the middleware was running before my AuthorizationHandlers.
To get the authorization middleware to run after the authorization attributes have run, create a class that implements the IAuthorizationMiddlewareResultHandler interface as described here.
Additional notes:
For anyone else that wasted an afternoon and stumbles across this answer looking for a way to get the custom authorization middleware to run after your resource-based authorization (after you call the AuthorizeAsync method), you can't. You will either need to use middleware or a result filter.
Basically, I would like to do a clean Integration Testing using net6.0. I want to invoke the endpoints of my controller which will eventually go into the database and write some data. Afterward, I will assert some aspects of the inserted data.
I can bypass the Bearer Token used by [Authorize] attribute but cannot bypass my custom attribute named as [HasPermission]
I have this custom attribute:
public class HasPermission : TypeFilterAttribute
{
public HasPermission(string resource, string scope)
: base(typeof(HasPermissionAuthorize))
{
Arguments = new object[] { resource, scope };
}
};
public class HasPermissionAuthorize : IAuthorizationFilter
{
private readonly string _resource;
private readonly string _scope;
private readonly IAuthService _authService;
public HasPermissionAuthorize(string resource, string scope, IAuthService authService)
{
_resource = resource;
_scope = scope;
_authService = authService;
}
public async void OnAuthorization(AuthorizationFilterContext context)
{
string token = context.HttpContext.Request.Headers["Authorization"];
token = token.Replace("Bearer ", "");
bool hasPermission = await _authService.HasClaimAsync(token, "permission", _resource + "." + _scope);
if (!hasPermission)
{
context.Result = new UnauthorizedResult();
}
}
And this is my controller:
[Authorize] //<-- I can bypass this
[Route("v1/[controller]")]
[ApiController]
public class MachinesController : ControllerBase
{
[HasPermission("Machines", "Create")] //<-- How do I bypass this?
[HttpPost]
public async Task<IActionResult> Post([FromBody] AddMachineRequest request)
{
//some logic
}
}
In my Integration Testing
public class ApiWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
//I'm bypassing the [Authorize] attribute here
//However, I do not know how to bypass [HasPermission] attribute
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Test")
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Test";
options.DefaultChallengeScheme = "Test";
options.DefaultScheme = "Test";
}) //TestAuthHandler is written somewhere else
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", options => { });
}
}
}
I'm testing my code using IClassFixture<ApiWebApplicationFactory>. Even though I can bypass [Authorize] attribute, I could NOT find an elegant way to bypass [HasPermission] attribute.
OK. I already was thinking about Mocking IAuthService but as also Guru Stron pointed out, I went for that approach. So, here is my latest update on the code that solves my problem:
public class ApiWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
//Added this line
services.AddTransient<IAuthService>(_ => _authServiceMock.Object);
//Above somewhere I also mocked it like this:
//_authServiceMock.Setup(c => c.HasClaimAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).ReturnsAsync(true);
//other stuff from my original question remains here and they are the same
}
}
}
I'm creating a simple Asp.Net Core Web App using Razor Pages and Asp.Net Core Identity and I having an issue where the authorization handlers for policies applied to a PageModel are still executed after a policy applied for the same Page's folder despite each authorization handler is related to a different Requirement Class. My problem is that even calling Context.Fail() at the folder policy authorization handler, an unauthenticated user seems to still reach the Page and this fires the Page policy authorization handler then throws an exception because the User object is null. I want to implement a simple Flow where requests to Pages are first validated by the policies set on the Folders they belong to, ok? continue to Page (and run the Page authorization handler), not ok, return a forbidden response immediately.
Any help is really appreciated.
Requirements:
public class SectionsAccessRequirement : IAuthorizationRequirement
{
public string SectionName { get; }
public SectionsAccessRequirement(string Name)
{
SectionName = Name;
}
}
public class RecordCreateRequirement : IAuthorizationRequirement { }
Authorization handlers:
public class SectionsAccessHandler : AuthorizationHandler<SectionsAccessRequirement>
{
public IHttpContextAccessor _accessor;
public SectionsAccessHandler(IHttpContextAccessor Accessor) => _accessor = Accessor;
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SectionsAccessRequirement requirement)
{
// Require authentication
if (!_accessor.HttpContext.User.Identity.IsAuthenticated) context.Fail();
// Admin
if (context.User.HasClaim(c => c.Type == ClaimNameConstants.UserGroup && c.Value == UserGroupConstants.AdminGroup)) context.Succeed(requirement);
if (requirement.SectionName == SectionNameConstants.AdminSection) context.Fail();
// Client
if (requirement.SectionName == SectionNameConstants.ClientSection && context.User.HasClaim(c => c.Type == ClaimNameConstants.ClientSection)) context.Succeed(requirement);
return Task.CompletedTask;
}
}
public class RecordCreateAuthorizationHandler : AuthorizationHandler<RecordCreateRequirement>
{
private readonly UserManager<AppUser> _users;
public RecordCreateAuthorizationHandler(UserManager<AppUser> Users) => _users = Users;
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RecordCreateRequirement requirement)
{
// User ID
int sessionUserId = int.Parse(_users.GetUserId(context.User));
// Privileges
if (!context.User.HasClaim(c => c.Type == ClaimNameConstants.UserGroup && c.Value == UserGroupConstants.AdminGroup))
{
if (context.User.HasClaim(c => c.Type == ClaimNameConstants.ClientCreate)) context.Succeed(requirement);
}
else
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
I have these two extension methods:
public static class AuthorizationOptionsExtension
{
public static void AddPolicies(this AuthorizationOptions Options)
{
// Folders policies
Options.AddPolicy(PolicyNameConstants.AdminSectionPolicy, p => p.Requirements.Add(new SectionsAccessRequirement(SectionNameConstants.AdminSection)));
Options.AddPolicy(PolicyNameConstants.ClientSectionPolicy, p => p.Requirements.Add(new SectionsAccessRequirement(SectionNameConstants.ClientSection)));
// Pages policies
Options.AddPolicy(PolicyNameConstants.ClientRecordCreatePolicy, p => p.Requirements.Add(new Client.RecordCreateRequirement()));
// Fallback policy
Options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
}
}
And:
public static class RazorPagesOptionsExtension
{
public static void AddFoldersAuthorization(this RazorPagesOptions Options)
{
Options.Conventions.AuthorizeFolder("/Admin", PolicyNameConstants.AdminSectionPolicy);
Options.Conventions.AuthorizeFolder("/Client", PolicyNameConstants.ClientSectionPolicy);
}
}
The Page's policy is applied using an Authorize attribute:
[Authorize(Policy = PolicyNameConstants.ClientRecordCreatePolicy)]
public class CreateModel : PageModel
{ ... }
This is my startup class, I'm using Asp.Net Core Identity:
public class Startup
{
private readonly IConfiguration _config;
public Startup(IConfiguration config)
{
_config = config;
}
// Add (and configure) services to the container
public void ConfigureServices(IServiceCollection services)
{
// Global application settings (formerly known as Web.config) and other settings
services.Configure<ApplicationSettings>(_config);
// Application
services.AddApplicationLayerServices();
services.AddRazorPages();
services.AddHttpContextAccessor();
services.Configure<RazorViewEngineOptions>(options =>
{
options.PageViewLocationFormats.Add("/Pages/Shared/Partials/{0}" + RazorViewEngine.ViewExtension);
options.PageViewLocationFormats.Add("/Pages/Client/Partials/{0}" + RazorViewEngine.ViewExtension);
});
// Data store
services.AddDbContext<DataContext>(options => options.UseOracle(_config.GetConnectionString("AppConnectionString")));
// Security
services.AddIdentity<AppUser, IdentityRole<int>>(options => {
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 8;
}).AddEntityFrameworkStores<DataContext>();
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(15);
options.LoginPath = "/SignIn";
options.AccessDeniedPath = "/FourZeroSomething";
options.SlidingExpiration = true;
});
services.AddAccessControlLayerServices();
services.AddAuthorization(options => options.AddPolicies());
services.Configure<RazorPagesOptions>(options => options.AddFoldersAuthorization());
}
// Configure the HTTP request pipeline
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
// Navigation
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// Security
app.UseAuthentication();
app.UseAuthorization();
// Endpoints
app.UseEndpoints(endpoints => endpoints.MapRazorPages());
}
}
}
As detailed in the docs, this behaviour is by design:
If a handler calls context.Succeed or context.Fail, all other handlers are still called. This allows requirements to produce side effects, such as logging, which takes place even if another handler has successfully validated or failed a requirement. When set to false, the InvokeHandlersAfterFailure property short-circuits the execution of handlers when context.Fail is called. InvokeHandlersAfterFailure defaults to true, in which case all handlers are called.
If you'd rather not set the InvokeHandlersAfterFailure option, you can check the AuthorizationHandlerContext.HasFailed property in your more specific handlers to see if it's false and return early. e.g.:
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RecordCreateRequirement requirement)
{
if (context.HasFailed)
return;
// User ID
int sessionUserId = int.Parse(_users.GetUserId(context.User));
// ...
}
I created a react project with the default react template in asp.net core with individual user accounts. [Authorize] attribute works fine however when I try to implement [Authorize(Roles = "Administrator")] I get a 403 status code.
I've added ProfileService to add claims.
ProfileService.cs
public class ProfileService : IProfileService
{
protected UserManager<ApplicationUser> mUserManager;
public ProfileService(UserManager<ApplicationUser> userManager)
{
mUserManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
ApplicationUser user = await mUserManager.GetUserAsync(context.Subject);
IList<string> roles = await mUserManager.GetRolesAsync(user);
IList<Claim> roleClaims = new List<Claim>();
foreach (string role in roles)
{
roleClaims.Add(new Claim(JwtClaimTypes.Role, role));
}
context.IssuedClaims.AddRange(roleClaims);
}
public Task IsActiveAsync(IsActiveContext context)
{
return Task.CompletedTask;
}
}
"role": "Administrator" exists in my JWT token.
I have the Authorize attribute added to my controller.
[Authorize(Roles = "Administrator")]
[HttpGet]
public async Task<ActionResult<IEnumerable<Order>>> GetOrders()
{
return await _context.Orders.ToListAsync();
}
I have also configured my Startup.cs as below.
Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
//services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
// .AddEntityFrameworkStores<ApplicationDbContext>();
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddAuthorization();
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.RoleClaimType = JwtClaimTypes.Role;
});
services.AddTransient<IProfileService, ProfileService>();
services.AddControllersWithViews();
services.AddRazorPages();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
Unsure where I'm going wrong with this.
If you want to use the built-in Role authorization, you will have to change the way you assign the role claim from this:
roleClaims.Add(new Claim(JwtClaimTypes.Role, role));
to this:
roleClaims.Add(new Claim(ClaimTypes.Role, role));
the better way will be to use policy-based authorization https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-5.0.
Configure your policy in ConfigureServices:
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole",
policy => policy.RequireClaim(ClaimTypes.Role, "Administrator"));
});
In ProfileService
var claims = roles.Select(role => new Claim(ClaimTypes.Role, role)).ToList();
Add to the controller:
[Authorize(Policy = "RequireAdministratorRole")]
It might not work because of the Microsoft claim name inconsistency can be fixed with:
services.Configure<JwtBearerOptions>(options =>
{
var validator = options.SecurityTokenValidators.OfType<JwtSecurityTokenHandler>().SingleOrDefault();
// Turn off Microsoft's JWT handler that maps claim types to .NET's long claim type names
validator.InboundClaimTypeMap = new Dictionary<string, string>();
validator.OutboundClaimTypeMap = new Dictionary<string, string>();
});
I am using JwtBearer authentication to secure my API. I am adding [Authorize] above each API and it worked.
I am using this code to add the authentication in the startup:
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:1234";
options.RequireHttpsMetadata = false;
options.Audience = "test";
});
I want a way to add the [Authorize] to a function in a service, or write a code in the function that works the same as [Authorize].
Using [Authorize] without passing any parameters boils down to a call that checks whether or not the user is authenticated. From inside a service, that would look something like this:
// If any of the properties being accessed are null, assume that the user
// is not authenticated.
var isAuthenticated = httpContext?.User?.Identity?.IsAuthenticated ?? false;
To access HttpContext inside of a service, you can use IHttpContextAccessor. Here's a complete example:
public class Service
{
private readonly IHttpContextAccessor httpContextAccessor;
public Service(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public void ServiceFunction()
{
var httpContext = httpContextAccessor.HttpContext;
var isAuthenticated = httpContext?.User?.Identity?.IsAuthenticated ?? false;
if (isAuthenticated)
{
// The user is authenticated.
}
}
}
If you want to apply an authorisation policy, you can use IAuthorizationService. Here's a complete example of that:
public class Service
{
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IAuthorizationService authzService;
public Service(IHttpContextAccessor httpContextAccessor,
IAuthorizationService authzService)
{
this.httpContextAccessor = httpContextAccessor;
this.authzService = authzService;
}
public async Task ServiceFunction()
{
var httpContext = httpContextAccessor.HttpContext;
var isAuthenticated = httpContext?.User?.Identity?.IsAuthenticated ?? false;
if (isAuthenticated)
{
// The user is authenticated.
var authzResult = await authzService.AuthorizeAsync(
httpContext.User,
"PolicyName");
if (authzResult.Succeeded)
{
// The user is authorised.
}
}
}
}
Note: To use IHttpContextAccessor, you might need to add services.AddHttpContextAccessor(); to your Startup.ConfigureServices method.