diff --git a/samples/AutoAuthorization/Controllers/ApiController.cs b/samples/AutoAuthorization/Controllers/ApiController.cs index 492cac0a..a6fcf052 100644 --- a/samples/AutoAuthorization/Controllers/ApiController.cs +++ b/samples/AutoAuthorization/Controllers/ApiController.cs @@ -1,4 +1,6 @@ namespace AutoAuthorization.Controllers; + +using Keycloak.AuthServices.Authorization; using Microsoft.AspNetCore.Mvc; [ApiController] @@ -8,4 +10,16 @@ public class ApiController : ControllerBase [Route("/api/auto-auth")] [ProducesResponseType(statusCode: 200, type: typeof(string))] public IActionResult GetData() => this.Ok("Auto Authorization works."); + + [HttpGet] + [Route("/api/no-auth")] + [ProducesResponseType(statusCode: 200, type: typeof(string))] + [DisableUriBasedResourceProtection] + public IActionResult GetDataWithoutAuth() => this.Ok("Auto Authorization works, but is disabled by attribute."); + + [HttpGet] + [Route("/api/explicit-auth")] + [ProducesResponseType(statusCode: 200, type: typeof(string))] + [ExplicitResourceProtection("/api/auto-auth", "GET")] + public IActionResult GetDataWithExplicitAuth() => this.Ok("Auto Authorization works with explicit choosing resource name and scope."); } diff --git a/src/Keycloak.AuthServices.Authorization/ExplicitResourceProtectionAttribute.cs b/src/Keycloak.AuthServices.Authorization/ExplicitResourceProtectionAttribute.cs new file mode 100644 index 00000000..021f5771 --- /dev/null +++ b/src/Keycloak.AuthServices.Authorization/ExplicitResourceProtectionAttribute.cs @@ -0,0 +1,30 @@ +namespace Keycloak.AuthServices.Authorization; + +/// +/// Use this attribute on a method when the +/// to exclude the method from uri based resource protection and set a specific resource name and scope instead. +/// +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +public class ExplicitResourceProtectionAttribute : Attribute +{ + /// + /// + /// + /// The name of the resource a configured in Keycloak mandatory and unique "Name" field (also referred as RESOURCE_ID). + /// A valid scope from the list of applied scopes to the resource. + public ExplicitResourceProtectionAttribute(string resourceName, string scope) + { + this.ResourceName = resourceName; + this.Scope = scope; + } + + /// + /// The resource name to authorize for. + /// + public string ResourceName { get; } + + /// + /// The scope to authorize the resource for. + /// + public string Scope { get; } +} \ No newline at end of file diff --git a/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/DisableUriBasedResourceProtection.cs b/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/DisableUriBasedResourceProtection.cs new file mode 100644 index 00000000..6ee06a10 --- /dev/null +++ b/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/DisableUriBasedResourceProtection.cs @@ -0,0 +1,10 @@ +namespace Keycloak.AuthServices.Authorization; + +/// +/// Use this attribute on a method when the +/// to exclude the method from uri based resource protection. +/// +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +public class DisableUriBasedResourceProtectionAttribute : Attribute +{ +} \ No newline at end of file diff --git a/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtectionMiddleware.cs b/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/UriBasedResourceProtectionMiddleware.cs similarity index 59% rename from src/Keycloak.AuthServices.Authorization/UriBasedResourceProtectionMiddleware.cs rename to src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/UriBasedResourceProtectionMiddleware.cs index a13ed181..b4d79d12 100644 --- a/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtectionMiddleware.cs +++ b/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/UriBasedResourceProtectionMiddleware.cs @@ -33,36 +33,64 @@ public UriBasedResourceProtectionMiddleware(RequestDelegate next, IKeycloakProte /// public async Task InvokeAsync(HttpContext context) { - var targetAllowsAnonymous = context.Features? + if (this.EvaluateAuthorization(context).Result) + { + await this.next(context); + } + } + + private async Task EvaluateAuthorization(HttpContext context) + { + var attributes = context.Features? .Get()? .Endpoint? - .Metadata - .Any(attribute => attribute is AllowAnonymousAttribute) ?? false; + .Metadata; + + if (UriBasedProtectionDisabled(attributes)) + { + return true; + } - if (!targetAllowsAnonymous) + var resourceName = ""; + var scope = ""; + + if (attributes != null && attributes + .SingleOrDefault(attribute => attribute is ExplicitResourceProtectionAttribute) + is ExplicitResourceProtectionAttribute explicitResourceProtectionAttribute) + { + resourceName = explicitResourceProtectionAttribute.ResourceName; + scope = explicitResourceProtectionAttribute.Scope; + } + else { - var isAuthorized = await this.client.VerifyAccessToResource( - context.Request.Path, context.Request.Method, CancellationToken.None); + resourceName = context.Request.Path; + scope = context.Request.Method; + } - if (!isAuthorized) - { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; - return; - } + var isAuthorized = await this.client.VerifyAccessToResource( + resourceName, scope, CancellationToken.None); + + if (isAuthorized) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return true; } - await this.next(context); + return false; } + + private static bool UriBasedProtectionDisabled(EndpointMetadataCollection? attributes) => attributes != null && attributes.Any(attribute => attribute is AllowAnonymousAttribute + or DisableUriBasedResourceProtectionAttribute); } /// public static class MiddlewareExtensions { /// - /// Extension method to enable automatic resource protection. + /// Extension method to enable automatic uri based resource protection. /// /// The isntance. /// The isntance with usage. public static IApplicationBuilder UseUriBasedKeycloakEndpointProtection( this IApplicationBuilder builder) => builder.UseMiddleware(); -} +} \ No newline at end of file