From 33c2b9336fefa6df643d39ca3ba716280b037def Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Wed, 22 Jun 2022 22:53:34 +0200 Subject: [PATCH] - Fixed problem with ForbidResult without having an authentication schema defined. Now only HTTP Status 403 (Forbid) is returned. - BasicAuthenticationAttribute is now in namespace AMWD.Common.AspNetCore.Attributes. --- .../BasicAuthenticationAttribute.cs | 26 +++++------ .../Attributes/IPBlacklistAttribute.cs | 27 ++++++----- .../Attributes/IPWhitelistAttribute.cs | 27 ++++++----- .../BasicAuthenticationHandler.cs | 5 ++- .../BasicAuthenticationMiddleware.cs | 45 ++++++++++++------- CHANGELOG.md | 11 ++++- 6 files changed, 90 insertions(+), 51 deletions(-) rename AMWD.Common.AspNetCore/{BasicAuthentication => Attributes}/BasicAuthenticationAttribute.cs (71%) diff --git a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationAttribute.cs b/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs similarity index 71% rename from AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationAttribute.cs rename to AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs index 61e3825..337aefc 100644 --- a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationAttribute.cs +++ b/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs @@ -1,31 +1,30 @@ using System; +using System.Linq; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; +using AMWD.Common.AspNetCore.BasicAuthentication; 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 +namespace AMWD.Common.AspNetCore.Attributes { /// /// 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) + public BasicAuthenticationAttribute(IServiceScopeFactory serviceScopeFactory) { - this.logger = logger; this.serviceScopeFactory = serviceScopeFactory; } @@ -62,29 +61,30 @@ namespace AMWD.Common.AspNetCore.BasicAuthentication return; } + using var scope = serviceScopeFactory.CreateScope(); + var logger = scope.ServiceProvider.GetService>(); + 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); + string[] credentials = plain.Split(':', 2, StringSplitOptions.RemoveEmptyEntries); if (!string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password)) { - if (Username == credentials[0] && Password == credentials[1]) + if (Username == credentials.First() && Password == credentials.Last()) return; } - using var scope = serviceScopeFactory.CreateScope(); var validator = scope.ServiceProvider.GetService(); - - var principal = await validator?.ValidateAsync(credentials[0], credentials[1], context.HttpContext.GetRemoteIpAddress()); + var principal = await validator?.ValidateAsync(credentials.First(), credentials.Last(), context.HttpContext.GetRemoteIpAddress()); if (principal == null) SetAuthenticateRequest(context); } catch (Exception ex) { - logger.LogError(ex, $"Failed to execute the basic authentication attribute: {ex.Message}"); + logger?.LogError(ex, $"Failed to execute the basic authentication attribute: {ex.Message}"); context.Result = new StatusCodeResult(StatusCodes.Status500InternalServerError); } } @@ -95,8 +95,8 @@ namespace AMWD.Common.AspNetCore.BasicAuthentication if (!string.IsNullOrWhiteSpace(Realm)) context.HttpContext.Response.Headers["WWW-Authenticate"] += $" realm=\"{Realm.Replace("\"", "")}\""; - context.HttpContext.Response.StatusCode = 401; - context.Result = new UnauthorizedResult(); + context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; + context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized); } } } diff --git a/AMWD.Common.AspNetCore/Attributes/IPBlacklistAttribute.cs b/AMWD.Common.AspNetCore/Attributes/IPBlacklistAttribute.cs index 9dbd87a..c18dca5 100644 --- a/AMWD.Common.AspNetCore/Attributes/IPBlacklistAttribute.cs +++ b/AMWD.Common.AspNetCore/Attributes/IPBlacklistAttribute.cs @@ -58,7 +58,7 @@ namespace AMWD.Common.AspNetCore.Attributes if (MatchesIpAddress(ipAddress, remoteIpAddress)) { - context.Result = new ForbidResult(); + context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); return; } } @@ -78,7 +78,7 @@ namespace AMWD.Common.AspNetCore.Attributes if (MatchesIpAddress(child.Value, remoteIpAddress)) { - context.Result = new ForbidResult(); + context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); return; } } @@ -87,17 +87,24 @@ namespace AMWD.Common.AspNetCore.Attributes private static bool MatchesIpAddress(string configIpAddress, IPAddress remoteIpAddress) { - if (configIpAddress.Contains('/')) + try { - string[] ipNetworkParts = configIpAddress.Split('/'); - var ip = IPAddress.Parse(ipNetworkParts.First()); - int prefix = int.Parse(ipNetworkParts.Last()); + if (configIpAddress.Contains('/')) + { + string[] ipNetworkParts = configIpAddress.Split('/'); + var ip = IPAddress.Parse(ipNetworkParts.First()); + int prefix = int.Parse(ipNetworkParts.Last()); - var net = new IPNetwork(ip, prefix); - return net.Contains(remoteIpAddress); + var net = new IPNetwork(ip, prefix); + return net.Contains(remoteIpAddress); + } + + return IPAddress.Parse(configIpAddress).Equals(remoteIpAddress); + } + catch + { + return false; } - - return IPAddress.Parse(configIpAddress).Equals(remoteIpAddress); } } } diff --git a/AMWD.Common.AspNetCore/Attributes/IPWhitelistAttribute.cs b/AMWD.Common.AspNetCore/Attributes/IPWhitelistAttribute.cs index 568fe6d..27a7d48 100644 --- a/AMWD.Common.AspNetCore/Attributes/IPWhitelistAttribute.cs +++ b/AMWD.Common.AspNetCore/Attributes/IPWhitelistAttribute.cs @@ -67,7 +67,7 @@ namespace AMWD.Common.AspNetCore.Attributes var section = configuration.GetSection(ConfigurationKey); if (!section.Exists()) { - context.Result = new ForbidResult(); + context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); return; } @@ -81,22 +81,29 @@ namespace AMWD.Common.AspNetCore.Attributes } } - context.Result = new ForbidResult(); + context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); } private static bool MatchesIpAddress(string configIpAddress, IPAddress remoteIpAddress) { - if (configIpAddress.Contains('/')) + try { - string[] ipNetworkParts = configIpAddress.Split('/'); - var ip = IPAddress.Parse(ipNetworkParts.First()); - int prefix = int.Parse(ipNetworkParts.Last()); + if (configIpAddress.Contains('/')) + { + string[] ipNetworkParts = configIpAddress.Split('/'); + var ip = IPAddress.Parse(ipNetworkParts.First()); + int prefix = int.Parse(ipNetworkParts.Last()); - var net = new IPNetwork(ip, prefix); - return net.Contains(remoteIpAddress); + var net = new IPNetwork(ip, prefix); + return net.Contains(remoteIpAddress); + } + + return IPAddress.Parse(configIpAddress).Equals(remoteIpAddress); + } + catch + { + return false; } - - return IPAddress.Parse(configIpAddress).Equals(remoteIpAddress); } } } diff --git a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs index 05dcd28..fbde971 100644 --- a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs +++ b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; @@ -50,10 +51,10 @@ namespace AMWD.Common.AspNetCore.BasicAuthentication { var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); string plain = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader.Parameter)); - string[] credentials = plain.Split(':', 2); + string[] credentials = plain.Split(':', 2, StringSplitOptions.RemoveEmptyEntries); var ipAddress = Context.GetRemoteIpAddress(); - principal = await validator.ValidateAsync(credentials[0], credentials[1], ipAddress); + principal = await validator.ValidateAsync(credentials.First(), credentials.Last(), ipAddress); } catch (Exception ex) { diff --git a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs index b96c046..ea574a1 100644 --- a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs +++ b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -33,25 +34,39 @@ namespace AMWD.Common.AspNetCore.BasicAuthentication /// An awaitable task. public async Task InvokeAsync(HttpContext httpContext) { - if (httpContext.Request.Headers.TryGetValue("Authorization", out var authHeader) - && ((string)authHeader).StartsWith("Basic ")) + if (!httpContext.Request.Headers.ContainsKey("Authorization")) { - string encoded = ((string)authHeader).Split(' ', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? ""; - - string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encoded)); - string[] parts = decoded.Split(':', 2); - - var principal = validator.ValidateAsync(parts[0], parts[1], httpContext.GetRemoteIpAddress()); - if (principal != null) - { - await next.Invoke(httpContext); - return; - } + SetAuthenticateRequest(httpContext, validator.Realm); + return; } + try + { + var authHeader = AuthenticationHeaderValue.Parse(httpContext.Request.Headers["Authorization"]); + byte[] decoded = Convert.FromBase64String(authHeader.Parameter); + string plain = Encoding.UTF8.GetString(decoded); + string[] credentials = plain.Split(':', 2, StringSplitOptions.RemoveEmptyEntries); + + var principal = await validator.ValidateAsync(credentials.First(), credentials.Last(), httpContext.GetRemoteIpAddress()); + if (principal == null) + { + SetAuthenticateRequest(httpContext, validator.Realm); + return; + } + + await next.Invoke(httpContext); + } + catch + { + SetAuthenticateRequest(httpContext, validator.Realm); + } + } + + private static void SetAuthenticateRequest(HttpContext httpContext, string realm) + { httpContext.Response.Headers["WWW-Authenticate"] = "Basic"; - if (!string.IsNullOrWhiteSpace(validator.Realm)) - httpContext.Response.Headers["WWW-Authenticate"] += $" realm=\"{validator.Realm}\""; + if (!string.IsNullOrWhiteSpace(realm)) + httpContext.Response.Headers["WWW-Authenticate"] += $" realm=\"{realm.Replace("\"", "")}\""; httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5d4ef..dcd479b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://git.am-wd.de/AM.WD/common/compare/v1.5.2...master) - 0000-00-00 +## [Unreleased](https://git.am-wd.de/AM.WD/common/compare/v1.5.3...master) - 0000-00-00 _nothing changed yet_ +## [v1.5.3](https://git.am-wd.de/AM.WD/common/compare/v1.5.2...v1.5.3) - 2022-06-22 +### Fixed +- Fixed problem with `ForbidResult` without having an authentication schema defined. + Now only HTTP Status 403 (Forbid) is returned. + +### Changed +- `BasicAuthenticationAttribute` is now in namespace `AMWD.Common.AspNetCore.Attributes`. + + ## [v1.5.2](https://git.am-wd.de/AM.WD/common/compare/v1.5.1...v1.5.2) - 2022-06-20 ### Removed - Removed support for .NET 5.0 due to EOL (2022-05-10, see [.NET and .NET Core release lifecycle](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core#lifecycle))