I'm actually migrating some parts of my previous WCF services to Web API.
I had used QueryInterceptor on my Machine entity which checks whether the current user has access to the desired data and returns all the data or a filtered set that they are allowed to see.
[QueryInterceptor("Machines")]
public Expression<Func<Machine, bool>> FilterMachines()
{
return CheckMachineAccess<Machine>(m => m.MachineRole==xyz && m.userHasPermission);
}
I'm finding it difficult to implement the same in Web API. I'm using odata v4, OWIN hosted web API.
Anyone has any suggestions regarding this? Thanks in advance :)
Edit:
I have followed this approach. Don't know if this is the right way to follow.
[HttpGet]
[ODataRoute("Machines")]
[EnableQuery]
public IQueryable<Machine> FilterMachines(ODataQueryOptions opts)
{
var expression = CheckMachineAccess<Machine>(m => m.MachineRole==xyz && m.userHasPermission);
var result = db.Machines.Where(expression);
return (IQueryable<Machine>)result;
}
OP you are on the right track, if that is working for you then I totally support it!
I'll address the Title of your question directly first.
While using middleware is a good way to intercept incoming requests for Authentication and Access control, it is not a great way to implement row level security or to manipulate the query used in your controller.
Why? To manipulate the query for the controller, before the request is passed to the controller your middleware code will need to know so much about the controller and the data context that a lot of code will be duplicated.
In OData services, a good replacement for the many QueryInterceptor implementations is to Inherit from the EnableQuery Attribute.
[AttributeUsage(validOn: AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class EnableQueryAttribute : System.Web.OData.EnableQueryAttribute
{
public EnableQueryAttribute()
{
// TODO: Reset default values
}
/// <summary>
/// Intercept before the query, here we can safely manipulate the URL before the WebAPI request has been processed so before the OData context has been resolved.
/// </summary>
/// <remarks>Simple implementation of common url replacement tasks in OData</remarks>
/// <param name="actionContext"></param>
public override void OnActionExecuting(HttpActionContext actionContext)
{
var tokens = HttpUtility.ParseQueryString(actionContext.Request.RequestUri.AbsoluteUri);
// If the caller requested oDataV2 $inlinecount then remove it!
if (tokens.AllKeys.Contains("$inlinecount"))
{
// CS: we don't care what value they requested, OData v4 will only return the allPages count
tokens["$count"] = "true";
tokens.Remove("$inlinecount");
}
// if caller forgot to ask for count and we are top'ing but paging hasn't been configured lets add the overall count for good measure
else if (String.IsNullOrEmpty(tokens["$count"])
&& !String.IsNullOrEmpty(tokens["$top"])
&& this.PageSize <= 0
)
{
// we want to add $count if it is not there
tokens["$count"] = "true";
}
var modifiedUrl = ParseUri(tokens);
// if we modified the url, reset it. Leaving this in a logic block to make an obvious point to extend the process, say to perform other clean up when we know we have modified the url
if (modifiedUrl != actionContext.Request.RequestUri.AbsoluteUri)
actionContext.Request.RequestUri = new Uri(modifiedUrl);
base.OnActionExecuting(actionContext);
}
/// <summary>
/// Simple validator that can fix common issues when converting NameValueCollection back to Uri when the collection has been modified.
/// </summary>
/// <param name="tokens"></param>
/// <returns></returns>
private static string ParseUri(System.Collections.Specialized.NameValueCollection tokens)
{
var query = tokens.ToHttpQuery().TrimStart('=');
if (!query.Contains('?')) query = query.Insert(query.IndexOf('&'), "?");
return query.Replace("?&", "?");
}
/// <summary>
/// Here we can intercept the IQueryable result AFTER the controller has processed the request and created the intial query.
/// </summary>
/// <remarks>
/// So you could append filter conditions to the query, but, like middleware you may need to know a lot about the controller
/// or you have to make a lot of assumptions to make effective use of this override. Stick to operations that modify the queryOptions
/// or that conditionally modify the properties on this EnableQuery attribute
/// </remarks>
/// <param name="queryable">The original queryable instance from the controller</param>
/// <param name="queryOptions">The System.Web.OData.Query.ODataQueryOptions instance constructed based on the incomming request</param>
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
// I do not offer common examples of this override, because they would be specific to your business logic, but know that it is an available option
return base.ApplyQuery(queryable, queryOptions);
}
}
But how do we solve your issue of what is effectively an implementation of Row Level Security?
What you have implemented already is very similar to what I would have done. You are right, in your controller method you have enough information about the
context to be able to apply a filter to your query.
I had a similar idea in my projects and have a common base class for all my controllers that has a single method that all inheriting controllers must use to get the initial filtered query for their respective entity type:
The following are a cut down version of my base class methods for applying security style rules to a query
/// <summary>
/// Get the base table query for this entity, with user policy applied
/// </summary>
/// <returns>Default IQueryable reference to use in this controller</returns>
protected Task<IQueryable<TEntity>> GetQuery()
{
var dbQuery = this.GetEntityQuery();
return this.ApplyUserPolicy(dbQuery);
}
/// <summary>
/// Inheriting classes MUST override this method to include standard related tables to the DB query
/// </summary>
/// <returns></returns>
protected abstract DbQuery<TEntity> GetEntityQuery();
/// <summary>
/// Apply default user policy to the DBQuery that will be used by actions on this controller.
/// </summary>
/// <remarks>
/// Allow inheriting classes to implement or override the DBQuery before it is parsed to an IQueryable, note that you cannot easily add include statements once it is IQueryable
/// </remarks>
/// <param name="dataTable">DbQuery to parse</param>
/// <param name="tokenParameters">Security and Context Token variables that you can apply if you want to</param>
/// <returns></returns>
protected virtual IQueryable<TEntity> ApplyUserPolicy(DbQuery<TEntity> dataTable, System.Collections.Specialized.NameValueCollection tokenParameters)
{
// TODO: Implement default user policy filtering - like filter by tenant or customer.
return dataTable;
}
So now in your controller you would override the ApplyUserPolicy method to evaluate your security rules in the specific context of the Machine data, which would result in the following changes to your endpoint.
Note that I have also included additional endpoints to show how with this pattern ALL endpoints in your controller
should use GetQuery() to ensure they have the correct security rules applied.
The implication of this pattern though is that A single item Get will return not found instead of access denied if the item is not
found because it is out of scope for that user. I prefer this limitation because my user should not have any knowledge that the other data
that they are not allowed to access exists.
/// <summary>
/// Check that User has permission to view the rows and the required role level
/// </summary>
/// <remarks>This applies to all queries on this controller</remarks>
/// <param name="dataTable">Base DbQuery to parse</param>
/// <returns></returns>
protected override IQueryable<Machine> ApplyUserPolicy(DbQuery<Machine> dataTable)
{
// Apply base level policies, we only want to add further filtering conditions, we are not trying to circumvent base level security
var query = base.ApplyUserPolicy(dataTable, tokenParameters);
// I am faking your CheckMachineAccess code, as I don't know what your logic is
var role = GetUserRole();
query = query.Where(m => m.MachineRole == role);
// additional rule... prehaps user is associated to a specific plant or site and con only access machines at that plant
var plant = GetUserPlant();
if (plant != null) // Maybe plant is optional, so admin users might not return a plant, as they can access all machines
{
query = query.Where(m => m.PlantId == plant.PlantId);
}
return query;
}
[HttpGet]
[ODataRoute("Machines")]
[EnableQuery]
public IQueryable<Machine> FilterMachines(ODataQueryOptions opts)
{
// Get the default query with security applied
var expression = GetQuery();
// TODO: apply any additional queries specific to this endpoint, if there are any
return expression;
}
[HttpGet]
[ODataRoute("Machine")]
[EnableQuery] // so we can still apply $select and $expand
[HttpGet]
public SingleResult<Machine> GetMachine([FromODataUri] int key)
{
// Get the default query with security applied
var query = GetQuery();
// Now filter for just this item by id
query = query.Where(m => m.Id == key);
return SingleResult.Create(query);
}
[HttpGet]
[ODataRoute("MachinesThatNeedService")]
[EnableQuery]
internal IQueryable<Machine> GetMachinesServiceDue(ODataQueryOptions opts)
{
// Get the default query with security applied
var query = GetQuery();
// apply the specific filter for this endpoint
var lastValidServiceDate = DateTimeOffset.Now.Add(-TimeSpan.FromDays(60));
query = query.Where(m => m.LastService < lastValidServiceDate);
return query;
}
You can use OWIN middelware to enter in the pipe of the request.
You will have a function with HTTP request and you can decide to accept or reject the request.
Function to implement is like this:
public async override Task Invoke(IOwinContext context)
{
// here do your check!!
if(isValid)
{
await Next.Invoke(context);
}
Console.WriteLine("End Request");
}
Related
I'm having trouble specifying two separate Authorization attributes on a class method: the user is to be allowed access if either of the two attributes are true.
The Athorization class looks like this:
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class AuthAttribute : AuthorizeAttribute {
. . .
and the action:
[Auth(Roles = AuthRole.SuperAdministrator)]
[Auth(Roles = AuthRole.Administrator, Module = ModuleID.SomeModule)]
public ActionResult Index() {
return View(GetIndexViewModel());
}
Is there a way to solve this or do I need to rethink my approach?
This is to be run in MVC2.
There is a better way to do this in later versions of asp.net you can do both OR and AND on roles. This is done through convention, listing multiple roles in a single Authorize will perform an OR where adding Multiple Authorize Attributes will perform AND.
OR example
[Authorize(Roles = "PowerUser,ControlPanelUser")]
AND Example
[Authorize(Roles = "PowerUser")]
[Authorize(Roles = "ControlPanelUser")]
You can find more info on this at the following link
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles
Multiple AuthorizeAttribute instances are processed by MVC as if they were joined with AND. If you want an OR behaviour you will need to implement your own logic for checks. Preferably implement AuthAttribute to take multiple roles and perform an own check with OR logic.
Another solution is to use standard AuthorizeAttribute and implement custom IPrincipal that will implement bool IsInRole(string role) method to provide 'OR' behaviour.
An example is here:
https://stackoverflow.com/a/10754108/449906
I've been using this solution in production environment for awhile now, using .NET Core 3.0. I wanted the OR behavior between a custom attribute and the native AuthorizeAttribute. To do so, I implemented the IAuthorizationEvaluator interface, which gets called as soon as all authorizers evaluate theirs results.
/// <summary>
/// Responsible for evaluating if authorization was successful or not, after execution of
/// authorization handler pipelines.
/// This class was implemented because MVC default behavior is to apply an AND behavior
/// with the result of each authorization handler. But to allow our API to have multiple
/// authorization handlers, in which the final authorization result is if ANY handlers return
/// true, the class <cref name="IAuthorizationEvaluator" /> had to be extended to add this
/// OR behavior.
/// </summary>
public class CustomAuthorizationEvaluator : IAuthorizationEvaluator
{
/// <summary>
/// Evaluates the results of all authorization handlers called in the pipeline.
/// Will fail if: at least ONE authorization handler calls context.Fail() OR none of
/// authorization handlers call context.Success().
/// Will succeed if: at least one authorization handler calls context.Success().
/// </summary>
/// <param name="context">Shared context among handlers.</param>
/// <returns>Authorization result.</returns>
public AuthorizationResult Evaluate(AuthorizationHandlerContext context)
{
// If context.Fail() got called in ANY of the authorization handlers:
if (context.HasFailed == true)
{
return AuthorizationResult.Failed(AuthorizationFailure.ExplicitFail());
}
// If none handler called context.Fail(), some of them could have called
// context.Success(). MVC treats the context.HasSucceeded with an AND behavior,
// meaning that if one of the custom authorization handlers have called
// context.Success() and others didn't, the property context.HasSucceeded will be
// false. Thus, this class is responsible for applying the OR behavior instead of
// the default AND.
bool success =
context.PendingRequirements.Count() < context.Requirements.Count();
return success == true
? AuthorizationResult.Success()
: AuthorizationResult.Failed(AuthorizationFailure.ExplicitFail());
}
}
This evaluator will only be called if added to .NET service collection (in your startup class) as follows:
services.AddSingleton<IAuthorizationEvaluator, CustomAuthorizationEvaluator>();
In the controller class, decorate each method with both attributes. In my case [Authorize] and [CustomAuthorize].
I'm not sure how others feel about this but I wanted an OR behavior too. In my AuthorizationHandlers I just called Succeed if any of them passed. Note this did NOT work with the built-in Authorize attribute that has no parameters.
public class LoggedInHandler : AuthorizationHandler<LoggedInAuthReq>
{
private readonly IHttpContextAccessor httpContextAccessor;
public LoggedInHandler(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LoggedInAuthReq requirement)
{
var httpContext = httpContextAccessor.HttpContext;
if (httpContext != null && requirement.IsLoggedIn())
{
context.Succeed(requirement);
foreach (var req in context.Requirements)
{
context.Succeed(req);
}
}
return Task.CompletedTask;
}
}
Supply your own LoggedInAuthReq. In startup inject these in services with
services.AddAuthorization(o => {
o.AddPolicy("AadLoggedIn", policy => policy.AddRequirements(new LoggedInAuthReq()));
... more here
});
services.AddSingleton<IAuthorizationHandler, LoggedInHandler>();
... more here
And in your controller method
[Authorize("FacebookLoggedIn")]
[Authorize("MsaLoggedIn")]
[Authorize("AadLoggedIn")]
[HttpGet("anyuser")]
public JsonResult AnyUser()
{
return new JsonResult(new { I = "did it with Any User!" })
{
StatusCode = (int)HttpStatusCode.OK,
};
}
This could probably also be accomplished with a single attribute and a bunch of if statements. It works for me in this scenario. asp.net core 2.2 as of this writing.
EDIT: This has been solved. Please see my EDIT below. For a more "default" solution where you do not need a "ignore the claim completely" override, see the accepted answer. At the time of writing, My EDIT below contains code that helps you support another scenario where you want to completely ignore the requirement instead of override it.
I have seen multiple posts on this issue but none really solve my problem.
One promising one sadly did not work for me.
I have some some policies. The default one requires the existence of a claim. But another policy requires that this claim does NOT exist. If the default one is applied on the controller, I can not apply the other one on the method. Instead of overriding my previous policy, the policies are all collected together and the first policy fails because the claim is not available.
A good example:
Startup:
services.AddAuthorization(options =>
{
// 99% of the methods require you to have this claim, so we set this as default.
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireClaim("UserId")
.Build();
options.AddPolicy("UnregisteredUsers",
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser() // You DO need to be authorized, but that does not mean you already have an account!
.RequireAssertion(x => x.User.FindUserIdClaim() == null) // If you do not have a user ID, you can not access endpoints that require you to be registered until you have an ID
unregistered.
.Build());
// A more specific policy
options.AddPolicy("Administrator",
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireRole(Roles.Administrator)
.Build());
});
Controllers:
[Route("api/[controller]")]
[ApiController]
[Authorize] // You could also use MapControllers().RequireAuthorization() in Startup.cs instead of [Authorize]
public class BaseController : ControllerBase
{
}
public class UsersController : BaseController
{
// You can only access this endpoint IF you do not have an account.
[HttpPost]
[Authorize(Policy = "UnregisteredUsers")]
public async Task<IActionResult> CreateAccount()
{
// Code here
return Ok();
}
// This one uses the default policy, just like many other policies
[HttpGet]
public async Task<IActionResult> GetUsers()
{
// This would throw if unregistered users would access this endpoint because the claim is not set.
var userId = User.GetUserIdClaim();
// Other code here
return Ok();
}
}
As you can see, if you were to try to create an account, the basecontroller would already deny you access because you do not have that claim. But I require the claim to NOT exist, so this is not a useful comment. Basically, my 2nd policy is more important than the first one.
I hope you guys can help me out!
EDIT:
Thanks to #King King I have been able to fix this.
Their answer works, but I needed to make some changes for some specific scenario's:
The order is apparently not guaranteed.
The answer works when you want to "override" an existing policy by "turning it around". In this case, by default I require a user ID, but another policy requires that you DO not have it. But a scenario that is not supported out of the box is when you have a policy that does not care if you have user ID or not. I will post my FULL solution here but I am very grateful for #King King for his help!
// Note: Scope is REQUIRED!
services.AddScoped<IAuthorizationHandler, UserIdClaimRequirementHandler>();
services.AddAuthorization(options =>
{
// 99% of the methods require you to have this claim, so we set this as default.
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new UserIdClaimRequirement(UserIdClaimSetting.ClaimMustExist))
.Build();
options.AddPolicy("UnregisteredUsers",
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser() // You DO need to be authorized, but that does not mean you already have an account!
.AddRequirements(new UserIdClaimRequirement(UserIdClaimSetting.ClaimMustNotExist)) // If you do not have a user ID, it means you are not in the database yet, which means you are unregistered.
.Build());
// Registed users and Unregistered users can access these endpoints, but they DO need to be authenticated.
options.AddPolicy("AllUsers",
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new UserIdClaimRequirement(UserIdClaimSetting.IgnoreClaim))
.Build());
// A more specific policy. This builds upon the default policy, so a user ID is required. Technically we could also omit the RequireAuthenticatedUser() call but I like that this is explicit.
options.AddPolicy("Administrator",
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireRole(Roles.Administrator)
.Build());
});
Requirement class:
/// <summary>
/// Can be used to configure the requirement of the existence of the UserId claim.
/// Either it must exist, it must NOT exist or it does not matter. <br/><br/>
///
/// Please note: Order is not guaranteed with policies. <br/>
/// In the case when you use a <see cref="UserIdClaimSetting.IgnoreClaim"/> or <see cref="UserIdClaimSetting.ClaimMustNotExist"/> and the default policy uses a <see cref="UserIdClaimSetting.ClaimMustExist"/>,
/// the handler should prioritize the result of the <see cref="UserIdClaimSetting.IgnoreClaim"/> and <see cref="UserIdClaimSetting.ClaimMustNotExist"/>
/// and just not evaluate <see cref="UserIdClaimSetting.ClaimMustExist"/> to prevent it returning 403 when that one runs last.
/// In order to do so, the handler MUST be registered as Scoped so it resets for the next reset.
/// </summary>
/// <remarks>
/// This is quite a silly solution!
/// The reason it is necessary is because policies are built on top of each other.
/// The default policy requires that the claim exists because this is true for 99% of the requests,
/// so it makes sense to make this the default to prevent having to explicitly setup authorization on each endpoint. <br/><br/>
/// The "UnregisteredUsers" policy requires that it does NOT exist.<br/><br/>
/// The "AllUsers" policy does not care if it exists or not.
/// Your first thought is probably that using this requirement would be unnecessary in that case,
/// but if this requirement is not used there, the default policy's requirement will require the existence of the claim which will break this policy.
/// </remarks>
public class UserIdClaimRequirement : IAuthorizationRequirement
{
public UserIdClaimSetting Setting { get; }
public UserIdClaimRequirement(UserIdClaimSetting setting)
{
Setting = setting;
}
}
public enum UserIdClaimSetting
{
/// <summary>
/// If the claim does <b>not</b> exist, authorization will fail
/// </summary>
ClaimMustExist,
/// <summary>
/// If the claim exists, authorization will fail
/// </summary>
ClaimMustNotExist,
/// <summary>
/// It does not matter if the claim exists. Either way, authorization will succeed.
/// </summary>
IgnoreClaim
}
Requirement Handler:
public class UserIdClaimRequirementHandler : AuthorizationHandler<UserIdClaimRequirement>
{
/// <summary>
/// Order is not guaranteed with policies.
/// In the case when you use a <see cref="UserIdClaimSetting.IgnoreClaim"/> or <see cref="UserIdClaimSetting.ClaimMustNotExist"/> and the default policy uses a <see cref="UserIdClaimSetting.ClaimMustExist"/>,
/// the handler should prioritize the result of the <see cref="UserIdClaimSetting.IgnoreClaim"/> and <see cref="UserIdClaimSetting.ClaimMustNotExist"/>
/// and just not evaluate <see cref="UserIdClaimSetting.ClaimMustExist"/> to prevent it returning 403 when that one runs last.
/// In order to do so, the handler MUST be registered as Scoped so it resets for the next reset.
/// </summary>
private bool _policyHasAlreadySucceeded = false;
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserIdClaimRequirement requirement)
{
if(_policyHasAlreadySucceeded)
{
return Task.CompletedTask;
}
var hasUserId = context.User.FindFirst("UserId") != null;
// If the claim must not exist but it does -> FAIL
// If the claim must exist but it does not -> FAIL
// If it doesn't matter if the claim exists -> SUCCEED
if ((requirement.Setting == UserIdClaimSetting.ClaimMustNotExist && hasUserId) ||
(requirement.Setting == UserIdClaimSetting.ClaimMustExist && !hasUserId) ||
(requirement.Setting == UserIdClaimSetting.IgnoreClaim && false))
{
context.Fail();
}
else
{
// This requirement has succeeded!
_policyHasAlreadySucceeded = requirement.Setting == UserIdClaimSetting.IgnoreClaim || requirement.Setting == UserIdClaimSetting.ClaimMustNotExist;
// Also, if there are other policy requirements that use the UserId claim, just set them to SUCCEEDED because this requirement is more important than those.
// Example: The default policy requires you to have a user id claim, while this requirement might be used by requiring the claim to NOT exist.
// In order to make this work, we have to override the "require user id claim" requirement by telling it that it succeeded even though it did not!
var otherUserIdClaimRequirements = context.Requirements.Where(e => e is UserIdClaimRequirement || e is ClaimsAuthorizationRequirement cu && cu.ClaimType == "UserId");
foreach (var r in otherUserIdClaimRequirements)
{
context.Succeed(r);
}
}
return Task.CompletedTask;
}
}
The authorization requirement handlers are ANDed. So if any failed, the whole will fail.
The authorization policies will be transformed into a set of authorization requirement handlers. Per my debugging, there are 2 authorization requirements transformed from the default policy (in your code) namely DenyAnonymousAuthorizationRequirement (corresponding to RequireAuthenticatedUser()) and ClaimsAuthorizationRequirement with ClaimType = "UserId" (corresponding to RequireClaim("UserId")).
I've found myself one way to override the result (or simply skip, I'm not so sure about this) of the handlers that handle those 2 requirements. That is by implement a custom requirement handler in which you have access to the AuthorizationHandlerContext which exposes all the authorization requirements that need to be handled (of course including the 2 those I mentioned above). By calling Succeed on them, they seem to be ignored (from being handled again or simply skipped). We can add our second custom authorization requirement handler to verify that but it's not important at all (so I did not do it).
Here is how you build your custom policy (UnregisteredUsers) using a custom authorization requirement handler instead of basing on RequireAssertion:
//the custom requirement class which must implement IAuthorizationRequirement
public class NoUserIdClaimRequirement : IAuthorizationRequirement
{
}
//the corresponding handler
public class NoUserIdClaimRequirementHandler : AuthorizationHandler<NoUserIdClaimRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NoUserIdClaimRequirement requirement)
{
var hasUserId = context.User.FindFirst("UserId") != null;
if (hasUserId)
{
context.Fail();
} else
{
//NOTE: if you're sure about the extremely high priority of this requirement
//(so we can discard/ignore all the other requirements), just remove the .Where for a shorter code
foreach (var r in context.Requirements
.Where(e => e is NoUserIdClaimRequirement ||
e is ClaimsAuthorizationRequirement cu && cu.ClaimType == "UserId"))
{
//mark all as succeeded
context.Succeed(r);
}
}
return Task.CompletedTask;
}
}
In the code above, I understand that the user still need to be authenticated so we don't call Succeed for DenyAnonymousAuthorizationRequirement. If that's not true, you can include it in the filter above.
You need to register the authorization requirement handler type in ConfigureServices:
services.AddSingleton<IAuthorizationHandler, NoUserIdClaimRequirementHandler>();
Now instead of using RequireAssertion, you need to build your custom policy like this:
options.AddPolicy("UnregisteredUsers",
x => x.RequireAuthenticatedUser()
.AddRequirements(new NoUserIdClaimRequirement()));
I've tried a simple demo on my own, which works perfectly. But it may need some tweak from your own side. If any error occurs, please let me know. The code here is just to show the idea about a possible way to solve this issue. You can base on that to build a more complicated & general solution.
I need to implement user impersonation through a HTTP header. For example, a user will send a request to /api/something with the header impersonate=someUser.
I tried to following process:
User gets authenticated by one of multiple authentication schemes.
The authenticated user gets replaced by the impersonated user, if it passes some security checks.
The /api/something endpoint is called
I wrote some custom middleware for this, that runs just after the builtin authentication middelware:
if (!context.Request.Headers.TryGetValue("%Impersonation header%", out StringValues subject))
{
await _next(context);
return;
}
if (context.User?.Identity?.IsAuthenticated != true)
{
// return error
}
...
context.User = impersonatedUser
await _next(context);
However, when it finally reaches the controller, the initial user is still used because the ClaimsPrincipal has been replaced by the default authorization into a new object with two identities. The first identity is the real user, the second identity is the impersonated user.
I could potentially resolve the user then using the second identity, but I'm not sure this process is following best practices?
Edit: this is for ASP.NET Core 2.2 / 3.1
configure IISServerOptions in Startup.cs
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddSimpleRoleAuthorization<CustomWindowsAuthenticationProvider>();
services.Configure<IISServerOptions>(opt=>{
opt.AutomaticAuthentication=true;
opt.AuthenticationDisplayName="SIMS";
});
then implement your own IClaimsTransformation to validate the user and set the claims apropriately
public class CustomWindowsAuthenticationProvider : ISimpleRoleProvider
{
public CustomWindowsAuthenticationProvider(UnitOfWork unitOfWork)
{
this._unitOfWork = unitOfWork;
}
private UnitOfWork _unitOfWork;
public Task<ICollection<string>> GetUserRolesAsync(string userName)
{
ICollection<string> result = new string[0];
string[] user = userName.Split("\\");
var roles = _unitOfWork.UserMod.GetRolesForUser(user[1]);
if (roles!=null)
result = roles.Select(d => d.RoleName).ToArray();
return Task.FromResult(result);
}
}
public interface ISimpleRoleProvider
{
#region Public Methods
/// <summary>
/// Loads and returns the role names for a given user name.
/// </summary>
/// <param name="userName">The login name of the user for which to return the roles.</param>
/// <returns>
/// A collection of <see cref="string" /> that describes the roles assigned to the user;
/// An empty collection of no roles are assigned to the user.
/// </returns>
/// <remarks>
/// <para>Beware that this method is called for each controller call. It might impact performance.</para>
/// <para>
/// If Windows authentication is used, the passed <paramref name="userName" />
/// is the full user name including the domain or machine name (e.g "CostroDomain\JohnDoe" or
/// "JOHN-WORKSTATION\JohnDoe").
/// </para>
/// <para>
/// The returned roles names can be used to restrict access to controllers using the <see cref="AuthorizeAttribute" />
/// (<c>[Authorize(Roles="...")]</c>
/// </para>
/// </remarks>
Task<ICollection<string>> GetUserRolesAsync(string userName);
#endregion
}
public class SimpleRoleAuthorizationTransform : IClaimsTransformation
{
#region Private Fields
private static readonly string RoleClaimType = ClaimTypes.Role;// $"http://{typeof(SimpleRoleAuthorizationTransform).FullName.Replace('.', '/')}/role";
private readonly ISimpleRoleProvider _roleProvider;
#endregion
#region Public Constructors
public SimpleRoleAuthorizationTransform(ISimpleRoleProvider roleProvider)
{
_roleProvider = roleProvider ?? throw new ArgumentNullException(nameof(roleProvider));
}
#endregion
#region Public Methods
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
// Cast the principal identity to a Claims identity to access claims etc...
var oldIdentity = (ClaimsIdentity)principal.Identity;
// "Clone" the old identity to avoid nasty side effects.
// NB: We take a chance to replace the claim type used to define the roles with our own.
var newIdentity = new ClaimsIdentity(
oldIdentity.Claims,
oldIdentity.AuthenticationType,
oldIdentity.NameClaimType,
RoleClaimType);
// Fetch the roles for the user and add the claims of the correct type so that roles can be recognized.
var roles = await _roleProvider.GetUserRolesAsync(newIdentity.Name);
if(roles.Count>0)
newIdentity.AddClaims(roles.Select(r => new Claim(RoleClaimType, r)));
// Create and return a new claims principal
return new ClaimsPrincipal(newIdentity);
}
#endregion
}
public static class SimpleRoleAuthorizationServiceCollectionExtensions
{
#region Public Static Methods
/// <summary>
/// Activates simple role authorization for Windows authentication for the ASP.Net Core web site.
/// </summary>
/// <typeparam name="TRoleProvider">The <see cref="Type"/> of the <see cref="ISimpleRoleProvider"/> implementation that will provide user roles.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> onto which to register the services.</param>
public static void AddSimpleRoleAuthorization<TRoleProvider>(this IServiceCollection services)
where TRoleProvider : class, ISimpleRoleProvider
{
services.AddScoped<ISimpleRoleProvider, TRoleProvider>();
services.AddScoped<IClaimsTransformation, SimpleRoleAuthorizationTransform>();
}
#endregion
}
after that you can host the app in iis, and use the iis authentication to determine what kind of method and settings you want to use.
I've read the Microsoft documentation of fundamentals for Options and Configuration, but still can't find the right way to extract configuration into an object while validating data annotations.
One approach I tried in Startup.ConfigureServices
services.AddOptions<EmailConfig>().Bind(Configuration.GetSection("Email")).ValidateDataAnnotations();
This "should" allow accessing the configuration by adding this in the class constructor: (IOptions<EmailConfig> emailConfig)
However it's not working.
Another approach is to add (IConfiguration configuration) to the constructor, but this doesn't allow me to call ValidateDataAnnotations.
configuration.GetSection("Email").Get<EmailConfig>();
First question: does the responsibility to bind and validate the configuration belong to the Startup class or to the class using it? If it's used by several classes I'd say it belongs to Startup; and the class could be used in another project with different configuration layout.
Second question: what is the correct syntax to bind and validate the configuration so it can be accessed from the class?
Third question: if I'm validating through data annotations in Startup, then the class using the configuration simply assumes the configuration is valid and I don't put any re-validation whatsoever?
UPDATE: After gaining more experience and reviewing the structure of all my code, I changed my approach to follow standard patterns.
The following code DOES work... but only validates it when used. This can be registered in a class library and won't throw any errors until the particular service is used.
services.AddOptions<EmailConfig>()
.Bind(configuration.GetSection("Email"))
.ValidateDataAnnotations();
Then, in Configure, I add this to force validation of needed configuration values at startup (CheckNotNull is a custom extension method, what matters is simply that you call IOptions.Value
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app?.ApplicationServices.GetService<IOptions<EmailConfig>>().Value.CheckNotNull("Config: Email");
app?.ApplicationServices.GetService<IOptions<OntraportConfig>>().Value.CheckNotNull("Config: Ontraport");
...
Then in the class using it
public class EmailService(IOptions<EmailConfig> config)
You can try validating the class yourself in start up before adding it to service collection.
Startup
var settings = Configuration.GetSection("Email").Get<EmailConfig>();
//validate
var validationResults = new List<ValidationResult>();
var validationContext = new ValidationContext(settings, serviceProvider: null, items: null);
if (!Validator.TryValidateObject(settings, validationContext, validationResults,
validateAllProperties: true)) {
//...Fail early
//will have the validation results in the list
}
services.AddSingleton(settings);
That way you are not coupled to IOptions and you also allow your code to fail early and you can explicitly inject the dependency where needed.
You could package the validation up into your own extension method like
public static T GetValid<T>(this IConfiguration configuration) {
var obj = configuration.Get<T>();
//validate
Validator.ValidateObject(obj, new ValidationContext(obj), true);
return obj;
}
for calls like
EmailConfig emailSection = Configuration.GetSection("Email").GetValid<EmailConfig>();
services.AddSingleton(emailSection);
Internally, ValidateDataAnnotations is basically doing the same thing.
/// <summary>
/// Validates a specific named options instance (or all when name is null).
/// </summary>
/// <param name="name">The name of the options instance being validated.</param>
/// <param name="options">The options instance.</param>
/// <returns>The <see cref="ValidateOptionsResult"/> result.</returns>
public ValidateOptionsResult Validate(string name, TOptions options)
{
// Null name is used to configure all named options.
if (Name == null || name == Name)
{
var validationResults = new List<ValidationResult>();
if (Validator.TryValidateObject(options,
new ValidationContext(options, serviceProvider: null, items: null),
validationResults,
validateAllProperties: true))
{
return ValidateOptionsResult.Success;
}
return ValidateOptionsResult.Fail(String.Join(Environment.NewLine,
validationResults.Select(r => "DataAnnotation validation failed for members " +
String.Join(", ", r.MemberNames) +
" with the error '" + r.ErrorMessage + "'.")));
}
// Ignored if not validating this instance.
return ValidateOptionsResult.Skip;
}
Source Code
Update from the Future
Newer versions of .NET added more extension methods to simplify this.
Note: Technically these are all from Microsoft.Extensions.XYZ packages released alongside .NET. It's possible that these packages are compatible with earlier versions of .NET as well, but I haven't verified backward-compatibility.
OP's Example
services.AddOptions<EmailConfig>()
.Bind(configuration.GetSection("Email"))
.ValidateDataAnnotations();
Can now be simplified to:
// Requires .NET 5 extensions or greater
services.AddOptions<EmailConfig>()
.BindConfiguration("Email")
.ValidateDataAnnotations();
...and for eager validation at startup (rather than when options are used), we can add a single line:
// Requires .NET 6 extensions or greater
services.AddOptions<EmailConfig>()
.BindConfiguration("Email")
.ValidateDataAnnotations()
.ValidateOnStart();
Source/Credit
I learned about these updates from Andrew Lock's blog post. Credit and thanks go to him: Adding validation to strongly typed configuration objects in .NET 6
There is still no answer as to how ValidateDataAnnotations work, but based on Nkosi's answer, I wrote this class extension to easily run the validation on-demand. Because it's an extension on Object, I put it into a sub-namespace to only enable it when needed.
namespace Websites.Business.Validation {
/// <summary>
/// Provides methods to validate objects based on DataAnnotations.
/// </summary>
public static class ValidationExtensions {
/// <summary>
/// Validates an object based on its DataAnnotations and throws an exception if the object is not valid.
/// </summary>
/// <param name="obj">The object to validate.</param>
public static T ValidateAndThrow<T>(this T obj) {
Validator.ValidateObject(obj, new ValidationContext(obj), true);
return obj;
}
/// <summary>
/// Validates an object based on its DataAnnotations and returns a list of validation errors.
/// </summary>
/// <param name="obj">The object to validate.</param>
/// <returns>A list of validation errors.</returns>
public static ICollection<ValidationResult> Validate<T>(this T obj) {
var Results = new List<ValidationResult>();
var Context = new ValidationContext(obj);
if (!Validator.TryValidateObject(obj, Context, Results, true))
return Results;
return null;
}
}
}
Then in Startup it's quite straightforward
EmailConfig EmailSection = Configuration.GetSection("Email").Get<EmailConfig>().ValidateAndThrow();
services.AddSingleton<EmailConfig>(EmailSection);
Works like a charm; actually works like I'd expect ValidateDataAnnotations to work.
You can also use a method to validate all IOptions in your IOC conainter
private void CheckConfiguration(IApplicationBuilder app, IServiceCollection services)
{
var optionsServiceDescriptors = services.Where(s => s.ServiceType.Name.Contains("IOptionsChangeTokenSource"));
foreach (var service in optionsServiceDescriptors)
{
var genericTypes = service.ServiceType.GenericTypeArguments;
if (genericTypes.Length > 0)
{
var optionsType = genericTypes[0];
var genericOptions = typeof(IOptions<>).MakeGenericType(optionsType);
dynamic instance = app.ApplicationServices.GetService(genericOptions);
var options = instance.Value;
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(options, new ValidationContext(options), results, true);
if (!isValid)
{
var messages = new List<string> { "Configuration issues" };
messages.AddRange(results.Select(r => r.ErrorMessage));
throw new Exception(string.Join("\n", messages));
}
}
}
}
You can find a example here : https://github.com/michelcedric/GetRequiredSectionSample/blob/feature/add-check-configuration/GetRequiredSectionSample/Startup.cs#L73
I've been able create whatever endpoints I've wanted as long as the parameters for each one is different:
public IHttpActionResult GetFightersByWeightClass(string WeightClass)
...
public IHttpActionResult GetFighterByExactName(string NameEquals)
...
But as soon as I try to create two differently named functions that share the same parameters I am unable to use both. I have two endpoints that don't require parameters, shown below:
public class FighterController : ApiController
{
/// <summary>
/// Gets all fighters.
/// </summary>
/// <returns></returns>
[ActionName("GetAllFighters")]
public IEnumerable<Fighter> GetAllFighters()
{
return allFighters;
}
/// <summary>
/// Gets all fighters that are currently undefeated.
/// </summary>
/// <returns></returns>
[ActionName("GetAllUndefeatedFighters")]
public IHttpActionResult GetAllUndefeatedFighters()
{
var results = allFighters.FindAll(f => f.MMARecord.Losses == 0);
if (results == null)
{
return NotFound();
}
return Ok(results);
}
}
Both URLs return this:
{"Message":"An error has occurred.","ExceptionMessage":"Multiple actions were found that match the request: \r\nGetAllFighters on type MMAAPI.Controllers.FighterController\r\nGetAllUndefeatedFighters on type MMAAPI.Controllers.FighterController","ExceptionType":"System.InvalidOperationException","StackTrace":" at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext)\r\n at System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext)\r\n at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)\r\n at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()"}
Not sure why this is happening they each have their own unique action and function name, so I thought they would work like this...:
http://localhost:55865/api/fighter/GetAllUndefeatedFighters -- Just shows fighters with zero losses
http://localhost:55865/api/fighter/ -- shows all fighters
...but instead neither works. If I remove one of them, they other works and vice versa. So they aren't working when they are both active. Any idea why?
Web API allows you to use Attribute routing to customize endpoint URIs.
To use it, add:
config.MapHttpAttributeRoutes();
to the Register method in your WebApiConfig class. Then you can set the endpoints to whatever you want regardless of the Action name.
[Route("getallfighters"), HttpGet, ResponseType(typeof(Fighter))]
public IHttpActionResult ThisNameDoesntMatterNow()
{
//...
}
And your URI becomes:
api/fighter/getallfighters
You can even add attribute routing to your controller:
[RoutePrefix("api/v1/fighters")]
public class FightersController : ApiController
{
//...
}
A combination of the two other answers works well for me. (I've changed the names slightly from the question.)
[RoutePrefix("api/v1/fighters")]
public class FighterController : ApiController
{
/// <summary>
/// Gets all fighters.
/// </summary>
/// <returns>An enumeration of fighters.</returns>
[Route(""), HttpGet]
public IEnumerable<Fighter> GetAllFighters()
{
return allFighters;
}
/// <summary>
/// Gets all fighters that are currently undefeated.
/// </summary>
/// <returns>An enumeration of fighters.</returns>
[Route("undefeated"), HttpGet]
public IEnumerable<Fighter> GetAllUndefeatedFighters()
{
return allFighters.FindAll(f => f.MMARecord.Losses == 0);
}
}
As such, your endpoints would be:
GET /api/v1/fighters
GET /api/v1/fighters/undefeated
Use route attribute
/// <summary>
/// Gets all fighters.
/// </summary>
/// <returns></returns>
[HttpGet]
[System.Web.Http.Route("api/GetAllFighters")]
public IEnumerable<Fighter> GetAllFighters()
{
return allFighters;
}
/// <summary>
/// Gets all fighters that are currently undefeated.
/// </summary>
/// <returns></returns>
[HttpGet]
[System.Web.Http.Route("api/GetAllUndefeatedFighters")]
public IHttpActionResult GetAllUndefeatedFighters()
{
var results = allFighters.FindAll(f => f.MMARecord.Losses == 0);
if (results == null)
{
return NotFound();
}
return Ok(results);
}
and call two method using different route
http://www.yourdomain/api/GetAllFighters
http://www.yourdomain/api/GetAllUndefeatedFighters