You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

174 lines
6.5 KiB
C#

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<BasicAuthenticationOptions>
{
private const string _Scheme = "BasicScheme";
private readonly UTF8Encoding _utf8ValidatingEncoding = new UTF8Encoding(false, true);
public BasicAuthenticationHandler
(
IOptionsMonitor<BasicAuthenticationOptions> 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<object> CreateEventsAsync() => Task.FromResult<object>(new BasicAuthenticationEvents());
protected override async Task<AuthenticateResult> 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;
}
}
}