Last active
August 29, 2022 11:23
-
-
Save AaronSadlerUK/11447f7b5031c545e3857a3135795bcd to your computer and use it in GitHub Desktop.
YubiKey OTP - Umbraco V10
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
{ | |
"YubiKey": { | |
"ApiUrl": "https://api.yubico.com/wsapi/2.0/verify?", | |
"ClientId": "", | |
"SecretKey": "" | |
} | |
} |
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
public interface IValidateYubiKeyOTPService | |
{ | |
Task<bool> ValidateYubiKey(string otp); | |
} |
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 Microsoft.AspNetCore.Mvc; | |
using Umbraco.Cms.Core.Cache; | |
using Umbraco.Cms.Core.Logging; | |
using Umbraco.Cms.Core.Models; | |
using Umbraco.Cms.Core.Routing; | |
using Umbraco.Cms.Core.Security; | |
using Umbraco.Cms.Core.Services; | |
using Umbraco.Cms.Core.Web; | |
using Umbraco.Cms.Infrastructure.Persistence; | |
using Umbraco.Cms.Web.Website.Controllers; | |
public class MemberAuthenticationSurfaceController : SurfaceController | |
{ | |
[HttpPost] | |
public async Task<IActionResult> YubiKeyValidateAndSaveSetup( | |
string providerName, | |
string otp, | |
string? returnUrl = null) | |
{ | |
var member = await _memberManager.GetCurrentMemberAsync(); | |
var yubikeyId = otp.Substring(0, 12); | |
var isValid = await _validateYubiKeyOTPService.ValidateYubiKey(otp); | |
if (isValid && member != null) | |
{ | |
var twoFactorLogin = new TwoFactorLogin | |
{ | |
Confirmed = true, | |
Secret = yubikeyId, | |
UserOrMemberKey = member.Key, | |
ProviderName = providerName, | |
}; | |
await _twoFactorLoginService.SaveAsync(twoFactorLogin); | |
} | |
else | |
{ | |
ModelState.AddModelError(nameof(otp), "YubiKey OTP is not valid"); | |
} | |
return RedirectToLocal(returnUrl); | |
} | |
private IActionResult RedirectToLocal(string? returnUrl) => | |
Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage(); | |
private readonly IMemberManager _memberManager; | |
private readonly ITwoFactorLoginService _twoFactorLoginService; | |
private readonly IValidateYubiKeyOTPService _validateYubiKeyOTPService; | |
public MemberAuthenticationSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider, IValidateYubiKeyOTPService validateYubiKeyOTPService, IMemberManager memberManager, ITwoFactorLoginService twoFactorLoginService) | |
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) | |
{ | |
_validateYubiKeyOTPService = validateYubiKeyOTPService; | |
_memberManager = memberManager; | |
_twoFactorLoginService = twoFactorLoginService; | |
} | |
} |
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
public class RegisterValidateYubiKeyOTPService : IComposer | |
{ | |
public void Compose(IUmbracoBuilder builder) | |
{ | |
builder.Services.Configure<YubiKeyConfiguration>(builder.Config.GetSection(YubiKeyConfiguration.YubiKey)); | |
builder.Services.AddTransient<IValidateYubiKeyOTPService, ValidateYubiKeyOTPService>(); | |
} | |
} |
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 Umbraco.Cms.Core.Services | |
@using Umbraco.Cms.Web.Website.Controllers | |
@using Umbraco.Cms.Web.Website.Models | |
@inject MemberModelBuilderFactory memberModelBuilderFactory; | |
@inject ITwoFactorLoginService twoFactorLoginService | |
@{ | |
// Build a profile model to edit | |
var profileModel = await memberModelBuilderFactory | |
.CreateProfileModel() | |
.BuildForCurrentMemberAsync(); | |
// Show all two factor providers | |
var providerNames = twoFactorLoginService.GetAllProviderNames(); | |
if (providerNames.Any()) | |
{ | |
<div asp-validation-summary="All" class="text-danger"></div> | |
foreach (var providerName in providerNames) | |
{ | |
var setupData = await twoFactorLoginService.GetSetupInfoAsync(profileModel.Key, providerName); | |
if (setupData is null) | |
{ | |
@using (Html.BeginUmbracoForm<UmbTwoFactorLoginController>(nameof(UmbTwoFactorLoginController.Disable))) | |
{ | |
<input type="hidden" name="providerName" value="@providerName"/> | |
<button type="submit">Disable @providerName</button> | |
} | |
} | |
else if(setupData is YubiKeySetupData qrCodeSetupData) | |
{ | |
@using (Html.BeginUmbracoForm<MemberAuthenticationSurfaceController>(nameof(MemberAuthenticationSurfaceController.YubiKeyValidateAndSaveSetup))) | |
{ | |
<h3>Setup @providerName</h3> | |
<input type="text" name="otp" /> | |
<input type="hidden" name="providerName" value="@providerName" /> | |
<button type="submit">Validate & save</button> | |
} | |
} | |
} | |
} | |
} |
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.Security.Cryptography; | |
using System.Text.RegularExpressions; | |
using System.Text; | |
using Microsoft.Extensions.Options; | |
public class ValidateYubiKeyOTPService : IValidateYubiKeyOTPService | |
{ | |
private readonly YubiKeyConfiguration _configuration; | |
public ValidateYubiKeyOTPService(IOptions<YubiKeyConfiguration> configuration) | |
{ | |
_configuration = configuration.Value; | |
} | |
public async Task<bool> ValidateYubiKey(string otp) | |
{ | |
var yubicoApiClientId = _configuration.YubiKey.ClientId; | |
var yubicoApiPrivateKey = _configuration.YubiKey.PrivateKey; | |
var yubikeyValidationUrl = _configuration.YubiKey.ApiUrl; | |
string nonce; | |
//Create the key based on the api key string | |
var privateKey = Convert.FromBase64String(yubicoApiPrivateKey); | |
//Create a nonce | |
using (var random = RandomNumberGenerator.Create()) | |
{ | |
var tmpNonce = new byte[16]; | |
random.GetBytes(tmpNonce); | |
nonce = BitConverter.ToString(tmpNonce).Replace("-", ""); | |
} | |
//Prepare the parameters to be signed (Ordered alphabetically) | |
var verifyParameters = $"id={yubicoApiClientId}&nonce={nonce}&otp={otp}"; | |
string signature; | |
using (var hmac = new HMACSHA1(privateKey)) | |
{ | |
//Create the hmacsha1 | |
var signatureAsByte = hmac.ComputeHash(Encoding.UTF8.GetBytes(verifyParameters)); | |
signature = Convert.ToBase64String(signatureAsByte); | |
} | |
//Add the signature | |
verifyParameters += $"&h={signature}"; | |
var client = new HttpClient(); | |
var url = $"{yubikeyValidationUrl}{verifyParameters}"; | |
var result = await client.GetAsync(url); | |
var response = await result.Content.ReadAsStringAsync(); | |
var m = Regex.Match(response, "status=\\w*", RegexOptions.IgnoreCase); | |
if (m.Success) | |
{ | |
//The response contains a signature (h parameter) which was signed with the same private key | |
//This means we can just calculate the hmacsha1 again (Without the h parameter and with ordering of the parameter) | |
//and then compare the returned signature with the created siganture | |
var lines = response.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).ToList(); | |
var returnedSignature = string.Empty; | |
var returnParameterToCheck = string.Empty; | |
foreach (var item in lines.OrderBy(x => x)) | |
{ | |
if (!string.IsNullOrEmpty(item) && !item.StartsWith("h=")) | |
returnParameterToCheck += $"&{item}"; | |
if (!string.IsNullOrEmpty(item) && item.StartsWith("h=")) | |
returnedSignature = item.Replace("h=", ""); | |
} | |
//Remove the first unnecessary '&' character | |
returnParameterToCheck = returnParameterToCheck.Remove(0, 1); | |
string signatureToCompare; | |
using (var hmac1 = new HMACSHA1(privateKey)) | |
{ | |
signatureToCompare = | |
Convert.ToBase64String(hmac1.ComputeHash(Encoding.UTF8.GetBytes(returnParameterToCheck))); | |
} | |
if (returnedSignature == signatureToCompare) | |
{ | |
return true; | |
} | |
} | |
return false; | |
} | |
} |
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
public class YubiKeyConfiguration | |
{ | |
public const string YubiKey = "YubiKey"; | |
public string ApiUrl { get; init; } = string.Empty; | |
public string ClientId { get; init; } = string.Empty; | |
public string SecretKey { get; init; } = string.Empty; | |
} |
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 Google.Authenticator; | |
using UmbHost.Core.Interfaces.Clients; | |
using Umbraco.Cms.Core.Security; | |
using Umbraco.Cms.Core.Services; | |
/// <summary> | |
/// Model with the required data to setup YubiKey OTP. | |
/// </summary> | |
public class YubiKeySetupData | |
{ | |
/// <summary> | |
/// The secret unique code for the user and this ITwoFactorProvider. | |
/// </summary> | |
public string Secret { get; init; } | |
} | |
/// <summary> | |
/// App Authenticator implementation of the ITwoFactorProvider | |
/// </summary> | |
public class YubikeyUmbracoAppAuthenticator : ITwoFactorProvider | |
{ | |
/// <summary> | |
/// The unique name of the ITwoFactorProvider. This is saved in a constant for reusability. | |
/// </summary> | |
public const string Name = "YubikeyUmbracoAppAuthenticator"; | |
private readonly IValidateYubiKeyOTPService _validateYubiKeyOtpService; | |
/// <summary> | |
/// Initializes a new instance of the <see cref="YubikeyUmbracoAppAuthenticator"/> class. | |
/// </summary> | |
public YubikeyUmbracoAppAuthenticator(IValidateYubiKeyOTPService validateYubiKeyOtpService) | |
{ | |
_validateYubiKeyOtpService = validateYubiKeyOtpService; | |
} | |
/// <summary> | |
/// The unique provider name of ITwoFactorProvider implementation. | |
/// </summary> | |
/// <remarks> | |
/// This value will be saved in the database to connect the member with this ITwoFactorProvider. | |
/// </remarks> | |
public string ProviderName => Name; | |
/// <summary> | |
/// Returns the required data to setup this specific ITwoFactorProvider implementation. | |
/// </summary> | |
/// <param name="userOrMemberKey">The key of the user or member</param> | |
/// <param name="secret">The YubiKey id that ensures only this user can connect use the YubiKey provided</param> | |
/// <returns>The required data to setup the YubiKey OTP</returns> | |
public Task<object> GetSetupDataAsync(Guid userOrMemberKey, string secret) | |
{ | |
return Task.FromResult<object>(new YubiKeySetupData() | |
{ | |
Secret = secret | |
}); | |
} | |
/// <summary> | |
/// Validated the code and the secret of the user. | |
/// </summary> | |
public bool ValidateTwoFactorPIN(string secret, string code) | |
{ | |
return secret == code.Substring(0, 12) && _validateYubiKeyOtpService.ValidateYubiKey(code).Result; | |
} | |
/// <summary> | |
/// Validated the two factor setup | |
/// </summary> | |
/// <remarks>Called to confirm the setup of two factor on the user. In this case we confirm in the same way as we login by validating the OTP.</remarks> | |
public bool ValidateTwoFactorSetup(string otp, string token) => ValidateTwoFactorPIN(otp, token); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment