diff --git a/DI/XDIHelperExtension.cs b/DI/XDIHelperExtension.cs index 5979118..53162ad 100644 --- a/DI/XDIHelperExtension.cs +++ b/DI/XDIHelperExtension.cs @@ -399,31 +399,56 @@ namespace xIds.DI /// /// /// - /// public static void AddXIdentityServerAuthentication( this IServiceCollection services, - IConfiguration configuration, - bool addApiKeyAuthentication = false + IConfiguration configuration ) { // // Adding Requirements ... services.AddXIdentityResourceConfiguration(configuration); + + // + // Create Authentication Builder ... + // var authBuilder = services.AddAuthentication(); // // Create Authentication Builder ... - var authBuilder = services.AddAuthentication(); - - // - // For Best Support we Have to Add ApiKey Authentication First ... - if (addApiKeyAuthentication) + var authBuilder = services.AddAuthentication(options => { // - authBuilder - .AddScheme( - XAuthenticationScheme.XApiKey.GetStringValue(), - options => { }); - } + // Setting Default Authentication Schema ... + // For Handling Smart Schema Forwarder ... + options.DefaultScheme = XAuthentication.SMART_SCHEME; + options.DefaultChallengeScheme = XAuthentication.SMART_SCHEME; + options.DefaultAuthenticateScheme = XAuthentication.SMART_SCHEME; + }) + .AddPolicyScheme( + authenticationScheme: XAuthentication.SMART_SCHEME, + displayName: XAuthentication.SMART_SCHEME, + configureOptions: options => + { + // + // Configure Forward Default Selector ... + options.ForwardDefaultSelector = context => + { + // + // Retrieve XApiKey Header Key ... + var apiKeyHeader = XHeader.ApiKey.GetStringValue(); + + // + // If Header Contains Key ... + if (context.Request.Headers.ContainsKey(apiKeyHeader)) + { + return XAuthenticationScheme.XApiKey.GetStringValue(); + } + + // + // If not ... + return XAuthentication.IDENTITY_SERVER_LOCAL_API; + }; + } + ); // // Add JwtBearer by Smart Schema ForwardDefaultSelector ... @@ -432,26 +457,7 @@ namespace xIds.DI // options.SaveToken = true; options.RequireHttpsMetadata = false; - // options.ForwardDefault = XAuthentication.IDENTITY_SERVER_LOCAL_API; - - // - // Selecting Authentication Schema Based On Header ... - options.ForwardDefaultSelector = context => - { - // - // if Header Contains X-Api-Key use Scheme XApiKey ... - var apiKeyHeader = XHeader.ApiKey - .GetStringValue() - .ToNormalString(); - if (context.Request.Headers.ContainsKey(apiKeyHeader)) - { - return XAuthenticationScheme.XApiKey.GetStringValue(); - } - - // - // If Not, Use Local Api ... - return XAuthentication.IDENTITY_SERVER_LOCAL_API; - }; + options.ForwardDefault = XAuthentication.IDENTITY_SERVER_LOCAL_API; }) .AddLocalApi(); } @@ -505,7 +511,13 @@ namespace xIds.DI options.AddPolicy(IdentityServerConstants.LocalApi.PolicyName, policy => { // - policy.AddAuthenticationSchemes(IdentityServerConstants.LocalApi.AuthenticationScheme); + policy.AddAuthenticationSchemes( + [ + XAuthenticationScheme.XApiKey.GetStringValue(), + IdentityServerConstants.LocalApi.AuthenticationScheme + ]); + + // policy.RequireAuthenticatedUser(); }); }); diff --git a/Helpers/XApiKeyHelper.cs b/Helpers/XApiKeyHelper.cs index 89c00e3..184fe15 100644 --- a/Helpers/XApiKeyHelper.cs +++ b/Helpers/XApiKeyHelper.cs @@ -1,6 +1,8 @@ using System; using System.Text; using System.Security.Cryptography; +using xIdentityModels.Constants; +using xIdentityHelper; namespace xIds.Helpers { @@ -73,5 +75,35 @@ namespace xIds.Helpers var hash = ComputeHash(plainKey); return hash == storedHash; } + + /// + /// Retrieve All ApiKey Scopes ... + /// + /// + public static XApiKeyScope[] GetXApiKeyScopes() + { + // + return [ + XApiKeyScope.Read, + XApiKeyScope.Write, + XApiKeyScope.Manage, + XApiKeyScope.Admin, + ]; + } + + /// + /// Retrieve All ApiKey Policies ... + /// + /// + public static string[] GetXApiKeyPolicies() + { + return [ + XPolicies.ApiKeyAccess, + XPolicies.ApiKeyReadAccess, + XPolicies.ApiKeyWriteAccess, + XPolicies.ApiKeyAdminAccess, + XPolicies.ApiKeyManageAccess, + ]; + } } } \ No newline at end of file diff --git a/Providers/XApiKeyAuthenticationHandler.cs b/Providers/XApiKeyAuthenticationHandler.cs index 9f36f61..26aa9ac 100644 --- a/Providers/XApiKeyAuthenticationHandler.cs +++ b/Providers/XApiKeyAuthenticationHandler.cs @@ -1,3 +1,5 @@ +using System.Linq; +using xIds.Helpers; using IdentityModel; using xIds.Interfaces; using xCommons.Constants; @@ -5,10 +7,12 @@ using xCommons.Extensions; using System.Threading.Tasks; using System.Security.Claims; using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Http; using xIdentityService.Constants; using System.Collections.Generic; using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication; namespace xIds.Providers @@ -41,18 +45,49 @@ namespace xIds.Providers /// protected override async Task HandleAuthenticateAsync() { + // + // Obtain the endpoint currently being executed + var endpoint = Context.GetEndpoint(); + + // + // Retrieve all AuthorizeAttribute metadata from the endpoint + var hasApiKeyScheme = false; + var apiKeyScopes = XApiKeyHelper.GetXApiKeyScopes(); + var apiKeyPolicies = XApiKeyHelper.GetXApiKeyPolicies(); + var authorizeAttributes = endpoint?.Metadata.GetOrderedMetadata(); + if ( + apiKeyPolicies.HasChild() && + authorizeAttributes.HasChild() + ) + { + // + // Retrieve a List of ApiKey Policies ... + hasApiKeyScheme = authorizeAttributes.Any(attr => + apiKeyPolicies.Any(asc => asc.ToNormalString() == attr.Policy.ToNormalString()) || + attr.AuthenticationSchemes.ToNormalString() == XAuthenticationScheme.XApiKey.GetStringValue().ToNormalString() + ); + } + + // + // if there is not any Policies for ApiKey Authentication + // or Authorization, pass no Result ... + if (!hasApiKeyScheme) + { + return AuthenticateResult.NoResult(); + } + // var apiKeyHeader = HeaderName; if (!Request.Headers.ContainsKey(apiKeyHeader)) { - return AuthenticateResult.NoResult(); + return AuthenticateResult.Fail("ApiKey not Exists ..."); } // var apiKey = Request.Headers[apiKeyHeader].ToString(); if (apiKey.IsNullOrEmpty()) { - return AuthenticateResult.NoResult(); + return AuthenticateResult.Fail("ApiKey not Provided ..."); } // Extract Client IP ... diff --git a/Providers/XApiKeyAuthorizationFilter.cs b/Providers/XApiKeyAuthorizationFilter.cs new file mode 100644 index 0000000..1812d93 --- /dev/null +++ b/Providers/XApiKeyAuthorizationFilter.cs @@ -0,0 +1,192 @@ +using System; +using System.Linq; +using xIds.Helpers; +using IdentityModel; +using xIds.Interfaces; +using xIdentityHelper; +using xCommons.Constants; +using xCommons.Extensions; +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using xIdentityModels.Constants; +using xIdentityService.Constants; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; + +namespace xIds.Providers +{ + [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Method, + AllowMultiple = false, + Inherited = true + )] + public class XApiKeyAuthorizationFilter : AuthorizeAttribute, IAuthorizationFilter + { + // /// + // /// Optional required scope for this endpoint. + // /// Example: [XApiKeyAuthorizationFilter(RequiredScope = "read")] + // /// + // public string RequiredScope { get; set; } + + // /// + // /// If false, a missing header will only result in NoResult (anonymous) + // /// instead of a 401. Default is true (must provide a key). + // /// + // public bool ApiKeyRequired { get; set; } = true; + + // /// + // /// Optional allowed scopes. If empty, no scope enforcement. + // /// + // public string[] AllowedScopes { get; set; } + + public XApiKeyAuthorizationFilter() { } + + /// + /// Authorization Filtering ... + /// + /// + public void OnAuthorization(AuthorizationFilterContext context) + { + Task.FromResult(OnAuthorizationAsync(context)); + } + + /// + /// Handle Authorization Filtering Using Tasks ... + /// + /// + /// + private async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + // + // Access HttpContext ... + var http = context.HttpContext; + + // + try + { + // + // Obtain the endpoint currently being executed + var endpoint = http.GetEndpoint(); + + // + // Retrieve all AuthorizeAttribute metadata from the endpoint + var hasApiKeyScheme = false; + var apiKeyScopes = XApiKeyHelper.GetXApiKeyScopes(); + var apiKeyPolicies = XApiKeyHelper.GetXApiKeyPolicies(); + var authorizeAttributes = endpoint?.Metadata.GetOrderedMetadata(); + if ( + apiKeyPolicies.HasChild() && + authorizeAttributes.HasChild() + ) + { + // + // Retrieve a List of ApiKey Policies ... + hasApiKeyScheme = authorizeAttributes.Any(attr => + apiKeyPolicies.Any(asc => asc.ToNormalString() == attr.Policy.ToNormalString()) + ); + } + + // + // if there is not any Policies for ApiKey Authentication + // or Authorization, pass no Result ... + if (!hasApiKeyScheme) + { + return; + } + + // + // Retrieve the application provider from DI ... + var applicationProvider = http.RequestServices + .GetService(typeof(IXApplicationProvider)) + as IXApplicationProvider; + if (applicationProvider.IsNull()) + { + // + context.Result = new UnauthorizedObjectResult("Provider Service Not Registered ..."); + return; + } + + // + // Checking Header ... + var apiKeyHeader = XHeader.ApiKey.GetStringValue(); + var apiKeyAuthentication = XAuthenticationScheme.XApiKey.GetStringValue(); + if (!http.Request.Headers.ContainsKey(apiKeyHeader)) + { + // + context.Result = new UnauthorizedObjectResult("ApiKey not Exists ..."); + return; + } + + // + // Extract ApiKey from Header ... + var apiKey = http.Request.Headers[apiKeyHeader].ToString(); + if (apiKey.IsNullOrEmpty()) + { + // + context.Result = new UnauthorizedObjectResult("ApiKey not Provided ..."); + return; + } + + // + // Extract Client IP ... + var clientIP = http.Request.HttpContext.Connection.RemoteIpAddress?.ToString(); + + // + // Validating ApiKey ... + var validationResult = await applicationProvider + .ValidateApiKey( + apiKey: apiKey, + clientIP: clientIP + ); + if (validationResult.Errors.HasChild()) + { + // + var message = validationResult.Errors.ToListString('\n'); + context.Result = new UnauthorizedObjectResult(message); + return; + } + + // + // Creating Claims ... + var claims = new List { + // + // Owner Identifier of API Key ... + new(ClaimTypes.Name, validationResult.OwnerId), + new(JwtClaimTypes.Subject, validationResult.OwnerId), + // + // Application Id ... + new(XCustomClaims.ApplicationId, validationResult.ApplicationId.ToString()), + // + // Authentication Type ... + new(XCustomClaims.AuthType, apiKeyAuthentication), + }; + + // + // Add Scopes ... + if (validationResult.Scopes != null) + { + // + foreach (var scope in validationResult.Scopes) + { + claims.Add(new Claim(JwtClaimTypes.Scope, scope)); + } + } + + // + // Create Principal and Ticket ... + var identity = new ClaimsIdentity(claims, apiKeyAuthentication); + var principal = new ClaimsPrincipal(identity); + + // + http.User = principal; + } + catch + { + context.Result = new UnauthorizedObjectResult("ApiKey authorization failed ..."); + } + } + } +} \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index 546448b..000389d 100644 --- a/Startup.cs +++ b/Startup.cs @@ -67,10 +67,7 @@ namespace xIds // // Register Authentication ... - services.AddXIdentityServerAuthentication( - configuration: Configuration, - addApiKeyAuthentication: true - ); + services.AddXIdentityServerAuthentication(Configuration); // // Register Authorization ... diff --git a/Validators/XResourceOwnerPasswordValidator.cs b/Validators/XResourceOwnerPasswordValidator.cs index b4ece99..e65c179 100644 --- a/Validators/XResourceOwnerPasswordValidator.cs +++ b/Validators/XResourceOwnerPasswordValidator.cs @@ -1,14 +1,14 @@ using System; +using xIds.Interfaces; +using xCommons.Extensions; +using xExceptions.Constants; using System.Threading.Tasks; using IdentityServer4.Events; using IdentityServer4.Models; using IdentityServer4.Services; using IdentityServer4.Validation; -using Microsoft.Extensions.Logging; -using xCommons.Extensions; -using xExceptions.Constants; using xIdentityModels.Navigations; -using xIds.Interfaces; +using Microsoft.Extensions.Logging; using static IdentityModel.OidcConstants; namespace xIds.Validators