Skip to content

Instantly share code, notes, and snippets.

@AaronSadlerUK
Last active August 29, 2022 11:23
Show Gist options
  • Save AaronSadlerUK/11447f7b5031c545e3857a3135795bcd to your computer and use it in GitHub Desktop.
Save AaronSadlerUK/11447f7b5031c545e3857a3135795bcd to your computer and use it in GitHub Desktop.
YubiKey OTP - Umbraco V10
{
"YubiKey": {
"ApiUrl": "https://api.yubico.com/wsapi/2.0/verify?",
"ClientId": "",
"SecretKey": ""
}
}
public interface IValidateYubiKeyOTPService
{
Task<bool> ValidateYubiKey(string otp);
}
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;
}
}
public class RegisterValidateYubiKeyOTPService : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.Configure<YubiKeyConfiguration>(builder.Config.GetSection(YubiKeyConfiguration.YubiKey));
builder.Services.AddTransient<IValidateYubiKeyOTPService, ValidateYubiKeyOTPService>();
}
}
@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>
}
}
}
}
}
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;
}
}
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;
}
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