Skip to content

Instantly share code, notes, and snippets.

@jbardon
Last active April 24, 2020 08:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jbardon/ce5a896004c96ce377e8068ee79a047b to your computer and use it in GitHub Desktop.
Save jbardon/ce5a896004c96ce377e8068ee79a047b to your computer and use it in GitHub Desktop.
.NET Core Http Basic authentication handler
using System;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using auth_netcore.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace ApiCommonConfig.Authentication.Handlers
{
public class HttpBasicAuthenticationHandler: AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string AuthenticationScheme = "HttpBasic";
private readonly Service _service;
public HttpBasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
Service service)
: base(options, logger, encoder, clock)
{
this._service = service;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
try {
dynamic credentials = GetUserCredentials(Request.Headers);
// A real service would:
// - check the credentials in the database
// - get user informations (claims)
// - build the ClaimsIdentity and ClaimsPrincipal
//
// Read again the previous part of the article as a reminder
var claimsPrincipal = await this._service.GetClaimsPrincipalAsync(credentials.Username, credentials.Password, AuthenticationScheme);
if (claimsPrincipal != null) {
// Prefer a custom exception
throw new UnauthorizedAccessException("Invalid credentials, login and password don't match");
}
var ticket = new AuthenticationTicket(claimsPrincipal, AuthenticationScheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
catch(Exception exception) {
return Task.FromResult(AuthenticateResult.Fail(exception.Message));
}
}
private object GetUserCredentials(IHeaderDictionary headers)
{
if (!headers.ContainsKey("Authorization"))
{
throw new FormatException("Missing Authorization Header");
}
AuthenticationHeaderValue parsedHeader;
try {
var rawHeader = headers["Authorization"];
parsedHeader = AuthenticationHeaderValue.Parse(rawHeader);
} catch(Exception) {
throw new FormatException("Format is invalid, must be \"{scheme} {token}\"");
}
if (parsedHeader.Scheme != "Basic") {
throw new FormatException($"Scheme must be Basic (found: {parsedHeader.Scheme})");
}
string decodedToken;
try {
decodedToken = Encoding.UTF8.GetString(Convert.FromBase64String(parsedHeader.Parameter));
}
catch (Exception) {
throw new FormatException("Token encoding isn't valid, use base64");
}
const char credentialsDelimiter = ':';
var credentials = decodedToken
// Only split once in case the password contains the delimiter
// We assume the login can't contain the delimiter (colon)
.Split(credentialsDelimiter, 2);
if (credentials.Count() != 2) {
throw new FormatException("Token is missing login and/or password, use format \"{login}:{password}\"");
}
return new { // For demonstration purpose, prefer using a class
Username = credentials[0],
Password = credentials[1]
};
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment