I'm trying to setup my authorization policy with a jwt based token for my apis. I have two controllers, used by separate apis. I need to make sure a user can only access the ones that he/she is allowed to use. So I figured I'd go with policy based authorization
[Authorize(Policy = "API1")]
[Route("api1/endpoint")]
public class API1Controller : Controller
{
// my actions for api 1
}
[Authorize(Policy = "API2")]
[Route("api2/endpoint")]
public class API2Controller : Controller
{
// my actions for api 2
}
Adding policies on Startup
services.AddAuthorization(options => {
options.AddPolicy("API1User", policy => policy.Requirements.Add(new ApplicationTypeRequirement(ApplicationType.API1)));
options.AddPolicy("API2User", policy => policy.Requirements.Add(new ApplicationTypeRequirement(ApplicationType.API2)));
});
// Adding handlers after this
So my question is, where is the best place to call a stored procedure to check the database for the users application permission. Reading from the following, (https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1) , it details the use of the claims from the token.
Right now what I have with the JWT token that I save is the userid, first name, last name and email, that's it.
The best place to check authentication and authorization in ActionFilter, you can check auth policy in database side and also with JWT.
If you want to authorise your controller, you have to use a middle ware (ActionFilterAttribute), which will detect user's http request and validate them by decoding user's token. you can filter all http methods (GET,POST,PUT,DELETE...etc), and can implement your own authorisation logic for specific http method.
AuthorizationRequiredAttribute.cs
N.B: here all codes are not relevant to your problem. but hope you'll understand how actually i filter get/post request with condition.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class AuthorizationRequiredAttribute : ActionFilterAttribute
{
private readonly IAccessTokenServices _accessTokenServices;
private readonly IPermissionServices _permissionServices;
private readonly IAuditLogServices _auditLogServices;
private IConfiguration _config;
public AuthorizationRequiredAttribute(IAccessTokenServices accessTokenServices, IPermissionServices permissionServices,
IAuditLogServices auditLogServices,IConfiguration config)
{
_accessTokenServices = accessTokenServices;
_config = config;
_permissionServices = permissionServices;
_auditLogServices = auditLogServices;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
try
{
if (context.HttpContext.Request.Headers.ContainsKey(Constants.HttpHeaders.Token))
{
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadToken(context.HttpContext.Request.Headers[Constants.HttpHeaders.Token])
as JwtSecurityToken;
var expireDate = Convert.ToDateTime(token.Claims.First(claim => claim.Type == Constants.JwtClaims.ExpiresOn).Value);
if (context.HttpContext.Request.Method == WebRequestMethods.Http.Get)
{
if (expireDate < DateTime.Now)
{
context.Result = new UnauthorizedResult();
}
}
else
{
var accessToken = _accessTokenServices
.Details(x => x.Token == context.HttpContext.Request.Headers[Constants.HttpHeaders.Token]);
if (accessToken != null)
{
if (accessToken.ExpiresOn < DateTime.Now)
{
_accessTokenServices.Delete(accessToken);
context.Result = new UnauthorizedResult();
}
else
{
var userId = Convert.ToInt32(token.Claims.First(claim => claim.Type == Constants.JwtClaims.UserId).Value);
var userTypeId = Convert.ToInt32(token.Claims.First(claim => claim.Type == Constants.JwtClaims.UserTypeId).Value);
if (accessToken == null)
{
context.Result = new UnauthorizedResult();
}
else if (!_permissionServices.IsPermissionExist(context.HttpContext.Request.Path.ToString(), userTypeId))
{
context.Result = new StatusCodeResult((int)HttpStatusCode.NotAcceptable);
}
else
{
_auditLogServices.Save(context.HttpContext.Request.Path.ToString(), userId);
accessToken.ExpiresOn = DateTime.Now.AddMinutes(Convert.ToInt16(_config["Jwt:ExpiresOn"]));
_accessTokenServices.UpdateExpireTime(accessToken);
}
}
}
else
{
context.Result = new UnauthorizedResult();
}
}
}
else
{
context.Result = new NotFoundResult();
}
}
catch (Exception ex)
{
context.Result = new BadRequestResult();
}
base.OnActionExecuting(context);
}
}
}
HomeController.cs
Now you can use AuthorizationRequiredAttribute as api/controller filter service. i have modified your controller and see the Message method
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace Chat.Controllers
{
[Route("api/home")]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[HttpGet("message"), ServiceFilter(typeof(AuthorizationRequiredAttribute))]
public IActionResult Message()
{
return Ok("Hello World!");
}
}
}
Related
I'm working on adding authentication (and eventually authorization) with AzureAD to an ASP.NET Core 3.1 app using a custom authorization attribute filter. The code below implements the IAuthorizationFilter's OnAuthorization method within which I redirect the user to the SignIn page when their authentication expires.
When a controller action with [CustomAuthorizationFilter] is hit I expect the attribute's OnAuthorization method to be hit right away whether or not the authentication cookie has expired.
That expectation doesn't happen and instead if a user is not authenticated and a controller action is hit, user is automatically reauthenticated with Microsoft and a valid cookie is created, and only then the OnAuthorization method is hit, defeating what I thought was the purpose of the OnAuthorization method.
I've been doing a lot of research to understand this behavior, but I'm clearly missing something. The most useful piece of information I found was in Microsoft docs:
As of ASP.NET Core 3.0, MVC doesn't add AllowAnonymousFilters for
[AllowAnonymous] attributes that were discovered on controllers and
action methods. This change is addressed locally for derivatives of
AuthorizeAttribute, but it's a breaking change for
IAsyncAuthorizationFilter and IAuthorizationFilter implementations.
So, it appears that implementations with IAuthorizationFilter may be broken in 3.0+ and I don't know how to fix it.
Is this behavior normal or is my implementation incorrect?
If normal, why am I reauthenticated before the OnAuthorization method runs?
If incorrect, how can I implement it correctly?
CustomAuthorizationFilter.cs
public class CustomAuthorizationFilter : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
string signInPageUrl = "/UserAccess/SignIn";
if (context.HttpContext.User.Identity.IsAuthenticated == false)
{
if (context.HttpContext.Request.IsAjaxRequest())
{
context.HttpContext.Response.StatusCode = 401;
JsonResult jsonResult = new JsonResult(new { redirectUrl = signInPageUrl });
context.Result = jsonResult;
}
else
{
context.Result = new RedirectResult(signInPageUrl);
}
}
}
}
The IsAjaxRequest() extension used:
//Needed code equivalent of Request.IsAjaxRequest().
//Found this solution for ASP.NET Core: https://stackoverflow.com/questions/29282190/where-is-request-isajaxrequest-in-asp-net-core-mvc
//This is the one used in ASP.NET MVC 5: https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/AjaxRequestExtensions.cs
public static class AjaxRequestExtensions
{
public static bool IsAjaxRequest(this HttpRequest request)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
if (request.Headers != null)
{
return (request.Headers["X-Requested-With"] == "XMLHttpRequest");
}
return false;
}
}
AzureAD authentication implementation in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
IAppSettings appSettings = new AppSettings();
Configuration.Bind("AppSettings", appSettings);
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options =>
{
options.Instance = appSettings.Authentication.Instance;
options.Domain = appSettings.Authentication.Domain;
options.TenantId = appSettings.Authentication.TenantId;
options.ClientId = appSettings.Authentication.ClientId;
options.CallbackPath = appSettings.Authentication.CallbackPath;
});
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.UseTokenLifetime = false;
options.Authority = options.Authority + "/v2.0/"; //Microsoft identity platform
options.TokenValidationParameters.ValidateIssuer = true;
// https://stackoverflow.com/questions/49469979/azure-ad-b2c-user-identity-name-is-null-but-user-identity-m-instance-claims9
// https://stackoverflow.com/questions/54444747/user-identity-name-is-null-after-federated-azure-ad-login-with-aspnetcore-2-2
options.TokenValidationParameters.NameClaimType = "name";
//https://stackoverflow.com/a/53918948/12300287
options.Events.OnSignedOutCallbackRedirect = context =>
{
context.Response.Redirect("/UserAccess/LogoutSuccess");
context.HandleResponse();
return Task.CompletedTask;
};
});
services.Configure<CookieAuthenticationOptions>(AzureADDefaults.CookieScheme, options =>
{
options.AccessDeniedPath = "/UserAccess/NotAuthorized";
options.LogoutPath = "/UserAccess/Logout";
options.ExpireTimeSpan = TimeSpan.FromMinutes(appSettings.Authentication.TimeoutInMinutes);
options.SlidingExpiration = true;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// 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.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication(); // who are you?
app.UseAuthorization(); // are you allowed?
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=UserAccess}/{action=Login}/{id?}");
});
}
I hoped to find a way to create an AuthorizeAttribute filter to solve this issue, but due to time constraints I settled on a regular action filter. It works with AJAX calls and it redirects the user to the appropriate pages if they are unauthorized or unauthenticated:
AjaxAuthorize action filter:
//custom AjaxAuthorize filter inherits from ActionFilterAttribute because there is an issue with
//a inheriting from AuthorizeAttribute.
//post about issue:
//https://stackoverflow.com/questions/64017688/custom-authorization-filter-not-working-in-asp-net-core-3
//The statuses for AJAX calls are handled in InitializeGlobalAjaxEventHandlers JS function.
//While this filter was made to be used on actions that are called by AJAX, it can also handle
//authorization not called through AJAX.
//When using this filter always place it above any others as it is not guaranteed to run first.
//usage: [AjaxAuthorize(new[] {"RoleName", "AnotherRoleName"})]
public class AjaxAuthorize : ActionFilterAttribute
{
public string[] Roles { get; set; }
public AjaxAuthorize(params string[] roles)
{
Roles = roles;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
string signInPageUrl = "/UserAccess/SignIn";
string notAuthorizedUrl = "/UserAccess/NotAuthorized";
if (context.HttpContext.User.Identity.IsAuthenticated)
{
if (Roles.Length > 0)
{
bool userHasRole = false;
foreach (var item in Roles)
{
if (context.HttpContext.User.IsInRole(item))
{
userHasRole = true;
}
}
if (userHasRole == false)
{
if (context.HttpContext.Request.IsAjaxRequest())
{
context.HttpContext.Response.StatusCode = 401;
JsonResult jsonResult = new JsonResult(new { redirectUrl = notAuthorizedUrl });
context.Result = jsonResult;
}
else
{
context.Result = new RedirectResult(notAuthorizedUrl);
}
}
}
}
else
{
if (context.HttpContext.Request.IsAjaxRequest())
{
context.HttpContext.Response.StatusCode = 403;
JsonResult jsonResult = new JsonResult(new { redirectUrl = signInPageUrl });
context.Result = jsonResult;
}
else
{
context.Result = new RedirectResult(signInPageUrl);
}
}
}
}
The IsAjaxRequest() extension used (reposted for a complete answer):
//Needed code equivalent of Request.IsAjaxRequest().
//Found this solution for ASP.NET Core: https://stackoverflow.com/questions/29282190/where-is-request-isajaxrequest-in-asp-net-core-mvc
//This is the one used in ASP.NET MVC 5: https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/AjaxRequestExtensions.cs
public static class AjaxRequestExtensions
{
public static bool IsAjaxRequest(this HttpRequest request)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
if (request.Headers != null)
{
return (request.Headers["X-Requested-With"] == "XMLHttpRequest");
}
return false;
}
}
JavaScript ajax global error handler:
//global settings for the AJAX error handler. All AJAX error events are routed to this function.
function InitializeGlobalAjaxEventHandlers() {
$(document).ajaxError(function (event, xhr, ajaxSettings, thrownError) {
//these statuses are set in the [AjaxAuthorize] action filter
if (xhr.status == 401 || xhr.status == 403) {
var response = $.parseJSON(xhr.responseText);
window.location.replace(response.redirectUrl);
} else {
RedirectUserToErrorPage();
}
});
}
I wanted to test very simple auth layer
public class CustomAuth : AuthorizeAttribute, IAuthorizationFilter
{
public CustomAuth()
{
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var cookies = context.HttpContext.Request.Cookies;
var ok = cookies["Auth0"] == "asdf";
if (!ok)
{
context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
return;
}
}
}
[CustomAuth]
public IActionResult Index()
{
return View();
}
And when there's no cookie named Auth0 with value asdf then everything work's fine, but when I add it then No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
I tried setting context.Result = ...; to e.g new OkResult() or RedirectToActionResult and it worked, but I just want to let him go straight to that Index instead of moving everything from that action to that OnAuthorization method
how can I achieve that?
For CustomAuth, it inherited from AuthorizeAttribute. The Authentication middleware will check the identity by default authentication scheme.
If you prefer go to Index without configuring any authentication, you could try change AuthorizeAttribute to Attribute like
public class CustomAuth : Attribute, IAuthorizationFilter
{
public CustomAuth()
{
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var cookies = context.HttpContext.Request.Cookies;
var ok = cookies["Auth0"] == "asdf";
if (!ok)
{
context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
return;
}
}
}
I have configured my web api to work with windows authentication. My goal is essentially to restrict certain actions in my controllers based on a users windows account. Some will be able to preform read actions while others will be able to preform actions that will write to the underlying database. I have found plenty of documentation on how to set up claims based authorization which is the route I think I need to go. What I have not found is how to set this up with windows auth. I think I am missing a middle step such as registering the windows auth as the identity provider?
startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddAuthorization(options =>
{
options.AddPolicy("readOnly", policy =>
policy.RequireClaim(`???????????????????????`));
options.AddPolicy("write", policy =>
policy.RequireClaim(`???????????????????????`));
});
}
Controller
[Authorize(Policy = "ReadOnly")]
public class MyController : Controller
{
public ActionResult SomeReadOnlyAction()
{
//Return data from database
}
[Authorize(Policy = "Write")]
public ActionResult AWriteAction()
{
//Create/Update/Delete data from database
}
}
I guess another way to ask this question is how do you configure or access claims/roles etc... with windows authentication.
That seems you want to use claims-based authorization via policies . After setting windows authentication in your application , you could add custom claim to ClaimsPrincipal ,check user's identity and confirm which permission current user has :
You can add a claims transformation service to your application:
class ClaimsTransformer : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var id = ((ClaimsIdentity)principal.Identity);
var ci = new ClaimsIdentity(id.Claims, id.AuthenticationType, id.NameClaimType, id.RoleClaimType);
if (ci.Name.Equals("name"))
{
ci.AddClaim(new Claim("permission", "readOnly"));
}
else
{
ci.AddClaim(new Claim("permission", "write"));
}
var cp = new ClaimsPrincipal(ci);
return Task.FromResult(cp);
}
}
Add to Startup.cs(.net Core 2.0) :
services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
Set your policy :
services.AddAuthorization(options =>
{
options.AddPolicy("Readonly", policy =>
policy.RequireClaim("permission", "readOnly"));
options.AddPolicy("Write", policy =>
policy.RequireClaim("permission", "write"));
});
Restrict access to a controller or action by requiring this policy:
[Authorize(Policy = "Write")]
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
If you have already add groups(write,readonly) in your AD and add the related users to group , you can also check the groups :
public static class Security
{
public static bool IsInGroup(this ClaimsPrincipal User, string GroupName)
{
var groups = new List<string>();
var wi = (WindowsIdentity)User.Identity;
if (wi.Groups != null)
{
foreach (var group in wi.Groups)
{
try
{
groups.Add(group.Translate(typeof(NTAccount)).ToString());
}
catch (Exception)
{
// ignored
}
}
return groups.Contains(GroupName);
}
return false;
}
}
And use like :
if (User.IsInGroup("GroupName"))
{
}
I am identity 2.1.2 with asp.net core 2.0, I have application claim table which have claim type and claim value
i.e Assets ,Assets Edit,Assets, Assets View, where claim types are same with distinct claim values and I am creating policies using claim type name which is working fine for me no clue about how to add multiple policies in one action. Below code is being used in startup file to create policies.
services.AddAuthorization(options =>
{
var dbContext = SqlServerDbContextOptionsExtensions.UseSqlServer(new DbContextOptionsBuilder<MyDBContext>(),
Configuration.GetConnectionString("TestIdentityClaimAuth")).Options;
var dbCon = new MyDBContext(dbContext);
//Getting the list of application claims.
var applicationClaims = dbCon.ApplicationClaims.ToList();
var strClaimValues = string.Empty;
List<ClaimVM> lstClaimTypeVM = new List<ClaimVM>();
IEnumerable<string> lstClaimValueVM = null;// new IEnumerable<string>();
lstClaimTypeVM = (from dbAppClaim
in dbCon.ApplicationClaims
select new ClaimVM
{
ClaimType = dbAppClaim.ClaimType
}).Distinct().ToList();
foreach (ClaimVM objClaimType in lstClaimTypeVM)
{
lstClaimValueVM = (from dbClaimValues in dbCon.ApplicationClaims
where dbClaimValues.ClaimType == objClaimType.ClaimType
select dbClaimValues.ClaimValue).ToList();
options.AddPolicy(objClaimType.ClaimType, policy => policy.RequireClaim(objClaimType.ClaimType, lstClaimValueVM));
lstClaimValueVM = null;
}
});
And in my controller using the Autherize attribute like this.
[Authorize(Policy = "Assets Edit")]
Please shade some light on it thanks in advance.
For multiple policys, you could implement your own AuthorizeAttribute.
MultiplePolicysAuthorizeAttribute
public class MultiplePolicysAuthorizeAttribute : TypeFilterAttribute
{
public MultiplePolicysAuthorizeAttribute(string policys, bool isAnd = false) : base(typeof(MultiplePolicysAuthorizeFilter))
{
Arguments = new object[] { policys, isAnd };
}
}
MultiplePolicysAuthorizeFilter
public class MultiplePolicysAuthorizeFilter : IAsyncAuthorizationFilter
{
private readonly IAuthorizationService _authorization;
public string Policys { get; private set; }
public bool IsAnd { get; private set; }
public MultiplePolicysAuthorizeFilter(string policys, bool isAnd, IAuthorizationService authorization)
{
Policys = policys;
IsAnd = isAnd;
_authorization = authorization;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var policys = Policys.Split(";").ToList();
if (IsAnd)
{
foreach (var policy in policys)
{
var authorized = await _authorization.AuthorizeAsync(context.HttpContext.User, policy);
if (!authorized.Succeeded)
{
context.Result = new ForbidResult();
return;
}
}
}
else
{
foreach (var policy in policys)
{
var authorized = await _authorization.AuthorizeAsync(context.HttpContext.User, policy);
if (authorized.Succeeded)
{
return;
}
}
context.Result = new ForbidResult();
return;
}
}
}
only require one of the policy
[MultiplePolicysAuthorize("Assets View;Assets Edit;Assets Delete")]
only require all the policys
[MultiplePolicysAuthorize("Assets View;Assets Edit;Assets Delete", true)]
If you simply want to apply multiple policies, you can do this:
[Authorize(Policy = "Asset")]
[Authorize(Policy = "Edit")]
public class MyController : Controller {
}
EDIT: to clarify, this is additive - you must pass both policy requirements.
You can use make multiple requirements class implementing IAuthorizationRequirement, and register to the DI container the multiple requirements handlers of AuthorizationHandler.
So you can simply add them to your Policy using the AddRequirement inside AuthorizationPolicyBuilder
public AuthorizationPolicyBuilder AddRequirements(params IAuthorizationRequirement[] requirements);
Startup.cs:
services.AddScoped<IAuthorizationHandler, FooHandler>();
services.AddScoped<IAuthorizationHandler, BooHandler>();
services.AddAuthorization(authorizationOptions =>
{
authorizationOptions.AddPolicy(
"FooAndBooPolicy",
policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.AddRequirements(new FooRequirement(), new BooRequirement());
});
});
Requirements.cs:
public class FooRequirement : IAuthorizationRequirement { }
public class FooHandler : AuthorizationHandler<FooRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationContext context, FooRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == "Foo" && c.Value == true))
{
context.Succeed(requirement);
return Task.FromResult(0);
}
}
}
public class BooRequirement : IAuthorizationRequirement { }
public class BooHandler : AuthorizationHandler<BooRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationContext context, BooRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == "Boo" && c.Value == true))
{
context.Succeed(requirement);
return Task.FromResult(0);
}
}
}
Currently, I am authenticating users in my application using role based authentication with OAuth and WebApi. I've set this up like so:
public override async Task GrantResourceOwnerCredentials (OAuthGrantResourceOwnerCredentialsContext context)
{
var user = await AuthRepository.FindUser(context.UserName, context.Password);
if (user === null)
{
context.SetError("invalid_grant", "The username or password is incorrect");
return;
}
var id = new ClaimsIdentity(context.Options.AuthenticationType);
id.AddClaim(New Claim(ClaimTypes.Name, context.UserName));
foreach (UserRole userRole in user.UserRoles)
{
id.AddClaim(new Claim(ClaimTypes.Role, userRole.Role.Name));
}
context.Validated(id);
}
Protecting my API routes with the <Authorize> tag.
I've since, however, run into an issue where my users can hold different roles for different clients. For example:
User A can be associated to multiple clients: Client A and Client B.
User A can have different "roles" when accessing information from either client. So User A may be an Admin for Client A and a basic User for Client B.
Which means, the following example:
[Authorize(Roles = "Admin")]
[Route("api/clients/{clientId}/billingInformation")]
public IHttpActionResult GetBillingInformation(int clientId)
{
...
}
User A may access billing information for Client A, but not for Client B.
Obviously, what I have now won't work for this type of authentication. What would be the best way to set up Client specific Role based authentication? Can I simply change up what I have now, or would I have to set it up a different way entirely?
You could remove the authorize tag and do the role validation inside the function instead.
Lambda solution:
Are there roles that are added based on CustomerID and UserID?
If so you could do something like the example below where you get the customer based of the values you have and then return the response.
string userID = RequestContext.Principal.Identity.GetUserId();
var customer = Customer.WHERE(x => x.UserID == userID && x.clientId == clientId && x.Roles == '1')
Can you provide us with abit more information about what you use to store the connection/role between the Customer and User.
EDIT:
Here is an example on how you could use the ActionFilterAttribute. It gets the CustomerId from the request and then takes the UserId of the identity from the request. So you can replace [Authorize] with [UserAuthorizeAttribute]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class UserAuthorizeAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
try
{
var authHeader = actionContext.Request.Headers.GetValues("Authorization").First();
if (string.IsNullOrEmpty(authHeader))
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
Content = new StringContent("Missing Authorization-Token")
};
return;
}
ClaimsPrincipal claimPrincipal = actionContext.Request.GetRequestContext().Principal as ClaimsPrincipal;
if (!IsAuthoticationvalid(claimPrincipal))
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
Content = new StringContent("Invalid Authorization-Token")
};
return;
}
if (!IsUserValid(claimPrincipal))
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
Content = new StringContent("Invalid User name or Password")
};
return;
}
//Finally role has perpession to access the particular function
if (!IsAuthorizationValid(actionContext, claimPrincipal))
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
Content = new StringContent("Permission Denied")
};
return;
}
}
catch (Exception ex)
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
Content = new StringContent("Missing Authorization-Token")
};
return;
}
try
{
//AuthorizedUserRepository.GetUsers().First(x => x.Name == RSAClass.Decrypt(token));
base.OnActionExecuting(actionContext);
}
catch (Exception)
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
{
Content = new StringContent("Unauthorized User")
};
return;
}
}
private bool IsAuthoticationvalid(ClaimsPrincipal claimPrincipal)
{
if (claimPrincipal.Identity.AuthenticationType.ToLower() == "bearer"
&& claimPrincipal.Identity.IsAuthenticated)
{
return true;
}
return false;
}
private bool IsUserValid(ClaimsPrincipal claimPrincipal)
{
string userID = claimPrincipal.Identity.GetUserId();
var securityStamp = claimPrincipal.Claims.Where(c => c.Type.Equals("AspNet.Identity.SecurityStamp", StringComparison.OrdinalIgnoreCase)).Single().Value;
var user = _context.AspNetUsers.Where(x => x.userID.Equals(userID, StringComparison.OrdinalIgnoreCase)
&& x.SecurityStamp.Equals(securityStamp, StringComparison.OrdinalIgnoreCase));
if (user != null)
{
return true;
}
return false;
}
private bool IsAuthorizationValid(HttpActionContext actionContext, ClaimsPrincipal claimPrincipal)
{
string userId = claimPrincipal.Identity.GetUserId();
string customerId = (string)actionContext.ActionArguments["CustomerId"];
return AllowedToView(userId, customerId);
}
private bool AllowedToView(string userId, string customerId)
{
var customer = _context.WHERE(x => x.UserId == userId && x.CustomerId == customerId && x.RoleId == '1')
return false;
}
}
Personally I think you need to move away from using the [Authorize] attribute entirely. It's clear that your requirements for authorisation are more complex than that method "out-the-box" was intended for.
Also in the question about I think Authentication and Authorisation are being used interchangably. What we are dealing with here is Authorisation.
Since you are using Identity and claims based authorisation. I would look at doing this "on-the-fly" so to speak. Along with claims you could make use of dynamic policy generation as well as service based Authorisation using IAuthorizationRequirement instances to build up complex rules and requirements.
Going into depth on the implementation of this is a big topic but there are some very good resources available. The original approach (that I have used myself) was orginally detailed by Dom and Brock of IdentityServer fame.
They did a comprehensive video presentation on this at NDC last year which you can watch here.
Based closely on the concepts discussed in this video Jerrie Pelser blogged about a close implementation which you can read here.
The general components are:
The [Authorize] attributes would be replaced by policy generator such as:
public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
private readonly IConfiguration _configuration;
public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options, IConfiguration configuration) : base(options)
{
_configuration = configuration;
}
public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
// Check static policies first
var policy = await base.GetPolicyAsync(policyName);
if (policy == null)
{
policy = new AuthorizationPolicyBuilder()
.AddRequirements(new HasScopeRequirement(policyName, $"https://{_configuration["Auth0:Domain"]}/"))
.Build();
}
return policy;
}
}
And then you would author any instances of IAuthorizationRequirement required to ensure users are authroised properly, an example of that would be something like:
public class HasScopeRequirement : IAuthorizationRequirement
{
public string Issuer { get; }
public string Scope { get; }
public HasScopeRequirement(string scope, string issuer)
{
Scope = scope ?? throw new ArgumentNullException(nameof(scope));
Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer));
}
}
Dom and Brock then also detail a client implementation that ties all of this together which might look something like this:
public class AuthorisationProviderClient : IAuthorisationProviderClient
{
private readonly UserManager<ApplicationUser> userManager;
private readonly RoleManager<IdentityRole> roleManager;
public AuthorisationProviderClient(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager)
{
this.userManager = userManager;
this.roleManager = roleManager;
}
public async Task<bool> IsInRole(ClaimsPrincipal user, string role)
{
var appUser = await GetApplicationUser(user);
return await userManager.IsInRoleAsync(appUser, role);
}
public async Task<List<Claim>> GetAuthorisationsForUser(ClaimsPrincipal user)
{
List<Claim> claims = new List<Claim>();
var appUser = await GetApplicationUser(user);
var roles = await userManager.GetRolesAsync(appUser);
foreach (var role in roles)
{
var idrole = await roleManager.FindByNameAsync(role);
var roleClaims = await roleManager.GetClaimsAsync(idrole);
claims.AddRange(roleClaims);
}
return claims;
}
public async Task<bool> HasClaim(ClaimsPrincipal user, string claimValue)
{
Claim required = null;
var appUser = await GetApplicationUser(user);
var userRoles = await userManager.GetRolesAsync(appUser);
foreach (var userRole in userRoles)
{
var identityRole = await roleManager.FindByNameAsync(userRole);
// this only checks the AspNetRoleClaims table
var roleClaims = await roleManager.GetClaimsAsync(identityRole);
required = roleClaims.FirstOrDefault(x => x.Value == claimValue);
if (required != null)
{
break;
}
}
if (required == null)
{
// this only checks the AspNetUserClaims table
var userClaims = await userManager.GetClaimsAsync(appUser);
required = userClaims.FirstOrDefault(x => x.Value == claimValue);
}
return required != null;
}
private async Task<ApplicationUser> GetApplicationUser(ClaimsPrincipal user)
{
return await userManager.GetUserAsync(user);
}
}
Whilst this implementation doesn't address your exact requirements (which would be hard to do anyway), this is almost certainly the approach that I would adopt given the scenario you illustrated in the question.
One solution would be to add the clients/user relationship as part of the ClaimsIdentity, and then check that with a derived AuthorizeAttribute.
You would extend the User object with a Dictionary containing all their roles and the clients that they are authorized for in that role - presumably contained in your db:
public Dictionary<string, List<int>> ClientRoles { get; set; }
In your GrantResourceOwnerCredentials method, you would add these as individual Claims with the Client Ids as the value:
foreach (var userClientRole in user.ClientRoles)
{
oAuthIdentity.AddClaim(new Claim(userClientRole.Key,
string.Join("|", userClientRole.Value)));
}
And then create a custom attribute to handle reading the claims value. The slightly tricky part here is getting the clientId value. You have given one example where it is in the route, but that may not be consistent within your application. You could consider passing it explicitly in a header, or derive whatever URL / Route parsing function works in all required circumstances.
public class AuthorizeForCustomer : System.Web.Http.AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
var isAuthorized = base.IsAuthorized(actionContext);
string clientId = ""; //Get client ID from actionContext.Request;
var user = actionContext.ControllerContext.RequestContext.Principal as ClaimsPrincipal;
var claim = user.FindFirst(this.Roles);
var clientIds = claim.Value.Split('|');
return isAuthorized && clientIds.Contains(clientId);
}
}
And you would just swap
[Authorize(Roles = "Admin")] for [AuthorizeForCustomer(Roles = "Admin")]
Note that this simple example would only work with a single role, but you get the idea.
The requirement is about having users with different authorizations. Don't feel oblige to strictly match a user permissions/authorizations with his roles. Roles are part of user identity and should not depend from client.
I will suggest to decompose the requirement:
Only Admin user can access Billing domain/subsystem
//Role-based authorization
[Authorize(Roles = "Admin")]
public class BillingController {
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-2.1
User A may access billing information for Client A, but not for Client B.
Attribute could not work here because we need at least to load the concerned client.
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-2.1&tabs=aspnetcore2x
There is not a built-in way to solve this. You have to define your own merchant access right configuration system.
It might be a simple many to many table (1 user can access N merchants, 1 merchant can be accessed by N users)
public IHttpActionResult GetBillingInformation(int clientId)
{
var merchant = clientRepository.Get(clientId);
if(!UserIsConfiguredToAccessMerchant(User, merchant))
return Unauthorized();
}
NB: Claims should contain only user identity data (name, email, roles, ...). Adding authorizations, access rights claims in the token is not a good choice in my opion:
The token size might increase drastically
The user might have different authorizations regarding the domain
context or the micro service
Below some usefuls links:
https://learn.microsoft.com/en-us/dotnet/framework/security/claims-based-identity-model
https://leastprivilege.com/2016/12/16/identity-vs-permissions/
https://leastprivilege.com/2014/06/24/resourceaction-based-authorization-for-owin-and-mvc-and-web-api/