using System; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.Models; using BTCPayServer.Models.ManageViewModels; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace BTCPayServer.Controllers { public partial class ManageController { private const string RecoveryCodesKey = nameof(RecoveryCodesKey); private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; [HttpGet] public async Task TwoFactorAuthentication() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var model = new TwoFactorAuthenticationViewModel { Is2faEnabled = user.TwoFactorEnabled, RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user), }; return View(model); } [HttpGet] public async Task Disable2faWarning() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } if (!user.TwoFactorEnabled) { throw new ApplicationException( $"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'."); } return View("Confirm", new ConfirmModel() { Title = $"Disable two-factor authentication (2FA)", DescriptionHtml = true, Description = $"Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key used in an authenticator app you should reset your authenticator keys.", Action = "Disable 2FA", ActionUrl = Url.ActionLink(nameof(Disable2fa)) }); } [HttpPost] public async Task Disable2fa() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); if (!disable2faResult.Succeeded) { throw new ApplicationException( $"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'."); } _logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id); return RedirectToAction(nameof(TwoFactorAuthentication)); } [HttpGet] public async Task EnableAuthenticator() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var model = new EnableAuthenticatorViewModel(); await LoadSharedKeyAndQrCodeUriAsync(user, model); return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task EnableAuthenticator(EnableAuthenticatorViewModel model) { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } if (!ModelState.IsValid) { await LoadSharedKeyAndQrCodeUriAsync(user, model); return View(model); } // Strip spaces and hypens var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase) .Replace("-", string.Empty, StringComparison.OrdinalIgnoreCase); var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); if (!is2faTokenValid) { ModelState.AddModelError("Code", "Verification code is invalid."); await LoadSharedKeyAndQrCodeUriAsync(user, model); return View(model); } await _userManager.SetTwoFactorEnabledAsync(user, true); _logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id); var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); TempData[RecoveryCodesKey] = recoveryCodes.ToArray(); return RedirectToAction(nameof(GenerateRecoveryCodes), new {confirm = false}); } [HttpGet] public IActionResult ResetAuthenticatorWarning() { return View("Confirm", new ConfirmModel() { Title = $"Reset authenticator key", Description = $"This process disables 2FA until you verify your authenticator app and will also reset your 2FA recovery codes.{Environment.NewLine}If you do not complete your authenticator app configuration you may lose access to your account.", Action = "Reset", ActionUrl = Url.ActionLink(nameof(ResetAuthenticator)) }); } [HttpPost] public async Task ResetAuthenticator() { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } await _userManager.SetTwoFactorEnabledAsync(user, false); await _userManager.ResetAuthenticatorKeyAsync(user); _logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id); return RedirectToAction(nameof(EnableAuthenticator)); } [HttpGet] public async Task GenerateRecoveryCodes(bool confirm = true) { if (!confirm) { return await GenerateRecoveryCodes(); } return View("Confirm", new ConfirmModel() { Title = $"Are you sure you want to generate new recovery codes?", Description = "Your existing recovery codes will no longer be valid!", Action = "Generate", ActionUrl = Url.ActionLink(nameof(GenerateRecoveryCodes)) }); } [HttpPost] public async Task GenerateRecoveryCodes() { var recoveryCodes = (string[])TempData[RecoveryCodesKey]; if (recoveryCodes == null) { var user = await _userManager.GetUserAsync(User); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } recoveryCodes = (await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10)).ToArray(); } var model = new GenerateRecoveryCodesViewModel {RecoveryCodes = recoveryCodes}; return View(model); } private string GenerateQrCodeUri(string email, string unformattedKey) { return string.Format(CultureInfo.InvariantCulture, AuthenicatorUriFormat, _urlEncoder.Encode("BTCPayServer"), _urlEncoder.Encode(email), unformattedKey); } private string FormatKey(string unformattedKey) { var result = new StringBuilder(); int currentPosition = 0; while (currentPosition + 4 < unformattedKey.Length) { result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); currentPosition += 4; } if (currentPosition < unformattedKey.Length) { result.Append(unformattedKey.Substring(currentPosition)); } return result.ToString().ToLowerInvariant(); } private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user, EnableAuthenticatorViewModel model) { var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); if (string.IsNullOrEmpty(unformattedKey)) { await _userManager.ResetAuthenticatorKeyAsync(user); unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); } model.SharedKey = FormatKey(unformattedKey); model.AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); } } }