Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save VictorioBerra/8c333a228c55d86a7c15f7f300284634 to your computer and use it in GitHub Desktop.
Save VictorioBerra/8c333a228c55d86a7c15f7f300284634 to your computer and use it in GitHub Desktop.
Custom DefaultAuthorizationPolicyProvider which can distinguish between a token with and without a subject, and then handle requirements accordingly.
//
// This is a combination of two things:
//
// A slight alteration of the implementation of DefaultAuthorizationPolicyProvider that creates a new PermissionRequirement similar to the PolicyServer's for later use with IPolicyServerRuntimeClient in a requirement handler.
// But only if theres a subject claim.
//
// A slight alteration of the ClaimsAuthorizationRequirement.
// Works identically to the default implemenation, but only if theres no subject claim.
//
using IdentityModel;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.Extensions.Options;
using PolicyServer.Runtime.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WuitMyApp.API.Options;
namespace WuitMyApp.API.Authorization
{
/// <summary>
/// Authorization policy provider to automatically turn all permissions of a user into a ASP.NET Core authorization policy
/// Has additional functionality for pulling out a scope map for Client Credentials auth. <see href="https://github.com/PolicyServer/PolicyServer.Local"/>
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Authorization.DefaultAuthorizationPolicyProvider" />
public class MyAppAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
private readonly IOptions<IdentityServerOptions> identityServerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="AuthorizationPolicyProvider"/> class.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="identityServerOptions"></param>
public MyAppAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options, IOptions<IdentityServerOptions> identityServerOptions) : base(options)
{
this.identityServerOptions = identityServerOptions;
}
/// <summary>
/// Gets a <see cref="T:Microsoft.AspNetCore.Authorization.AuthorizationPolicy" /> from the given <paramref name="policyName" />
/// </summary>
/// <param name="policyName">The policy name to retrieve.</param>
/// <returns>
/// The named <see cref="T:Microsoft.AspNetCore.Authorization.AuthorizationPolicy" />.
/// </returns>
public async override Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
// check static policies first
var policy = await base.GetPolicyAsync(policyName);
if (policy == null)
{
var authzPolicyBuilder = new AuthorizationPolicyBuilder()
.AddRequirements(new PermissionRequirement(policyName));
// This is our permission to scope mapper. With scope based access (client credentials), regular access to the API means you can read and write inventory, x.admin access means full write all.
// These are handled be the requirement handlers
switch (policyName)
{
case "ReadAll":
case "WriteInventory":
authzPolicyBuilder.AddRequirements(new MyAppClaimsAuthorizationRequirement(JwtClaimTypes.Scope, identityServerOptions.Value.ProtectedResourceId, identityServerOptions.Value.ProtectedResourceId + ".admin"));
break;
case "WriteAll":
// Cant use this otherwise the default handler will kick in, even for tokens without a sub.
// authzPolicyBuilder.RequireScope();
authzPolicyBuilder.AddRequirements(new MyAppClaimsAuthorizationRequirement(JwtClaimTypes.Scope, identityServerOptions.Value.ProtectedResourceId + ".admin"));
break;
}
policy = authzPolicyBuilder.Build();
}
return policy;
}
}
/// <summary>
/// Borrowed from PolicyServer.io
/// This mirrors the internal PermissionRequirement from PolicyServer, we create these in a custom policy provider, and then pass them to IPolicyServerRuntimeClient just like PolicyServer does.
/// </summary>
internal class PermissionRequirement : IAuthorizationRequirement
{
public PermissionRequirement(string name)
{
Name = name;
}
public string Name { get; private set; }
}
internal class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly IPolicyServerRuntimeClient _client;
public PermissionHandler(IPolicyServerRuntimeClient client)
{
_client = client;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
// Only run the following if there IS a subject.
if (context.User.Claims.Any(x => x.Type == JwtClaimTypes.Subject))
{
if (await _client.HasPermissionAsync(context.User, requirement.Name))
{
context.Succeed(requirement);
}
}
else
{
// Token is a client credentials token, so we pass these permission requiremenets because the identity token still must fall back onto the PermissionPolicyHandler to do its job.
context.Succeed(requirement);
}
}
}
internal class MyAppClaimsAuthorizationRequirementHandler : AuthorizationHandler<MyAppClaimsAuthorizationRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MyAppClaimsAuthorizationRequirement requirement)
{
// Only do the following for Client Credentials Clients
if (context.User.Claims.Any(x => x.Type != JwtClaimTypes.Subject))
{
// This is basically copy and paste from ClaimsAuthorizationRequirement in AspNetCore
if (context.User != null)
{
var found = false;
if (requirement.AllowedValues == null || !requirement.AllowedValues.Any())
{
found = context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase));
}
else
{
found = context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase)
&& requirement.AllowedValues.Contains(c.Value, StringComparer.Ordinal));
}
if (found)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
} else
{
// Token is an identity token, so we pass these requiremenets because the identity token still must fall back onto the PermissionPolicyHandler to do its job.
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}
/// <summary>
/// Borrowed from <see cref="ClaimsAuthorizationRequirement"/> in Microsoft.AspNetCore.Authorization.Infrastructure
/// </summary>
internal class MyAppClaimsAuthorizationRequirement : IAuthorizationRequirement
{
/// <summary>
/// <see cref="ClaimsAuthorizationRequirement"/>
/// </summary>
/// <param name="claimType"></param>
/// <param name="allowedValues"></param>
public MyAppClaimsAuthorizationRequirement(string claimType, params string[] allowedValues)
{
if (claimType == null)
{
throw new ArgumentNullException("claimType");
}
ClaimType = claimType;
AllowedValues = allowedValues;
}
/// <summary>
/// <see cref="ClaimsAuthorizationRequirement.ClaimType"/>
/// </summary>
public string ClaimType
{
get;
}
/// <summary>
/// <see cref="ClaimsAuthorizationRequirement.AllowedValues"/>
/// </summary>
public IEnumerable<string> AllowedValues
{
get;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment