btcpayserver/BTCPayServer/Controllers/AccountController.cs

659 lines
24 KiB
C#
Raw Normal View History

2020-06-29 04:44:35 +02:00
using System;
2020-06-28 10:55:27 +02:00
using System.Globalization;
2017-09-13 08:47:34 +02:00
using System.Threading.Tasks;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Logging;
2017-09-13 08:47:34 +02:00
using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Security;
2017-09-13 08:47:34 +02:00
using BTCPayServer.Services;
using BTCPayServer.U2F;
using BTCPayServer.U2F.Models;
2020-06-28 10:55:27 +02:00
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NicolasDorier.RateLimits;
2020-01-14 09:48:06 +01:00
using U2F.Core.Exceptions;
2017-09-13 08:47:34 +02:00
namespace BTCPayServer.Controllers
{
2019-10-12 13:35:30 +02:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("[controller]/[action]")]
public class AccountController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
readonly RoleManager<IdentityRole> _RoleManager;
readonly SettingsRepository _SettingsRepository;
readonly Configuration.BTCPayServerOptions _Options;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
2020-02-29 06:15:14 +01:00
public U2FService _u2FService;
private readonly EventAggregator _eventAggregator;
readonly ILogger _logger;
public AccountController(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
SignInManager<ApplicationUser> signInManager,
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options,
BTCPayServerEnvironment btcPayServerEnvironment,
U2FService u2FService,
EventAggregator eventAggregator)
{
_userManager = userManager;
_signInManager = signInManager;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_Options = options;
_btcPayServerEnvironment = btcPayServerEnvironment;
_u2FService = u2FService;
_eventAggregator = eventAggregator;
2018-01-14 13:48:23 +01:00
_logger = Logs.PayServer;
}
[TempData]
public string ErrorMessage
{
get; set;
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Login(string returnUrl = null, string email = null)
{
2020-02-29 06:15:14 +01:00
if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl))
return RedirectToLocal();
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
2019-11-16 08:57:16 +01:00
if (!CanLoginOrRegister())
{
SetInsecureFlags();
}
2020-02-29 06:15:14 +01:00
ViewData["ReturnUrl"] = returnUrl;
return View(new LoginViewModel()
{
Email = email
});
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
if (!CanLoginOrRegister())
{
2019-11-16 08:57:16 +01:00
return RedirectToAction("Login");
}
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);
}
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id))
{
if (await _userManager.CheckPasswordAsync(user, model.Password))
{
LoginWith2faViewModel twoFModel = null;
2020-02-29 06:15:14 +01:00
if (user.TwoFactorEnabled)
{
// we need to do an actual sign in attempt so that 2fa can function in next step
await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
twoFModel = new LoginWith2faViewModel
{
RememberMe = model.RememberMe
};
}
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = twoFModel,
LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user)
});
}
else
{
2020-02-29 06:15:14 +01:00
var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user);
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}
2020-02-29 06:15:14 +01:00
2018-09-12 13:36:44 +02:00
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 View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = new LoginWith2faViewModel()
{
RememberMe = 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);
}
private async Task<LoginWithU2FViewModel> BuildU2FViewModel(bool rememberMe, ApplicationUser user)
{
if (_btcPayServerEnvironment.IsSecure)
{
var u2fChallenge = await _u2FService.GenerateDeviceChallenges(user.Id,
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
return new LoginWithU2FViewModel()
{
Version = u2fChallenge[0].version,
Challenge = u2fChallenge[0].challenge,
Challenges = u2fChallenge,
AppId = u2fChallenge[0].appId,
UserId = user.Id,
RememberMe = rememberMe
};
}
return null;
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWithU2F(LoginWithU2FViewModel viewModel, string returnUrl = null)
{
if (!CanLoginOrRegister())
{
return RedirectToAction("Login");
}
2020-02-29 06:15:14 +01:00
ViewData["ReturnUrl"] = returnUrl;
var user = await _userManager.FindByIdAsync(viewModel.UserId);
if (user == null)
{
return NotFound();
}
var errorMessage = string.Empty;
try
{
if (await _u2FService.AuthenticateUser(viewModel.UserId, viewModel.DeviceResponse))
{
await _signInManager.SignInAsync(user, viewModel.RememberMe, "U2F");
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
errorMessage = "Invalid login attempt.";
}
2020-01-14 09:48:06 +01:00
catch (U2fException e)
{
errorMessage = e.Message;
}
ModelState.AddModelError(string.Empty, errorMessage);
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWithU2FViewModel = viewModel,
LoginWith2FaViewModel = !user.TwoFactorEnabled
? null
: new LoginWith2faViewModel()
{
RememberMe = viewModel.RememberMe
}
});
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
{
if (!CanLoginOrRegister())
{
return RedirectToAction("Login");
}
// 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("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
2020-02-29 06:15:14 +01:00
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
});
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
{
if (!CanLoginOrRegister())
{
return RedirectToAction("Login");
}
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)}'.");
}
2018-02-17 05:18:16 +01:00
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("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = model,
2020-02-29 06:15:14 +01:00
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
});
}
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null)
{
if (!CanLoginOrRegister())
{
return RedirectToAction("Login");
}
// 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<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
{
if (!CanLoginOrRegister())
{
return RedirectToAction("Login");
}
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
2018-02-17 05:18:16 +01:00
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]
[RateLimitsFilter(ZoneLimits.Register, Scope = RateLimitsScope.RemoteAddress)]
2020-07-02 19:32:24 +02:00
public async Task<IActionResult> Register(string returnUrl = null, bool logon = true)
{
2019-11-16 08:57:16 +01:00
if (!CanLoginOrRegister())
{
SetInsecureFlags();
}
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
2017-12-03 16:55:39 +01:00
return RedirectToAction(nameof(HomeController.Index), "Home");
ViewData["ReturnUrl"] = returnUrl;
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
2018-08-01 17:16:16 +02:00
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null, bool logon = true)
{
if (!CanLoginOrRegister())
{
return RedirectToAction("Register");
}
ViewData["ReturnUrl"] = returnUrl;
2018-08-01 17:16:16 +02:00
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
2017-12-03 16:55:39 +01:00
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
2017-12-03 16:55:39 +01:00
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);
if (admin.Count == 0 || (model.IsAdmin && _Options.AllowAdminRegistration))
{
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>();
settings.FirstRun = false;
await _SettingsRepository.UpdateSetting<ThemeSettings>(settings);
await _SettingsRepository.FirstAdminRegistered(policies, _Options.UpdateUrl != null, _Options.DisableRegistration);
RegisteredAdmin = true;
}
_eventAggregator.Publish(new UserRegisteredEvent()
{
RequestUri = Request.GetAbsoluteRootUri(),
User = user,
Admin = RegisteredAdmin
});
RegisteredUserId = user.Id;
2019-01-06 15:53:37 +01:00
if (!policies.RequiresConfirmedEmail)
{
2020-02-29 06:15:14 +01:00
if (logon)
2018-08-01 17:16:16 +02:00
await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
else
{
2019-10-31 07:19:38 +01:00
TempData[WellKnownTempData.SuccessMessage] = "Account created, please confirm your email";
return View();
}
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
// Properties used by tests
public string RegisteredUserId { get; set; }
public bool RegisteredAdmin { get; set; }
[HttpGet]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
return RedirectToAction(nameof(HomeController.Index), "Home");
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> 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);
if (!await _userManager.HasPasswordAsync(user))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Info,
Message = "Your email has been confirmed but you still need to set your password."
});
return RedirectToAction("SetPassword", new { email = user.Email, code= await _userManager.GeneratePasswordResetTokenAsync(user)});
}
if (result.Succeeded)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = "Your email has been confirmed."
});
return RedirectToAction("Login", new {email = user.Email});
}
return View("Error");
}
[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPassword()
{
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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));
}
_eventAggregator.Publish(new UserPasswordResetRequestedEvent()
{
User = user, RequestUri = Request.GetAbsoluteRootUri()
});
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 async Task<IActionResult> SetPassword(string code = null, string userId = null, string email = null)
{
if (code == null)
{
throw new ApplicationException("A code must be supplied for password reset.");
}
if (!string.IsNullOrEmpty(userId))
{
var user = await _userManager.FindByIdAsync(userId);
email = user?.Email;
}
var model = new SetPasswordViewModel {Code = code, Email = email, EmailSetInternally = !string.IsNullOrEmpty(email)};
return View(model);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SetPassword(SetPasswordViewModel 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(Login));
}
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
if (result.Succeeded)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success, Message = "Password successfully set."
});
return RedirectToAction(nameof(Login));
}
AddErrors(result);
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 = null)
{
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction(nameof(HomeController.Index), "Home");
}
}
2020-02-29 06:15:14 +01:00
private bool CanLoginOrRegister()
{
2020-07-31 03:43:44 +02:00
return _btcPayServerEnvironment.IsDeveloping || _btcPayServerEnvironment.IsSecure;
2019-11-16 08:57:16 +01:00
}
private void SetInsecureFlags()
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "You cannot login over an insecure connection. Please use HTTPS or Tor."
});
2020-02-29 06:15:14 +01:00
ViewData["disabled"] = true;
}
#endregion
}
2017-09-13 08:47:34 +02:00
}