That’s indeed an approach worth considering too.
As an experiment, I’ve opted to try the enforcement using a policy-based approach using AspNetCore’s authorization pipeline and custom AuthorizationPolicy
requirements, where the requirement handlers use custom user identity claims mapped to URL route parameters. This is ITOPS infrastructure code to support authorization by group membership at the identity level.
The (view-model composition) AC that handles HTTP requests is in the same business service as the AC providing the saga so my assumption is that it’s okay to enforce the requirement upstream at the business service’s edge.
View-model ACs supply their own uniquely named (to the AC namespace) AuthorizationPolicy
and the policy is applied to the ACs endpoint routes using the familiar Authorize
attribute. These policies include an ITOPS implementation of IAuthorizationRequirement
called UserMustHaveContextGroupClaimRequirement
that merely informs the authorization pipeline of two things; GroupName
and ContextIdName
. So in my original example this would be “SellerOperator” and “SellerId” respectively. The policy is configured as part of the ACs DI setup phase, e.g. drop the AC into the host project and it’s good to go.
ITOPS supplies an AuthorizationHandler<TAuthorizationRequirement>
called UserMustHaveContextGroupClaimRequirementByHttpRouteHandler
that is responsible for the following:
- Firstly obtain an AspNetCore Endpoint Routing route Id from the current route where the route Id’s name is equal to ContextIdName value from the requirement.
- Check if we have a valid “Group” claim with a specific formatted value. Whilst a claim’s value can contain serialized data I opted for a simple “group-name:group-context-id” format.
In code this is:
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserMustHaveContextGroupClaimRequirement requirement)
{
var request = _httpContextAccessor.HttpContext?.Request;
var groupContextId = request?.GetRouteValue<Guid>(requirement.ContextIdName);
if (groupContextId != Guid.Empty &&
context.User?.HasClaim(c =>
c.Type == GroupClaimType &&
c.Value.Equals($"{requirement.GroupName}:{groupContextId}", StringComparison.OrdinalIgnoreCase) &&
c.Issuer == ClaimIssuer) == true)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
context.Fail();
return Task.CompletedTask;
}
So basically if the user doesn’t have the required claim they are sent back a HTTP 403 status via the normal authorization pipeline. This happens as fast as possible with no loading of remote storage, and happens early so doesn’t unnecessarily put a message on a queue that will be discarded.
Of course, this then poses the question of how to get these group name and context Id pairs of data into a user’s claims identity.
I’ve used IdentityServer4 for may years now so in this situation I’d provide a custom claims provider from ITOPS to include in IS4 for its use when building a list of required claims for an authenticated user. A separate ITOPS interface would support each business service adding their own group claims to the provider. Each business service manages their group membership data in what ever way they see fit.
If I come across any unforeseen issues with this approach I’ll add my observations here.