using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; using AuthStudy.Authentication.Basic.Events; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; namespace AuthStudy.Authentication.Basic { public class BasicAuthenticationHandler : AuthenticationHandler { private const string _Scheme = "BasicScheme"; private readonly UTF8Encoding _utf8ValidatingEncoding = new UTF8Encoding(false, true); public BasicAuthenticationHandler ( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock ) : base(options, logger, encoder, clock) { } protected new BasicAuthenticationEvents? Events { get { return (BasicAuthenticationEvents?)base.Events; } set { base.Events = value; } } protected override Task CreateEventsAsync() => Task.FromResult(new BasicAuthenticationEvents()); protected override async Task HandleAuthenticateAsync() { string? authorizationHeader = Request.Headers["Authorization"]; if (string.IsNullOrEmpty(authorizationHeader)) { return AuthenticateResult.NoResult(); } // Exact match on purpose, rather than using string compare // asp.net request parsing will always trim the header and remove trailing spaces if (_Scheme == authorizationHeader) { const string noCredentialsMessage = "Authorization scheme was Basic but the header had no credentials."; Logger.LogInformation(noCredentialsMessage); return AuthenticateResult.Fail(noCredentialsMessage); } if (!authorizationHeader.StartsWith(_Scheme + ' ', StringComparison.OrdinalIgnoreCase)) { return AuthenticateResult.NoResult(); } string encodedCredentials = authorizationHeader.Substring(_Scheme.Length).Trim(); try { string decodedCredentials = string.Empty; byte[] base64DecodedCredentials; try { base64DecodedCredentials = Convert.FromBase64String(encodedCredentials); } catch (FormatException) { const string failedToDecodeCredentials = "Cannot convert credentials from Base64."; Logger.LogInformation(failedToDecodeCredentials); return AuthenticateResult.Fail(failedToDecodeCredentials); } try { decodedCredentials = _utf8ValidatingEncoding.GetString(base64DecodedCredentials); } catch (Exception ex) { const string failedToDecodeCredentials = "Cannot build credentials from decoded base64 value, exception {ex.Message} encountered."; Logger.LogInformation(failedToDecodeCredentials, ex.Message); return AuthenticateResult.Fail(ex.Message); } var delimiterIndex = decodedCredentials.IndexOf(":", StringComparison.OrdinalIgnoreCase); if (delimiterIndex == -1) { const string missingDelimiterMessage = "Invalid credentials, missing delimiter."; Logger.LogInformation(missingDelimiterMessage); return AuthenticateResult.Fail(missingDelimiterMessage); } var username = decodedCredentials.Substring(0, delimiterIndex); var password = decodedCredentials.Substring(delimiterIndex + 1); var validateCredentialsContext = new ValidateCredentialsContext(Context, Scheme, Options) { Username = username, Password = password }; if (Events != null) { await Events.ValidateCredentials(validateCredentialsContext); } if (validateCredentialsContext.Result != null && validateCredentialsContext.Result.Succeeded) { var ticket = new AuthenticationTicket(validateCredentialsContext.Principal, Scheme.Name); return AuthenticateResult.Success(ticket); } if (validateCredentialsContext.Result != null && validateCredentialsContext.Result.Failure != null) { return AuthenticateResult.Fail(validateCredentialsContext.Result.Failure); } return AuthenticateResult.NoResult(); } catch (Exception ex) { var authenticationFailedContext = new BasicAuthenticationFailedContext(Context, Scheme, Options) { Exception = ex }; if (Events != null) { await Events.AuthenticationFailed(authenticationFailedContext).ConfigureAwait(true); } if (authenticationFailedContext.Result != null) { return authenticationFailedContext.Result; } throw; } } protected override Task HandleChallengeAsync(AuthenticationProperties properties) { if (!Request.IsHttps && !Options.AllowInsecureProtocol) { const string insecureProtocolMessage = "Request is HTTP, Basic Authentication will not respond."; Logger.LogInformation(insecureProtocolMessage); Response.StatusCode = StatusCodes.Status421MisdirectedRequest; } else { Response.StatusCode = 401; if (!Options.SuppressWWWAuthenticateHeader) { var headerValue = _Scheme + $" realm=\"{Options.Realm}\""; Response.Headers.Append(HeaderNames.WWWAuthenticate, headerValue); } } return Task.CompletedTask; } } }