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.
Related
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));
// ...
}
Our system is role based and makes a lot of use of the Authorize attribute
In our App Services we use the code below to set this up:
public void ConfigureAuth(IApplicationBuilder app)
{
if (Configuration.GetValue<bool>("UseLoadTest"))
{
app.UseMiddleware<ByPassAuthMiddleware>();
}
app.UseAuthentication();
}
How can I do this with an Azure function Http Trigger?
There is no UseAuthentication() method on IFunctionsHostBuilder.
public override void Configure(IFunctionsHostBuilder builder)
{
}
I'm using .NET Core 3.1.
AFAIK Functions doesn't have such an attribute currently. This issue tracks the work going in to support Filters for Functions but this is still in preview.
Based on the above preview feature, looks like this is one library - dark-loop/functions-authorize - that adds support for this.
I started using Middleware for this: Took inspiration from this article:
https://jinalkumarpatel.hashnode.dev/azure-functions-middleware-part-2-authentication-middleware
Create the middleware:
public class BearerAuthenticationMiddleware
: IFunctionsWorkerMiddleware
{
private readonly ILogger<BearerAuthenticationMiddleware> logger;
public BearerAuthenticationMiddleware(ILogger<BearerAuthenticationMiddleware> logger)
{
this.logger = logger;
}
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
if (context.IsHttpTriggerFunction())
{
var headers = context.BindingContext.BindingData["Headers"]?.ToString();
var httpHeaders = System.Text.Json.JsonSerializer.Deserialize<HttpHeaders>(headers);
if (httpHeaders?.Authorization != null &&
httpHeaders.Authorization.StartsWith("Bearer"))
{
//Validation logic for your token. Here If Bearer present I consider as Valid.
if (httpHeaders.Authorization.Contains("admin"))
{
// Originally based on token get user role.
// Put into context.Items so it will be used by next middleware.
context.Items.Add("UserRole", "Admin");
}
await next(context);
}
else
{
await context.CreateJsonResponse(System.Net.HttpStatusCode.Unauthorized, new { Message = "Token is not valid." });
}
}
else
{
await next(context);
}
}
}
Register the middleware:
public class Program
{
public static void Main()
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults(configure=>
{
// other middleware also configure following way.
// It will execute in same order it is configured over here.
configure.UseMiddleware<SimpleMiddleware>();
})
.Build();
host.Run();
}
}
I have a .NET Core 2.2 Web API that authenticates with JWT tokens. Tokens are generated by Identity Server 4 on a separate API.
All the authentication and authorisation works as expected with JWT tokens. But I need to extend this to allow usage of API keys. If an API key is supplied, I want to load up the claims of that particular user, add it to the request and let Authorize attribute deal with the set policies.
Here is what I have done so far following suggestions from here. My error is exactly the same as the linked post and it works for me as well using GenericPrincipal with a set of roles but I am using AuthorisationPolicies and I always get 401 error with my current implementation, giving me errors similar to the link above.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
options.Filters.Add(new AuthorizeFilter());
options.Filters.Add(typeof(ValidateModelStateAttribute));
options.AllowEmptyInputInBodyModelBinding = true;
})
.AddAuthorization(options =>
{
options.AddPolicies();
})
.AddJsonFormatters();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration["Authentication:Authority"];
options.RequireHttpsMetadata = true;
options.ApiName = Configuration["Authentication:ApiName"];
});
services.AddCors();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment 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.UseCors(policy =>
{
policy.AllowAnyHeader();
policy.AllowAnyMethod();
policy.AllowAnyOrigin();
});
app.UseHttpsRedirection();
app.UseMiddleware<ApiKeyMiddleware>();
app.UseAuthentication();
app.UseMvc();
}
AuthorizationPolicies.cs
public static class AuthorizationPolicies
{
public const string ReadUsersPolicy = "ReadUsers";
public const string EditUsersPolicy = "EditUsers";
public static void AddPolicies(this AuthorizationOptions options)
{
options.AddPolicy(ReadUsersPolicy, policy => policy.RequireClaim(Foo.Permission, Foo.CanReadUsers));
options.AddPolicy(EditUsersPolicy, policy => policy.RequireClaim(Foo.Permission, Foo.CanEditUsers));
}
}
ApiKeyMiddleware
public class ApiKeyMiddleware
{
public ApiKeyMiddleware(RequestDelegate next)
{
_next = next;
}
private readonly RequestDelegate _next;
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.StartsWithSegments(new PathString("/api")))
{
if (context.Request.Headers.Keys.Contains("ApiKey", StringComparer.InvariantCultureIgnoreCase))
{
var headerKey = context.Request.Headers["ApiKey"].FirstOrDefault();
await ValidateApiKey(context, _next, headerKey);
}
else
{
await _next.Invoke(context);
}
}
else
{
await _next.Invoke(context);
}
}
private async Task ValidateApiKey(HttpContext context, RequestDelegate next, string key)
{
var userClaimsService = context.RequestServices.GetService<IUserClaimsService>();
List<string> permissions = (await userClaimsService.GetAllPermissionsForApiKey(key))?.ToList();
if (permissions == null)
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.WriteAsync("Invalid Api Key");
return;
}
ICollection<Claim> claims = permissions.Select(x => new Claim(FooClaimTypes.Permission, x)).ToList();
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
context.User = principal;
await next.Invoke(context);
}
}
UsersController.cs
[Authorize(AuthorizationPolicies.EditUsersPolicy)]
public async Task<IActionResult> Put([FromBody] UserUpdateDto userUpdateDto)
{
...
}
Apparently, I had to set AuthenticationType to be Custom on the ClaimsIdentity as explained here.
var identity = new ClaimsIdentity(claims, "Custom");
I wanted to add a principle onto the thread by myself , without using the Identity mechanism which really reminds me the old membership/forms authentication mechanics.
So I've managed (successfully) to create a request with principle :
MyAuthMiddleware.cs
public class MyAuthMiddleware
{
private readonly RequestDelegate _next;
public MyAuthMiddleware(RequestDelegate next )
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
var claims = new List<Claim>
{
new Claim("userId", "22222222")
};
ClaimsIdentity userIdentity = new ClaimsIdentity(claims ,"MyAuthenticationType");
ClaimsPrincipal principal = new ClaimsPrincipal(userIdentity);
httpContext.User = principal;
await _next(httpContext);
}
}
The Configure method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//app.UseAuthentication(); //removed it. I will set the thread manually
if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
app.UseMyAuthMiddleware(); // <---------- Myne
app.UseMvc();
app.Run(async context => { await context.Response.WriteAsync("Hello World!"); });
}
And here is the Action in the Controller: (Notice Authorize attribute)
[HttpGet]
[Route("data")]
[Authorize]
public IActionResult GetData()
{
var a=User.Claims.First(f => f.Type == "userId");
return new JsonResult(new List<string> {"a", "b",a.ToString() , User.Identity.AuthenticationType});
}
Ok Let's try calling this method, Please notice that this does work :
So where is the problem 😊?
Please notice that there is an [Authorize] attribute. Now let's remove
setting principle on the thread ( by removing this line ) :
//httpContext.User = principal; // line is remarked
But now when I navigate to :
http://localhost:5330/api/cities/data
I'm being redirected to :
http://localhost:5330/Account/Login?ReturnUrl=%2Fapi%2Fcities%2Fdata
But I'm expecting to see Unauthorized error.
I'm after WebApi alike responses. This is not a website but an API.
Question:
Why don't I see the Unauthorized error ? And how can I make it appear?
Nb here is my ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication( CookieAuthenticationDefaults.AuthenticationScheme )
.AddCookie( CookieAuthenticationDefaults.AuthenticationScheme, a =>
{
a.LoginPath = "";
a.Cookie.Name = "myCookie";
});
services.AddMvc();
}
EDIT
Currently What I've managed to do is to use OnRedirectionToLogin :
But it will be really disappointing if that's the way to go. I'm expecting it to be like webapi.
The default implementation of the OnRedirectToLogin delegate looks like this:
public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogin { get; set; } = context =>
{
if (IsAjaxRequest(context.Request))
{
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 401;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
};
As is clear from the code above, the response that gets sent to the client is dependent upon the result of IsAjaxRequest(...), which itself looks like this:
private static bool IsAjaxRequest(HttpRequest request)
{
return string.Equals(request.Query["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal) ||
string.Equals(request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal);
}
This means that the response will be a 401 redirect if either a X-Requested-With request header or query-string value is set to XMLHttpRequest. When you hit your endpoint directly from the browser or from within e.g. Fiddler, this value is not set and so the response is a 302, as observed. Otherwise, when using XHR or Fetch in the browser, this value gets set for you as a header and so a 401 is returned.
I have developed a custom middleware in ASP.NET Core but it gets triggered with every request. My intent is to use it only when a request is authorized.
Update:
I create more sample for you and edit my answer. as you see, before the Next() method, I checked every token request. If didn't have Authorization tag in header of context request, be next(), If did check the token.
Now, may you have a question that what is the await _next(context); It is very complex and long, I want to suggest you that visit this link to know what is this issue.
For Create a Middlware you have to control and develop.
Create Generally class as Middleware that work General action like you action Authorization.
Create a static Extension class for relation between Middleware and startup.
And finally Register in startup configuration.
Now this is good sample for you:
General Middlware:
public class RequestTokenMiddleware
{
private readonly RequestDelegate _next;
private readonly SignInManager<User> _signInManager;
public RequestTokenMiddleware(RequestDelegate next, SignInManager<User> signInManager)
{
_next = next;
_signInManager = signInManager;
}
public async Task Invoke(HttpContext context)
{
try
{
var hasAuthorization = context.Request.Headers.ContainsKey("Authorization");
if (!hasAuthorization)
{
await _next(context);
}
else
{
var shouldBeNext = false;
foreach (var item in context.Request.Headers)
{
if (item.Key == "Authorization")
{
using (var contextBudget = BudgetUnitOfWork.Get())
{
var tokenCode = item.Value.ToString().Remove(0, 7);
var token = await contextBudget.Db.Token.FirstOrDefaultAsync(x => x.TokenCode == tokenCode).ConfigureAwait(false);
if (token == null || !token.IsValid)
{
signOut(context);
}
else
{
shouldBeNext = true;
}
}
}
}
if (shouldBeNext)
{
await _next(context);
}
}
}
catch (Exception exc)
{
signOut(context);
}
}
private async void signOut(HttpContext context)
{
try
{
await context.Response.WriteAsync(JsonConvert.SerializeObject(ResultModel.Failure(null, ResultModel.StatusType.InvalidToken)));
}
catch (Exception)
{
throw new Exception();
}
}
}
This is Static Extension class for Relation:
public static class ReuqestTokenMiddlewareExctention
{
public static IApplicationBuilder UseTokenValidator(this IApplicationBuilder applicationBuilder)
{
return applicationBuilder.UseMiddleware<RequestTokenMiddleware>();
}
}
Now Register your Middleware in startup:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IOptions<SetiaSettings> options)
{
app.UseTokenValidator();
}
I believe you have a misunderstanding of middleware. Fundamentally middleware is software that will be assembled into the application pipeline to manage each request and response.
That means that middleware is executed every time.
An important concept though, would be how middleware handles each component. In essence top down execution, all allowed code will execute unless conditionally stated otherwise.
Chooses whether to pass the request to the next component in the pipeline.
Can perform work before and after the next component in the pipeline is invokved.
So your early statement is the behavior you should experience, but now you have to ensure the request being instantiated is allowed to execute or not. Your approach for security would basically be in your middleware, allowing that authorization to occur.
An example would be the authorize attribute, but I am not sure what authorization / authentication mechanism you are using.
To explicitly answer your question, you could do something along these lines:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Administrator"));
});
}