using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using BTCPayServer.Models; using BTCPayServer.Models.AccountViewModels; using BTCPayServer.Services; using BTCPayServer.Services.Mails; using BTCPayServer.Services.Stores; using BTCPayServer.Logging; using BTCPayServer.Security; using System.Globalization; using NicolasDorier.RateLimits; namespace BTCPayServer.Controllers { [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] [Route("[controller]/[action]")] public class AccountController : Controller { private readonly UserManager _userManager; private readonly SignInManager _signInManager; private readonly IEmailSender _emailSender; StoreRepository storeRepository; RoleManager _RoleManager; SettingsRepository _SettingsRepository; Configuration.BTCPayServerOptions _Options; ILogger _logger; public AccountController( UserManager userManager, RoleManager roleManager, StoreRepository storeRepository, SignInManager signInManager, IEmailSender emailSender, SettingsRepository settingsRepository, Configuration.BTCPayServerOptions options) { this.storeRepository = storeRepository; _userManager = userManager; _signInManager = signInManager; _emailSender = emailSender; _RoleManager = roleManager; _SettingsRepository = settingsRepository; _Options = options; _logger = Logs.PayServer; } [TempData] public string ErrorMessage { get; set; } [HttpGet] [AllowAnonymous] public async Task Login(string returnUrl = null) { // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); ViewData["ReturnUrl"] = returnUrl; return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] [RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)] public async Task Login(LoginViewModel model, string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; if (ModelState.IsValid) { // Require the user to have a confirmed email before they can log on. var user = await _userManager.FindByEmailAsync(model.Email); if (user != null) { if (user.RequiresEmailConfirmation && !await _userManager.IsEmailConfirmedAsync(user)) { ModelState.AddModelError(string.Empty, "You must have a confirmed email to log in."); return View(model); } } // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true); if (result.Succeeded) { _logger.LogInformation("User logged in."); return RedirectToLocal(returnUrl); } if (result.RequiresTwoFactor) { return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe }); } if (result.IsLockedOut) { _logger.LogWarning("User account locked out."); return RedirectToAction(nameof(Lockout)); } else { ModelState.AddModelError(string.Empty, "Invalid login attempt."); return View(model); } } // If we got this far, something failed, redisplay form return View(model); } [HttpGet] [AllowAnonymous] public async Task LoginWith2fa(bool rememberMe, string returnUrl = null) { // Ensure the user has gone through the username & password screen first var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); if (user == null) { throw new ApplicationException($"Unable to load two-factor authentication user."); } var model = new LoginWith2faViewModel { RememberMe = rememberMe }; ViewData["ReturnUrl"] = returnUrl; return View(model); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null) { if (!ModelState.IsValid) { return View(model); } var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture); var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine); if (result.Succeeded) { _logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id); return RedirectToLocal(returnUrl); } else if (result.IsLockedOut) { _logger.LogWarning("User with ID {UserId} account locked out.", user.Id); return RedirectToAction(nameof(Lockout)); } else { _logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id); ModelState.AddModelError(string.Empty, "Invalid authenticator code."); return View(); } } [HttpGet] [AllowAnonymous] public async Task LoginWithRecoveryCode(string returnUrl = null) { // Ensure the user has gone through the username & password screen first var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); if (user == null) { throw new ApplicationException($"Unable to load two-factor authentication user."); } ViewData["ReturnUrl"] = returnUrl; return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null) { if (!ModelState.IsValid) { return View(model); } var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); if (user == null) { throw new ApplicationException($"Unable to load two-factor authentication user."); } var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty, StringComparison.InvariantCulture); var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); if (result.Succeeded) { _logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id); return RedirectToLocal(returnUrl); } if (result.IsLockedOut) { _logger.LogWarning("User with ID {UserId} account locked out.", user.Id); return RedirectToAction(nameof(Lockout)); } else { _logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id); ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); return View(); } } [HttpGet] [AllowAnonymous] public IActionResult Lockout() { return View(); } [HttpGet] [AllowAnonymous] public async Task Register(string returnUrl = null, bool logon = true) { var policies = await _SettingsRepository.GetSettingAsync() ?? new PoliciesSettings(); if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin)) return RedirectToAction(nameof(HomeController.Index), "Home"); ViewData["ReturnUrl"] = returnUrl; ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(); return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task Register(RegisterViewModel model, string returnUrl = null, bool logon = true) { ViewData["ReturnUrl"] = returnUrl; ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(); var policies = await _SettingsRepository.GetSettingAsync() ?? new PoliciesSettings(); if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin)) return RedirectToAction(nameof(HomeController.Index), "Home"); if (ModelState.IsValid) { var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail }; var result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin); Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}"); if (admin.Count == 0) { await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin)); await _userManager.AddToRoleAsync(user, Roles.ServerAdmin); if(_Options.DisableRegistration) { // Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users). policies.LockSubscription = true; await _SettingsRepository.UpdateSetting(policies); } } var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); RegisteredUserId = user.Id; await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl); if (!policies.RequiresConfirmedEmail) { if(logon) await _signInManager.SignInAsync(user, isPersistent: false); return RedirectToLocal(returnUrl); } else { TempData["StatusMessage"] = "Account created, please confirm your email"; return View(); } } AddErrors(result); } // If we got this far, something failed, redisplay form return View(model); } /// /// Test property /// public string RegisteredUserId { get; set; } [HttpGet] public async Task Logout() { await _signInManager.SignOutAsync(); _logger.LogInformation("User logged out."); return RedirectToAction(nameof(HomeController.Index), "Home"); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public IActionResult ExternalLogin(string provider, string returnUrl = null) { // Request a redirect to the external login provider. var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl }); var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); return Challenge(properties, provider); } [HttpGet] [AllowAnonymous] public async Task ExternalLoginCallback(string returnUrl = null, string remoteError = null) { if (remoteError != null) { ErrorMessage = $"Error from external provider: {remoteError}"; return RedirectToAction(nameof(Login)); } var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) { return RedirectToAction(nameof(Login)); } // Sign in the user with this external login provider if the user already has a login. var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); if (result.Succeeded) { _logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider); return RedirectToLocal(returnUrl); } if (result.IsLockedOut) { return RedirectToAction(nameof(Lockout)); } else { // If the user does not have an account, then ask the user to create an account. ViewData["ReturnUrl"] = returnUrl; ViewData["LoginProvider"] = info.LoginProvider; var email = info.Principal.FindFirstValue(ClaimTypes.Email); return View("ExternalLogin", new ExternalLoginViewModel { Email = email }); } } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null) { if (ModelState.IsValid) { // Get the information about the user from the external login provider var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) { throw new ApplicationException("Error loading external login information during confirmation."); } var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; var result = await _userManager.CreateAsync(user); if (result.Succeeded) { result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: false); _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); return RedirectToLocal(returnUrl); } } AddErrors(result); } ViewData["ReturnUrl"] = returnUrl; return View(nameof(ExternalLogin), model); } [HttpGet] [AllowAnonymous] public async Task ConfirmEmail(string userId, string code) { if (userId == null || code == null) { return RedirectToAction(nameof(HomeController.Index), "Home"); } var user = await _userManager.FindByIdAsync(userId); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{userId}'."); } var result = await _userManager.ConfirmEmailAsync(user, code); return View(result.Succeeded ? "ConfirmEmail" : "Error"); } [HttpGet] [AllowAnonymous] public IActionResult ForgotPassword() { return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task ForgotPassword(ForgotPasswordViewModel model) { if (ModelState.IsValid) { var user = await _userManager.FindByEmailAsync(model.Email); if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) { // Don't reveal that the user does not exist or is not confirmed return RedirectToAction(nameof(ForgotPasswordConfirmation)); } // For more information on how to enable account confirmation and password reset please // visit https://go.microsoft.com/fwlink/?LinkID=532713 var code = await _userManager.GeneratePasswordResetTokenAsync(user); var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme); await _emailSender.SendEmailAsync(model.Email, "Reset Password", $"Please reset your password by clicking here: link"); return RedirectToAction(nameof(ForgotPasswordConfirmation)); } // If we got this far, something failed, redisplay form return View(model); } [HttpGet] [AllowAnonymous] public IActionResult ForgotPasswordConfirmation() { return View(); } [HttpGet] [AllowAnonymous] public IActionResult ResetPassword(string code = null) { if (code == null) { throw new ApplicationException("A code must be supplied for password reset."); } var model = new ResetPasswordViewModel { Code = code }; return View(model); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task ResetPassword(ResetPasswordViewModel model) { if (!ModelState.IsValid) { return View(model); } var user = await _userManager.FindByEmailAsync(model.Email); if (user == null) { // Don't reveal that the user does not exist return RedirectToAction(nameof(ResetPasswordConfirmation)); } var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password); if (result.Succeeded) { return RedirectToAction(nameof(ResetPasswordConfirmation)); } AddErrors(result); return View(); } [HttpGet] [AllowAnonymous] public IActionResult ResetPasswordConfirmation() { return View(); } [HttpGet] public IActionResult AccessDenied() { return View(); } #region Helpers private void AddErrors(IdentityResult result) { foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } private IActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction(nameof(HomeController.Index), "Home"); } } #endregion } }