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