|
|
@ -2,11 +2,172 @@
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Text;
|
|
|
|
using System.Text;
|
|
|
|
|
|
|
|
using System.Text.Encodings.Web;
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
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
|
|
|
|
namespace AuthStudy.Authentication.Basic
|
|
|
|
{
|
|
|
|
{
|
|
|
|
internal class BasicAuthenticationHandler
|
|
|
|
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;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|