Instantly share code, notes, and snippets.
Last active
October 29, 2024 03:53
-
Star
(2)
2
You must be signed in to star a gist -
Fork
(2)
2
You must be signed in to fork a gist
-
Save captainsafia/309e408cb5e2c2e5acd44d2e19f7f88e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.AspNetCore.Authentication.JwtBearer; | |
using Microsoft.AspNetCore.Authorization; | |
using Microsoft.Extensions.Options; | |
using System.Globalization; | |
using System.Security.Claims; | |
using Microsoft.AspNetCore.Mvc; | |
var builder = WebApplication.CreateBuilder(); | |
builder.Services.AddAuthentication().AddJwtBearer(); | |
builder.Services.AddAuthorization(); | |
builder.Services.AddControllers(); | |
builder.Services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>(); | |
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeAuthorizationHandler>(); | |
builder.Services.AddControllers(); | |
var app = builder.Build(); | |
app.MapControllers(); | |
app.Run(); | |
[ApiController] | |
[Route("api/[controller]")] | |
public class GreetingsController : Controller | |
{ | |
[MinimumAgeAuthorize(16)] | |
[HttpGet("hello")] | |
public string Hello() => $"Hello {(HttpContext.User.Identity?.Name ?? "world")}!"; | |
} | |
class MinimumAgeAuthorizeAttribute : AuthorizeAttribute | |
{ | |
const string POLICY_PREFIX = "MinimumAge"; | |
public MinimumAgeAuthorizeAttribute(int age) => Age = age; | |
// Get or set the Age property by manipulating the underlying Policy property | |
public int Age | |
{ | |
get | |
{ | |
if (int.TryParse(Policy.Substring(POLICY_PREFIX.Length), out var age)) | |
{ | |
return age; | |
} | |
return default(int); | |
} | |
set | |
{ | |
Policy = $"{POLICY_PREFIX}{value.ToString()}"; | |
} | |
} | |
} | |
class MinimumAgePolicyProvider : IAuthorizationPolicyProvider | |
{ | |
const string POLICY_PREFIX = "MinimumAge"; | |
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; } | |
public MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options) | |
{ | |
// ASP.NET Core only uses one authorization policy provider, so if the custom implementation | |
// doesn't handle all policies (including default policies, etc.) it should fall back to an | |
// alternate provider. | |
// | |
// In this sample, a default authorization policy provider (constructed with options from the | |
// dependency injection container) is used if this custom provider isn't able to handle a given | |
// policy name. | |
// | |
// If a custom policy provider is able to handle all expected policy names then, of course, this | |
// fallback pattern is unnecessary. | |
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options); | |
} | |
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync(); | |
public Task<AuthorizationPolicy> GetFallbackPolicyAsync() => FallbackPolicyProvider.GetFallbackPolicyAsync(); | |
// Policies are looked up by string name, so expect 'parameters' (like age) | |
// to be embedded in the policy names. This is abstracted away from developers | |
// by the more strongly-typed attributes derived from AuthorizeAttribute | |
// (like [MinimumAgeAuthorize()] in this sample) | |
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName) | |
{ | |
if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase) && | |
int.TryParse(policyName.Substring(POLICY_PREFIX.Length), out var age)) | |
{ | |
var policy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme); | |
policy.AddRequirements(new MinimumAgeRequirement(age)); | |
return Task.FromResult(policy.Build()); | |
} | |
return Task.FromResult<AuthorizationPolicy>(null); | |
} | |
} | |
class MinimumAgeRequirement : IAuthorizationRequirement | |
{ | |
public int Age { get; private set; } | |
public MinimumAgeRequirement(int age) { Age = age; } | |
} | |
class MinimumAgeAuthorizationHandler : AuthorizationHandler<MinimumAgeRequirement> | |
{ | |
private readonly ILogger<MinimumAgeAuthorizationHandler> _logger; | |
public MinimumAgeAuthorizationHandler(ILogger<MinimumAgeAuthorizationHandler> logger) | |
{ | |
_logger = logger; | |
} | |
// Check whether a given MinimumAgeRequirement is satisfied or not for a particular context | |
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement) | |
{ | |
// Log as a warning so that it's very clear in sample output which authorization policies | |
// (and requirements/handlers) are in use | |
_logger.LogWarning("Evaluating authorization requirement for age >= {age}", requirement.Age); | |
// Check the user's age | |
var dateOfBirthClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth); | |
if (dateOfBirthClaim != null) | |
{ | |
// If the user has a date of birth claim, check their age | |
var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value, CultureInfo.InvariantCulture); | |
var age = DateTime.Now.Year - dateOfBirth.Year; | |
if (dateOfBirth > DateTime.Now.AddYears(-age)) | |
{ | |
// Adjust age if the user hasn't had a birthday yet this year | |
age--; | |
} | |
// If the user meets the age criterion, mark the authorization requirement succeeded | |
if (age >= requirement.Age) | |
{ | |
_logger.LogInformation("Minimum age authorization requirement {age} satisfied", requirement.Age); | |
context.Succeed(requirement); | |
} | |
else | |
{ | |
_logger.LogInformation("Current user's DateOfBirth claim ({dateOfBirth}) does not satisfy the minimum age authorization requirement {age}", | |
dateOfBirthClaim.Value, | |
requirement.Age); | |
} | |
} | |
else | |
{ | |
_logger.LogInformation("No DateOfBirth claim present"); | |
} | |
return Task.CompletedTask; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment