diff --git a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationAttribute.cs b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationAttribute.cs new file mode 100644 index 0000000..59f1464 --- /dev/null +++ b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationAttribute.cs @@ -0,0 +1,101 @@ +using System; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace AMWD.Common.AspNetCore.BasicAuthentication +{ + /// + /// A basic authentication as attribute to use for specific actions. + /// + public class BasicAuthenticationAttribute : ActionFilterAttribute + { + private readonly ILogger logger; + private readonly IServiceScopeFactory serviceScopeFactory; + + /// + /// Initializes a new instance of the class. + /// + /// A logger. + /// A service scope factory. + public BasicAuthenticationAttribute(ILogger logger, IServiceScopeFactory serviceScopeFactory) + { + this.logger = logger; + this.serviceScopeFactory = serviceScopeFactory; + } + + /// + /// Gets or sets a username to validate. + /// + public string Username { get; set; } + + /// + /// Gets or sets a password to validate. + /// + public string Password { get; set; } + + /// + /// Gets or sets a realm used on authentication header. + /// + public string Realm { get; set; } + + /// + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + await DoValidation(context); + await base.OnActionExecutionAsync(context, next); + } + + private async Task DoValidation(ActionExecutingContext context) + { + if (context.Result != null) + return; + + if (context.HttpContext.Request.Headers.ContainsKey("Authorization")) + { + SetAuthenticateRequest(context); + return; + } + + try + { + var authHeader = AuthenticationHeaderValue.Parse(context.HttpContext.Request.Headers["Authorization"]); + byte[] decoded = Convert.FromBase64String(authHeader.Parameter); + string plain = Encoding.UTF8.GetString(decoded); + string[] credentials = plain.Split(':', 2); + + if (!string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password)) + { + if (Username == credentials[0] && Password == credentials[1]) + return; + } + + using var scope = serviceScopeFactory.CreateScope(); + var validator = scope.ServiceProvider.GetService(); + + var principal = await validator?.ValidateAsync(credentials[0], credentials[1], context.HttpContext.GetRemoteIpAddress()); + if (principal == null) + SetAuthenticateRequest(context); + } + catch (Exception ex) + { + logger.LogError(ex, $"Failed to execute the basic authentication attribute: {ex.Message}"); + context.Result = new StatusCodeResult(StatusCodes.Status500InternalServerError); + } + } + + private void SetAuthenticateRequest(ActionExecutingContext context) + { + context.HttpContext.Response.Headers["WWW-Authenticate"] = "Basic"; + if (!string.IsNullOrWhiteSpace(Realm)) + context.HttpContext.Response.Headers["WWW-Authenticate"] += $" realm=\"{Realm.Replace("\"", "")}\""; + + context.Result = new UnauthorizedResult(); + } + } +} diff --git a/AMWD.Common.AspNetCore/AuthenticationHandler/BasicAuthenticationHandler.cs b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs similarity index 94% rename from AMWD.Common.AspNetCore/AuthenticationHandler/BasicAuthenticationHandler.cs rename to AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs index 22634dc..05dcd28 100644 --- a/AMWD.Common.AspNetCore/AuthenticationHandler/BasicAuthenticationHandler.cs +++ b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs @@ -4,12 +4,13 @@ using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Authentication +namespace AMWD.Common.AspNetCore.BasicAuthentication { /// /// Implements the for Basic Authentication. diff --git a/AMWD.Common.AspNetCore/Middlewares/BasicAuthMiddleware.cs b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs similarity index 62% rename from AMWD.Common.AspNetCore/Middlewares/BasicAuthMiddleware.cs rename to AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs index bd268b6..14e9b37 100644 --- a/AMWD.Common.AspNetCore/Middlewares/BasicAuthMiddleware.cs +++ b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs @@ -2,29 +2,30 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Http +namespace AMWD.Common.AspNetCore.BasicAuthentication { /// /// Implements a basic authentication. /// - public class BasicAuthMiddleware + public class BasicAuthenticationMiddleware { private readonly RequestDelegate next; private readonly string realm; - private readonly Func userPasswordAuth; + private readonly IBasicAuthenticationValidator validator; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The following delegate in the process chain. /// The realm to display when requesting for credentials. - /// The function (user, passwd) => result to validate username and password. - public BasicAuthMiddleware(RequestDelegate next, string realm, Func userPasswordAuth) + /// A basic authentication validator. + public BasicAuthenticationMiddleware(RequestDelegate next, string realm, IBasicAuthenticationValidator validator) { this.next = next; this.realm = realm; - this.userPasswordAuth = userPasswordAuth; + this.validator = validator; } /// @@ -41,18 +42,13 @@ namespace Microsoft.AspNetCore.Http string encoded = ((string)authHeader).Split(' ', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? ""; string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encoded)); - string[] parts = decoded.Split(':'); + string[] parts = decoded.Split(':', 2); - if (parts.Length >= 2) + var principal = validator.ValidateAsync(parts[0], parts[1], httpContext.GetRemoteIpAddress()); + if (principal != null) { - string username = parts[0].Trim().ToLower(); - string password = parts[1].Trim(); - - if (userPasswordAuth(username, password)) - { - await next.Invoke(httpContext); - return; - } + await next.Invoke(httpContext); + return; } } diff --git a/AMWD.Common.AspNetCore/AuthenticationHandler/IBasicAuthenticationValidator.cs b/AMWD.Common.AspNetCore/BasicAuthentication/IBasicAuthenticationValidator.cs similarity index 90% rename from AMWD.Common.AspNetCore/AuthenticationHandler/IBasicAuthenticationValidator.cs rename to AMWD.Common.AspNetCore/BasicAuthentication/IBasicAuthenticationValidator.cs index 6993641..84972f7 100644 --- a/AMWD.Common.AspNetCore/AuthenticationHandler/IBasicAuthenticationValidator.cs +++ b/AMWD.Common.AspNetCore/BasicAuthentication/IBasicAuthenticationValidator.cs @@ -2,7 +2,7 @@ using System.Security.Claims; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Authentication +namespace AMWD.Common.AspNetCore.BasicAuthentication { /// /// Interface representing the validation of a basic authentication. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6842b7a..05b4df3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CHANGELOG - `HtmlHelper.IsDarkColor` to classify a color as dark or light one (by luminance) - `ReadLine` and `ReadLineAsync` as `StreamExtensions` +- `BasicAuthenticationHandler` to use instead of other handlers (instead of e.g. CookieAuthentication) +- `BasicAuthenticationAttribute` to restrict single actions ### Changed - Unit-Tests enhanced - Unit-Tests excluded from code coverage calcualtion - Updated NuGet package `NetRevisionTask` +- Changed NuGet package `DnsClient` to `DNS` ## [v1.1.0](https://git.am-wd.de/AM.WD/common/compare/v1.0.2...v1.1.0) - 2021-11-22