Last active
April 24, 2020 08:08
-
-
Save jbardon/ce5a896004c96ce377e8068ee79a047b to your computer and use it in GitHub Desktop.
.NET Core Http Basic authentication handler
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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