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.Authorization { /// /// A basic authentication as attribute to use for specific actions. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class BasicAuthenticationAttribute : Attribute, IAsyncAuthorizationFilter { /// /// 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 async Task OnAuthorizationAsync(AuthorizationFilterContext context) { 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); // See: https://www.rfc-editor.org/rfc/rfc2617, page 6 string username = plain.Split(':').First(); string password = plain[(username.Length + 1)..]; if (!string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password)) { if (Username == username && Password == password) return; } if (validatorResult == null) SetAuthenticateRequest(context); } catch (Exception ex) { logger?.LogError(ex, $"Failed to execute the basic authentication attribute: {ex.InnerException?.Message ?? ex.Message}"); context.Result = new StatusCodeResult(StatusCodes.Status500InternalServerError); } } private void SetAuthenticateRequest(AuthorizationFilterContext context) { var validator = context.HttpContext.RequestServices.GetService(); string realm = string.IsNullOrWhiteSpace(Realm) ? string.IsNullOrWhiteSpace(validator?.Realm) ? null : validator.Realm : Realm; context.HttpContext.Response.Headers["WWW-Authenticate"] = "Basic"; if (!string.IsNullOrWhiteSpace(realm)) context.HttpContext.Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{realm.Trim().Replace("\"", "")}\""; 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); // See: https://www.rfc-editor.org/rfc/rfc2617, page 6 string username = plain.Split(':').First(); string password = plain[(username.Length + 1)..]; var validator = context.HttpContext.RequestServices.GetService(); if (validator == null) return null; var result = await validator.ValidateAsync(username, password, context.HttpContext.GetRemoteIpAddress()); if (result == null) return null; context.HttpContext.User = result; return result; } return null; } catch (Exception ex) { logger?.LogError(ex, $"Using validator to get HTTP user failed: {ex.InnerException?.Message ?? ex.Message}"); return null; } } } }