Does IdentityServer4 still allow ResourceAuthorize? - asp.net-core

I'm looking at upgrading from IdentityServer3 to IdentityServer4, specifically because we're upgrading existing projects from .NET 4.5 to .NET Core 3.1.
The biggest issue I see right now is that we use the ResourceAuthorize attribute to check if the user has permission against a resource
[ResourceAuthorize("Read","urn://someresource")]
But looking through the ID4 documentation and the code base, it doesn't look like ResourceAuthorize exists. The documentation does show examples of using Authorize, but I'm not seeing anything that lets me check for a permission against a resource.
Has the paradigm changed or is there another way to get this type of check done with ID4?

You can add policies:
Startup.cs
services.AddAuthorization(authorizationOptions =>
{
authorizationOptions.AddPolicy(
"SomePolicy",
policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.AddRequirements(
new SomePolicyRequirement());
});
});
SomePolicyRequirement.cs
public class SomePolicyRequirement : IAuthorizationRequirement
{
public SomePolicyRequirement()
{
}
}
SomePolicyHandler.cs
public class SomePolicyHandler : AuthorizationHandler<SomePolicyRequirement>
{
public SomePolicyHandler()
{
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SomePolicyRequirement requirement)
{
var endpoint = context.Resource as Endpoint;
if (endpoint == null)
{
context.Fail();
return Task.CompletedTask;
}
/*
//RouteData can be controller, action or id
var imageId = filterContext.RouteData.Values["id"].ToString();
if (!Guid.TryParse(imageId, out Guid imageIdAsGuid))
{
context.Fail();
return Task.CompletedTask;
}*/
/*
//Repository check can go here
var ownerId = context.User.Claims.FirstOrDefault(c => c.Type == "sub").Value;
if (!_someRepository.IsImageOwner(imageIdAsGuid, ownerId))
{
context.Fail();
return Task.CompletedTask;
}*/
// all checks out
context.Succeed(requirement);
return Task.CompletedTask;
}
}

Dotnet core added some great features for authorization. Resource base authorization can implemented very easy with policy based authorization.
Policy-based authorization

Related

Authorization: How to handle mutiple (dozen or more) requirements

I have a set of tables in our database with users, permissions, and a join that maps which users have what permissions.
Looking at the docs, the following is an example of how policies and the requirement(s) are set up on Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
}
And here is an example of a handler for multiple requirements:
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;
public class PermissionHandler : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
var pendingRequirements = context.PendingRequirements.ToList();
foreach (var requirement in pendingRequirements)
{
if (requirement is ReadPermission)
{
if (IsOwner(context.User, context.Resource) ||
IsSponsor(context.User, context.Resource))
{
context.Succeed(requirement);
}
}
else if (requirement is EditPermission ||
requirement is DeletePermission)
{
if (IsOwner(context.User, context.Resource))
{
context.Succeed(requirement);
}
}
}
//TODO: Use the following if targeting a version of
//.NET Framework older than 4.6:
// return Task.FromResult(0);
return Task.CompletedTask;
}
private bool IsOwner(ClaimsPrincipal user, object resource)
{
// Code omitted for brevity
return true;
}
private bool IsSponsor(ClaimsPrincipal user, object resource)
{
// Code omitted for brevity
return true;
}
}
My intention is to check my database tables within the handler to validate that the user has a setting that corresponds to the policy. To check if a user can upload files, the policy might look like this:
services.AddAuthorization(config =>
{
config.AddPolicy("CanUploadFiles", policy => policy.Requirements.Add(new CanDoRequirement("CanUploadFiles")));
});
Using an [Authorize] attribute for a given policy, I can check that within the handler. I have that much working.
Question: Given that I might have 10-20 separate "CanDo…" permissions in our table, is there a better way to set these up rather than have separate lines in AddAuthorization()?
Well, I'm not aware of any shortcuts when configuring the 20-ish requirements and policies that would remove the separate lines in startup, but you could consider implementing a sort of custom resource based authorization rather than a policy based one, policy-based being a declarative one. Declarative meaning the policy is pre-configured. Like so: [Authorize("policy")].
By using imperative authorization, rather than declarative, you would remove the need for x amount of policies to be configured. Instead of saying "Authorize this method", you let the framework take care of the authorization itself.
Consider the following requirements
A user must be authenticated.
That user can only upload a file if they satisfy the CanUploadFiles which is a boolean on the user's record in the database.
Now consider the following example
You have created your own ICustomAuthorizationHandler, somewhat similar to the the ASP.NET Core's IAuthorizationHandler, with the exception that you won't be satisfying a policy, but instead you will feed it a 'CanDoPermission' and it will return true or false if that user has that specific 'flag'.
public class FileController : Controller
{
private ICustomAuthorizationService _authService
public FileController(ICustomAuthorizationService authService)
{
_authService = authService;
}
[Authorize]
public async Task<IActionResult> Upload(IFormFile file)
{
var authResult = await _authService.AuthorizeAsync(User, "CanDoUpload");
if (!authResult.Succeeded)
{
return new ForbidResult();
}
// Process upload
return View();
}
}
This way, there wouldn't have to be policies nor requirements configured for checking if the user can upload a file. But, you would need to take care of a lot of the stuff that you get for 'free' by simply going for policies and configuring them in AddAuthorization.

Authorization in .NET Core 3: Order in which requirements are being executed

I am imposing different policies to be satisfied in order to execute certain actions and I'd like to decide the order in which these policies have to be exectued. If a user is not authenticated, I should not check if it has permissions or not.
In startup I have:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers()
.RequireAuthorization("UserIsAuthenticated")
.RequireAuthorization("UserIsRegistered");
});
and then I use the default [Authorize(Roles = "Administrator")] attribute and my [MyAuthorize] attributes.
The order in which these polices should be executed is:
First, UserIsAuthenticated, to check if the user is authenticated.
If they are, it should check UserIsRegistered.
Finally the attributes should be applied.
In my case order matters, because I have
services.AddAuthorization(options => { options.InvokeHandlersAfterFailure = false; });
(if a user is not authenticated, I can't check their claims and it makes no sense to check the following policies).
However, in some cases I've seen that the attributes are being evaluated before the Authentication policies.
Is there a way to impose the order of the requirements?
You could create a custom AuthorizeMultiplePolicyFilter to check these policies manually.Apply the filter to global then it will be excuted before action/controller filters.
public class AuthorizeMultiplePolicyFilter: IAsyncAuthorizationFilter
{
private IAuthorizationService _authorization;
private string[] _policies;
public AuthorizeMultiplePolicyFilter(string[] policies)
{
_policies = policies;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (context.HttpContext.Request.Path.StartsWithSegments("/Account"))
{
return;
}
_authorization = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
foreach (var policy in _policies)
{
var authorized = await _authorization.AuthorizeAsync(context.HttpContext.User, policy);
if (!authorized.Succeeded)
{
if(policy == "UserIsAuthenticated")
{
context.Result = new RedirectResult("/Account/Login");
}
if(policy == "UserIsRegistered")
{
context.Result = new ForbidResult();
}
return;
}
}
}
}
Startup.cs
services.AddControllersWithViews(options =>
options.Filters.Add(new AuthorizeMultiplePolicyFilter(new string[] { "UserIsAuthenticated", "UserIsRegistered" }))
);

Multiple Access Denied Pages

I'm creating an application that has two different authorization scenarios: admin and site.
If you try to access a /admin route without the policy succeeding the user should be redirected to an access denied page. At this point there's no action the user can take. They can't access the resource and there's nothing for them to do.
If you try to access a /site/joes-super-awesome-site route without the policy suceeding the user should be redirected to a different access denied. At this point the user should be able to request access. There is an action they can take.
What's the best way to achieve this? I know I can override the default OnRedirectToAccessDenied action but that will require some ugly string parsing (untested example below).
.AddCookie(options => {
options.Events.OnRedirectToAccessDenied = context => {
// parsing this kinda sucks.
var pathParts = context.Request.Path.Value.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (pathParts?[0] == "site") {
context.Response.Redirect($"/{pathParts[0]}/request-access");
} else {
context.Response.Redirect("/account/access-denied");
}
return Task.CompletedTask;
};
})
Doing some searching, I found the following information:
Someone with the same question on this GitHub issue
Tracking of authorization-related improvements in this GitHub issue
Unfortunately these improvements didn't make it to ASP.NET Core 2.1.
It seems that at this point, another option (apart from your suggestion of parsing the request URL) is to imperatively invoke the authorization service in your MVC actions.
It could go from:
// Highly imaginary current implementation
public class ImaginaryController : Controller
{
[HttpGet("site/{siteName}")]
[Authorize("SitePolicy")]
public IActionResult Site(string siteName)
{
return View();
}
[HttpGet("admin")]
[Authorize("AdminPolicy")]
public IActionResult Admin()
{
return View();
}
}
to:
public class ImaginaryController : Controller
{
private readonly IAuthorizationService _authorization;
public ImaginaryController(IAuthorizationService authorization)
{
_authorization = authorization;
}
[HttpGet("site/{siteName}")]
public Task<IActionResult> Site(string siteName)
{
var sitePolicyAuthorizationResult = await _authorization.AuthorizeAsync(User, "SitePolicy");
if (!sitePolicyAuthorizationResult.Success)
{
return Redirect($"/site/{siteName}/request-access");
}
return View();
}
[HttpGet("admin")]
public Task<IActionResult> Admin()
{
var adminPolicyAuthorizationResult = await _authorization.AuthorizeAsync(User, "AdminPolicy");
if (!adminPolicyAuthorizationResult.Success)
{
return Redirect("/account/access-denied");
}
return View();
}
}

ASp.net core 2 Identity app sometimes looses claim when kept logged in

I have an authorization handler, which is meant to restrict users to accessing info only for the organization they belong to, or ANY if they are of a certain account type (known as SuperAdmin here). Aforementioned account type may or may not have an associated Organization, which happens to be a navigation property. It should be mentioned I am working with "user" classes extended from ApplicationUser in the Identity framework.
Here's the handler:
public class OrganizationHandler : AuthorizationHandler<SameOrgRequirment,
Organization>
{
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
SameOrgRequirment requirement, Organization resource)
{
var value = context.User.FindFirst( "OrganizationID").Value;
long claimValue = long.Parse(value);
if(claimValue==resource.Id)
{
context.Succeed(requirement);
}
else
{
string orgClaimValue = context.User.FindFirst(c => c.Type ==
"AccountTypes").Value;
AccountTypes claimAsType =
(AccountTypes)Enum.Parse(typeof(AccountTypes), orgClaimValue);
if(claimAsType == AccountTypes.SuperAdmin)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
public class SameOrgRequirment:IAuthorizationRequirement{}
The problem is I am SOMETIMES getting a NullReferenceException on the line
var value = context.User.FindFirst( "OrganizationID").Value;
which makes no sense as all users in my dev setup have an organizationId navigation property. Inspecting the ClaimsPrincipal I see the AccountTypes claim (see below) exists but the other, OrganizationId does not when this NullReferenceException is thrown.
I have overridden the AppClaimsPrincipalFactory to create these claims:
public class AppClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, ApplicationRole>
{
public AppClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager
, RoleManager<ApplicationRole> roleManager
, IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager, optionsAccessor)
{ }
public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
var principal = await base.CreateAsync(user);
if (!string.IsNullOrWhiteSpace(user.Type.ToString()))
{
((ClaimsIdentity)principal.Identity).AddClaims(new[]
{
new Claim("AccountTypes", user.Type.ToString())
});
}
try
{
if (!string.IsNullOrWhiteSpace(user.Organization.Id.ToString()))
{
//Used later to restrict data
((ClaimsIdentity)principal.Identity).AddClaims(new[]
{
new Claim("OrganizationId", user.Organization.Id.ToString()),
});
}
}
catch(NullReferenceException ex)
{
((ClaimsIdentity)principal.Identity).AddClaims(new[]
{
new Claim("OrganizationId", "-1"),
});
}
return principal;
}
}
The AccountTypes claim doesnt seem to get lost.
The only thing I have narrowed down is that this only seems to happen for an account where Remember Me? is checked and the app is closed and reopened via visual studio. If I log out and log back with the same account, it works fine! All claims are there and the handler works correctly. Being able to stay logged is a customer requirement so I can't really change that.

OpenIdConnect access_token size and accessing claims server side

I am trying to wrap my head around several concepts here but I don't want this question to be too broad - basically what we are trying to do is use role claims as permissions to lock down our API but I am finding that the access_token is becoming too big.
We are using OpenIddict and ASP.NET Identity 3 on the server side. We have implemented the default AspNetRoleClaims table to store our claims for each role - using them as permissions.
We lock down our API endpoints using custom policy based claims authorization as shown here:
Custom Policy Based Authorization
The main issue I am finding is that our access_token containing our claims is becoming very large. We are attempting to make the ClaimType and Value to be very small in the database to make the claims footprint smaller. We have a basic CRUD type permission scheme, so for each "module" or screen in our SPA client app, there are 4 permissions. The more modules we add to our application, the more the claims are growing in the access_token and our Authorization Bearer header is becoming very large. I am worried about this becoming not very scalable as the app grows.
So the claims are embedded in the access_token and when I hit my endpoint that is locked down with a custom Policy like this...
[Authorize(Policy="MyModuleCanRead")]
[HttpGet]
public IEnumerable<MyViewModel> Get()
I can then access my ASP.NET Identity User and User.Claims in the AuthorizationHandler.
Sorry in advance if this is an obvious question - but I am wondering - in order to get the Custom Policy Based Authorization to work - does it absolutely require the claims to be in either the id_token or the access_token in order to call the handler?
If I remove the claims from the access_token, then my AuthorizationHandler code does not get hit and I cannot access my endpoint that is locked down with my custom Policy.
I am wondering if it is possible to use a custom claims policy but have the actual code that checks for the Claims inside the Authorization handler, so that the claims are not passed with each HTTP request, but are fetched server side from the Authorization cookie or from the database.
* UPDATE *
Pintpoint's answer using Authorization handlers along with the comment on how to remove additional role claims from the cookie achieved just what I was looking for.
In case this helps anyone else - here is the code to override the UserClaimsPrincipalFactory and prevent the role claims from being written to the cookie. (I had many role claims as permissions and the cookie(s) and request headers were becoming too large)
public class AppClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
public AppClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
{
}
public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var userId = await UserManager.GetUserIdAsync(user);
var userName = await UserManager.GetUserNameAsync(user);
var id = new ClaimsIdentity(Options.Cookies.ApplicationCookieAuthenticationScheme,
Options.ClaimsIdentity.UserNameClaimType,
Options.ClaimsIdentity.RoleClaimType);
id.AddClaim(new Claim(Options.ClaimsIdentity.UserIdClaimType, userId));
id.AddClaim(new Claim(Options.ClaimsIdentity.UserNameClaimType, userName));
if (UserManager.SupportsUserSecurityStamp)
{
id.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType,
await UserManager.GetSecurityStampAsync(user)));
}
// code removed that adds the role claims
if (UserManager.SupportsUserClaim)
{
id.AddClaims(await UserManager.GetClaimsAsync(user));
}
return new ClaimsPrincipal(id);
}
}
I am wondering if it is possible to use a custom claims policy but have the actual code that checks for the Claims inside the Authorization handler, so that the claims are not passed with each HTTP request, but are fetched server side from the Authorization cookie or from the database.
It's definitely possible. Here's how you could do that:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("Has-Edit-User-Profiles-Permission", builder =>
{
builder.RequirePermission("Edit-User-Profiles");
});
});
}
}
public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
public PermissionAuthorizationRequirement(string permission)
{
if (string.IsNullOrEmpty(permission))
{
throw new ArgumentException("The permission cannot be null or empty.", nameof(permission));
}
Permission = permission;
}
public string Permission { get; set; }
}
public class PermissionAuthorizationHandler :
AuthorizationHandler<PermissionAuthorizationRequirement>
{
private readonly UserManager<ApplicationUser> _userManager;
public PermissionAuthorizationHandler(UserManager<ApplicationUser> userManager)
{
if (userManager == null)
{
throw new ArgumentNullException(nameof(userManager));
}
_userManager = userManager;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionAuthorizationRequirement requirement)
{
if (context.User == null)
{
return;
}
var user = await _userManager.GetUserAsync(context.User);
if (user == null)
{
return;
}
// Use whatever API you need to ensure the user has the requested permission.
if (await _userManager.IsInRoleAsync(user, requirement.Permission))
{
context.Succeed(requirement);
}
}
}
public static class PermissionAuthorizationExtensions
{
public static AuthorizationPolicyBuilder RequirePermission(
this AuthorizationPolicyBuilder builder, string permission)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrEmpty(permission))
{
throw new ArgumentException("The permission cannot be null or empty.", nameof(permission));
}
return builder.AddRequirements(new PermissionAuthorizationRequirement(permission));
}
}