diff --git a/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs b/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs
index 73110a6..d3d08ae 100644
--- a/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs
+++ b/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs
@@ -1,19 +1,22 @@
using System;
using System.Linq;
using System.Net.Http.Headers;
+using System.Security.Claims;
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 Microsoft.AspNetCore.Mvc.Filters
+namespace Microsoft.AspNetCore.Authorization
{
///
/// A basic authentication as attribute to use for specific actions.
///
- public class BasicAuthenticationAttribute : ActionFilterAttribute
+ public class BasicAuthenticationAttribute : Attribute, IAsyncAuthorizationFilter
{
///
/// Gets or sets a username to validate.
@@ -31,26 +34,22 @@ namespace Microsoft.AspNetCore.Mvc.Filters
public string Realm { get; set; }
///
- public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
- 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;
- }
-
var logger = context.HttpContext.RequestServices.GetService>();
try
{
+ var validatorResult = await TrySetHttpUser(context);
+ bool isAllowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType().Any();
+ if (isAllowAnonymous)
+ return;
+
+ if (!context.HttpContext.Request.Headers.ContainsKey("Authorization"))
+ {
+ SetAuthenticateRequest(context);
+ return;
+ }
+
var authHeader = AuthenticationHeaderValue.Parse(context.HttpContext.Request.Headers["Authorization"]);
byte[] decoded = Convert.FromBase64String(authHeader.Parameter);
string plain = Encoding.UTF8.GetString(decoded);
@@ -62,19 +61,17 @@ namespace Microsoft.AspNetCore.Mvc.Filters
return;
}
- var validator = context.HttpContext.RequestServices.GetService();
- var principal = await validator?.ValidateAsync(credentials.First(), credentials.Last(), context.HttpContext.GetRemoteIpAddress());
- if (principal == null)
+ if (validatorResult == 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.InnerException?.Message ?? ex.Message}");
context.Result = new StatusCodeResult(StatusCodes.Status500InternalServerError);
}
}
- private void SetAuthenticateRequest(ActionExecutingContext context)
+ private void SetAuthenticateRequest(AuthorizationFilterContext context)
{
context.HttpContext.Response.Headers["WWW-Authenticate"] = "Basic";
if (!string.IsNullOrWhiteSpace(Realm))
@@ -83,5 +80,33 @@ namespace Microsoft.AspNetCore.Mvc.Filters
context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized);
}
+
+ private async Task TrySetHttpUser(AuthorizationFilterContext context)
+ {
+ var logger = context.HttpContext.RequestServices.GetService>();
+ try
+ {
+ if (context.HttpContext.Request.Headers.ContainsKey("Authorization"))
+ {
+ 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, StringSplitOptions.RemoveEmptyEntries);
+
+ var validator = context.HttpContext.RequestServices.GetService();
+ var result = await validator?.ValidateAsync(credentials.First(), credentials.Last(), context.HttpContext.GetRemoteIpAddress());
+ if (result != null)
+ context.HttpContext.User = result;
+
+ return result;
+ }
+ }
+ catch (Exception ex)
+ {
+ logger?.LogError(ex, $"Using validator to get HTTP user failed: {ex.InnerException?.Message ?? ex.Message}");
+ }
+
+ return null;
+ }
}
}
diff --git a/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs b/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs
index 29531df..25355a2 100644
--- a/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs
+++ b/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs
@@ -203,14 +203,17 @@ namespace AMWD.Common.Tests.Extensions
{
// arrange
var timeSpan = TimeSpan.Parse("1.10:11:12.345");
+ var negativeTimeSpan = TimeSpan.FromDays(-1.234);
// act
string shortString = timeSpan.ToShortString(withMilliseconds: false);
string shortStringWithMillis = timeSpan.ToShortString(withMilliseconds: true);
+ string shortStringNegative = negativeTimeSpan.ToShortString(withMilliseconds: true);
// assert
Assert.AreEqual("1d 10h 11m 12s", shortString);
Assert.AreEqual("1d 10h 11m 12s 345ms", shortStringWithMillis);
+ Assert.AreEqual("-1d 5h 36m 57s 600ms", shortStringNegative);
}
[TestMethod]
diff --git a/AMWD.Common/Extensions/DateTimeExtensions.cs b/AMWD.Common/Extensions/DateTimeExtensions.cs
index dbd554a..195ed8e 100644
--- a/AMWD.Common/Extensions/DateTimeExtensions.cs
+++ b/AMWD.Common/Extensions/DateTimeExtensions.cs
@@ -91,19 +91,22 @@ namespace System
{
var sb = new StringBuilder();
- if (timeSpan.TotalDays >= 1)
- sb.Append(timeSpan.Days).Append("d ");
+ if (timeSpan < TimeSpan.Zero)
+ sb.Append("-");
- if (timeSpan.TotalHours >= 1)
- sb.Append(timeSpan.Hours).Append("h ");
+ if (timeSpan.TotalDays != 0)
+ sb.Append(Math.Abs(timeSpan.Days)).Append("d ");
- if (timeSpan.TotalMinutes >= 1)
- sb.Append(timeSpan.Minutes).Append("m ");
+ if (timeSpan.TotalHours != 0)
+ sb.Append(Math.Abs(timeSpan.Hours)).Append("h ");
- sb.Append(timeSpan.Seconds).Append("s ");
+ if (timeSpan.TotalMinutes != 0)
+ sb.Append(Math.Abs(timeSpan.Minutes)).Append("m ");
+
+ sb.Append(Math.Abs(timeSpan.Seconds)).Append("s ");
if (withMilliseconds)
- sb.Append(timeSpan.Milliseconds).Append("ms");
+ sb.Append(Math.Abs(timeSpan.Milliseconds)).Append("ms");
return sb.ToString().Trim();
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c848fa..bdf514e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,26 +4,37 @@ 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.6.0...master) - 0000-00-00
+## [Unreleased](https://git.am-wd.de/AM.WD/common/compare/v1.6.1...master) - 0000-00-00
_nothing changed yet_
-## [v1.6.0](https://git.am-wd.de/AM.WD/common/compare/v1.5.3...v1.6.0) - 2022-06-22
-### Fixed
-- Fixed `BasicAuthenticationAttribute`.
+## [v1.6.1](https://git.am-wd.de/AM.WD/common/compare/v1.6.0...v1.6.1) - 2022-06-23
+### Added
+- `BasicAuthenticationAttribute` sets the `HttpContext.User` property when successfully validated.
+### Changed
+- Moved `BasicAuthenticationAttribute` from `ActionFilter` to `IAsyncAuthorizationFilter`.
+
+### Fixed
+- `TimeSpan.ToShortString()` is now capable of negative values.
+
+
+## [v1.6.0](https://git.am-wd.de/AM.WD/common/compare/v1.5.3...v1.6.0) - 2022-06-22
### Changed
- All attributes now reside in `Microsoft.AspNetCore.Mvc.Filters` namespace.
+### Fixed
+- Fixed `BasicAuthenticationAttribute`.
+
## [v1.5.3](https://git.am-wd.de/AM.WD/common/compare/v1.5.2...v1.5.3) - 2022-06-22
+### Changed
+- `BasicAuthenticationAttribute` is now in namespace `AMWD.Common.AspNetCore.Attributes`.
+
### 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
@@ -147,7 +158,6 @@ _nothing changed yet_
- `ResolveHost` returns a list of resolved ip addresses
- `ResolveInterface` returns a list of resolved ip addresses
-
### Removed
- `IPAddress fallback` parameter removed from `ResolveHost`
- `IPAddress fallback` parameter removed from `ResolveInterface`