using System.Globalization; using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Identity; using Finch.Rendering.QrCode; namespace Finch.Identity; public static class UserManagerExtensions { private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits={3}&algorithm={4}&period={5}"; /// /// Adds an implementation of identity information stores. /// public static Task GetTwoFactorKey(this UserManager userManager, T user, string issuer) where T : class => GetTwoFactorKey(userManager, user, new TwoFactorKeyOptions() { Issuer = issuer }); /// /// Adds an implementation of identity information stores. /// public static async Task GetTwoFactorKey(this UserManager userManager, T user, TwoFactorKeyOptions options) where T : class { string unformattedKey = await userManager.GetAuthenticatorKeyAsync(user); string email = await userManager.GetEmailAsync(user); if (unformattedKey.IsNullOrEmpty()) { await userManager.ResetAuthenticatorKeyAsync(user); unformattedKey = await userManager.GetAuthenticatorKeyAsync(user); } string url = string.Format( CultureInfo.InvariantCulture, AuthenticatorUriFormat, UrlEncoder.Default.Encode(options.Issuer), UrlEncoder.Default.Encode(email), unformattedKey, (int)options.Digits, options.Algorithm.ToString(), (int)options.Period); QrCode qr = QrCode.EncodeText(url, QrCode.Ecc.Medium); return new TwoFactorKey() { UserEmail = email, UnformattedKey = unformattedKey, FormattedKey = FormatTwoFactorAuthenticationKey(unformattedKey), AuthenticatorUrl = url, QrCode = qr, Options = options }; } public static async Task> SetupTwoFactorAuth(this UserManager userManager, T user, string inputCode, int recoveryCodesToGenerate = 10) where T : class { string token = inputCode.Replace(" ", string.Empty).Replace("-", string.Empty); bool is2FaTokenValid = await userManager.VerifyTwoFactorTokenAsync(user, userManager.Options.Tokens.AuthenticatorTokenProvider, token); if (!is2FaTokenValid) { return Result.Fail(); } // enable two-factor auth for account await userManager.SetTwoFactorEnabledAsync(user, true); // generate recovery codes if (await userManager.CountRecoveryCodesAsync(user) == 0) { IEnumerable recoveryCodes = await userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, recoveryCodesToGenerate); return Result.Success(recoveryCodes.ToArray()); } return Result.Success(Array.Empty()); } private static string FormatTwoFactorAuthenticationKey(string unformattedKey) { var result = new StringBuilder(); int currentPosition = 0; while (currentPosition + 4 < unformattedKey.Length) { result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' '); currentPosition += 4; } if (currentPosition < unformattedKey.Length) { result.Append(unformattedKey.AsSpan(currentPosition)); } return result.ToString().ToLowerInvariant(); } }