Files
Umbraco-CMS/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs

696 lines
28 KiB
C#
Raw Normal View History

2021-09-20 11:30:09 +02:00
using System.Globalization;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Mail;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
2021-06-04 09:50:49 +02:00
using Umbraco.Cms.Core.Models.Email;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Net;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Security;
using Umbraco.Cms.Web.BackOffice.Filters;
using Umbraco.Cms.Web.BackOffice.Security;
2021-02-10 11:42:04 +01:00
using Umbraco.Cms.Web.Common.ActionsResults;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Cms.Web.Common.Controllers;
using Umbraco.Cms.Web.Common.Filters;
using Umbraco.Cms.Web.Common.Models;
using Umbraco.Extensions;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
// See
// for a bigger example of this type of controller implementation in netcore:
// https://github.com/dotnet/AspNetCore.Docs/blob/2efb4554f8f659be97ee7cd5dd6143b871b330a5/aspnetcore/migration/1x-to-2x/samples/AspNetCoreDotNetCore2App/AspNetCoreDotNetCore2App/Controllers/AccountController.cs
// https://github.com/dotnet/AspNetCore.Docs/blob/ad16f5e1da6c04fa4996ee67b513f2a90fa0d712/aspnetcore/common/samples/WebApplication1/Controllers/AccountController.cs
// with authenticator app
// https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Controllers/AccountController.cs
[PluginController(Constants.Web.Mvc
.BackOfficeApiArea)] // TODO: Maybe this could be applied with our Application Model conventions
//[ValidationFilter] // TODO: I don't actually think this is required with our custom Application Model conventions applied
[AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions
[IsBackOffice]
[DisableBrowserCache]
public class AuthenticationController : UmbracoApiControllerBase
{
// NOTE: Each action must either be explicitly authorized or explicitly [AllowAnonymous], the latter is optional because
// this controller itself doesn't require authz but it's more clear what the intention is.
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions;
private readonly IEmailSender _emailSender;
private readonly IBackOfficeExternalLoginProviders _externalAuthenticationOptions;
private readonly GlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IIpResolver _ipResolver;
private readonly LinkGenerator _linkGenerator;
private readonly ILogger<AuthenticationController> _logger;
private readonly UserPasswordConfigurationSettings _passwordConfiguration;
private readonly SecuritySettings _securitySettings;
private readonly IBackOfficeSignInManager _signInManager;
private readonly ISmsSender _smsSender;
private readonly ILocalizedTextService _textService;
private readonly ITwoFactorLoginService _twoFactorLoginService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly IBackOfficeUserManager _userManager;
private readonly IUserService _userService;
private readonly WebRoutingSettings _webRoutingSettings;
// TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here
[ActivatorUtilitiesConstructor]
public AuthenticationController(
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IBackOfficeUserManager backOfficeUserManager,
IBackOfficeSignInManager signInManager,
IUserService userService,
ILocalizedTextService textService,
IUmbracoMapper umbracoMapper,
IOptionsSnapshot<GlobalSettings> globalSettings,
IOptionsSnapshot<SecuritySettings> securitySettings,
ILogger<AuthenticationController> logger,
IIpResolver ipResolver,
IOptionsSnapshot<UserPasswordConfigurationSettings> passwordConfiguration,
IEmailSender emailSender,
ISmsSender smsSender,
IHostingEnvironment hostingEnvironment,
LinkGenerator linkGenerator,
IBackOfficeExternalLoginProviders externalAuthenticationOptions,
IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions,
IHttpContextAccessor httpContextAccessor,
IOptions<WebRoutingSettings> webRoutingSettings,
ITwoFactorLoginService twoFactorLoginService)
{
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_userManager = backOfficeUserManager;
_signInManager = signInManager;
_userService = userService;
_textService = textService;
_umbracoMapper = umbracoMapper;
_globalSettings = globalSettings.Value;
_securitySettings = securitySettings.Value;
_logger = logger;
_ipResolver = ipResolver;
_passwordConfiguration = passwordConfiguration.Value;
_emailSender = emailSender;
_smsSender = smsSender;
_hostingEnvironment = hostingEnvironment;
_linkGenerator = linkGenerator;
_externalAuthenticationOptions = externalAuthenticationOptions;
_backOfficeTwoFactorOptions = backOfficeTwoFactorOptions;
_httpContextAccessor = httpContextAccessor;
_webRoutingSettings = webRoutingSettings.Value;
_twoFactorLoginService = twoFactorLoginService;
}
/// <summary>
/// Returns the configuration for the backoffice user membership provider - used to configure the change password
/// dialog
/// </summary>
[AllowAnonymous] // Needed for users that are invited when they use the link from the mail they are not authorized
[Authorize(Policy =
AuthorizationPolicies.BackOfficeAccess)] // Needed to enforce the principle set on the request, if one exists.
public IDictionary<string, object> GetPasswordConfig(int userId)
{
Attempt<int> currentUserId =
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId() ?? Attempt<int>.Fail();
return _passwordConfiguration.GetConfiguration(
currentUserId.Success
? currentUserId.Result != userId
: true);
}
/// <summary>
/// Checks if a valid token is specified for an invited user and if so logs the user in and returns the user object
/// </summary>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
/// <remarks>
/// This will also update the security stamp for the user so it can only be used once
/// </remarks>
[ValidateAngularAntiForgeryToken]
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
public async Task<ActionResult<UserDisplay?>> PostVerifyInvite([FromQuery] int id, [FromQuery] string token)
{
if (string.IsNullOrWhiteSpace(token))
{
return NotFound();
}
var decoded = token.FromUrlBase64();
if (decoded.IsNullOrWhiteSpace())
{
return NotFound();
}
BackOfficeIdentityUser? identityUser = await _userManager.FindByIdAsync(id.ToString());
if (identityUser == null)
{
return NotFound();
}
IdentityResult result = await _userManager.ConfirmEmailAsync(identityUser, decoded!);
if (result.Succeeded == false)
{
return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Errors.ToErrorMessage());
}
await _signInManager.SignOutAsync();
2020-10-23 14:18:53 +11:00
await _signInManager.SignInAsync(identityUser, false);
2020-10-23 14:18:53 +11:00
IUser? user = _userService.GetUserById(id);
return _umbracoMapper.Map<UserDisplay>(user);
}
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
[ValidateAngularAntiForgeryToken]
public async Task<IActionResult> PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel)
{
V11/feature/update to dotnet 7 (#12712) * Update projects to .NET 7 * Fix nullability errors * Fix up pipelines to run 7.0 * Update langversion to preview * Revert "Fix up pipelines to run 7.0" This reverts commit d0fa8d01b8126a4eaa59832a3814a567705419ae. * Fix up pipelines again, this time without indentation changes * Include preview versions * Versions not Version * Fix ModelTypeTests * Fix MemberPasswordHasherTests Microsoft wants to use SHA512 instead of SHA256, so our old hashes will return SuccessRehashNeeded now * Use dotnet cli instead of nuget restore * Update src/Umbraco.Web.UI/Umbraco.Web.UI.csproj * Update dependencies * Fix nullability issues * Fix unit test * Fix nullability in ChangingPasswordModel OldPassword can be null, if we're changing the password with password reset enabled. Additionally, we might as well use the new required keyword instead of supressing null. * Use required keyword instead of supressing null * Fix up pipelines again * fix up spelling-error * Use dotnet cli instead of nuget restore * Fix up another NuGet command * Use dotnet version 7 before building * Include preview versions * Remove condition * Use dotnet 7 before running powershell script * Update templates to .net 7 * Download version 7 before running linux container * Move use dotnet 7 even earlier in E2E process * Remove dotnet 7 * Reintroduce .NET 7 task * Update linux docker container and remove dotnet 7 from yml * Fix up dockerfile with ARG * Fix up docker file with nightly builds of dotnet 7 * Reintroduce dotnet 7 so windows can use it * Use aspnet 7 in docker Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch> Co-authored-by: Zeegaan <nge@umbraco.dk>
2022-08-23 11:31:05 +02:00
var userId = User.Identity?.GetUserId();
if (userId is null)
{
V11/feature/update to dotnet 7 (#12712) * Update projects to .NET 7 * Fix nullability errors * Fix up pipelines to run 7.0 * Update langversion to preview * Revert "Fix up pipelines to run 7.0" This reverts commit d0fa8d01b8126a4eaa59832a3814a567705419ae. * Fix up pipelines again, this time without indentation changes * Include preview versions * Versions not Version * Fix ModelTypeTests * Fix MemberPasswordHasherTests Microsoft wants to use SHA512 instead of SHA256, so our old hashes will return SuccessRehashNeeded now * Use dotnet cli instead of nuget restore * Update src/Umbraco.Web.UI/Umbraco.Web.UI.csproj * Update dependencies * Fix nullability issues * Fix unit test * Fix nullability in ChangingPasswordModel OldPassword can be null, if we're changing the password with password reset enabled. Additionally, we might as well use the new required keyword instead of supressing null. * Use required keyword instead of supressing null * Fix up pipelines again * fix up spelling-error * Use dotnet cli instead of nuget restore * Fix up another NuGet command * Use dotnet version 7 before building * Include preview versions * Remove condition * Use dotnet 7 before running powershell script * Update templates to .net 7 * Download version 7 before running linux container * Move use dotnet 7 even earlier in E2E process * Remove dotnet 7 * Reintroduce .NET 7 task * Update linux docker container and remove dotnet 7 from yml * Fix up dockerfile with ARG * Fix up docker file with nightly builds of dotnet 7 * Reintroduce dotnet 7 so windows can use it * Use aspnet 7 in docker Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch> Co-authored-by: Zeegaan <nge@umbraco.dk>
2022-08-23 11:31:05 +02:00
throw new InvalidOperationException("Could not find userId");
}
V11/feature/update to dotnet 7 (#12712) * Update projects to .NET 7 * Fix nullability errors * Fix up pipelines to run 7.0 * Update langversion to preview * Revert "Fix up pipelines to run 7.0" This reverts commit d0fa8d01b8126a4eaa59832a3814a567705419ae. * Fix up pipelines again, this time without indentation changes * Include preview versions * Versions not Version * Fix ModelTypeTests * Fix MemberPasswordHasherTests Microsoft wants to use SHA512 instead of SHA256, so our old hashes will return SuccessRehashNeeded now * Use dotnet cli instead of nuget restore * Update src/Umbraco.Web.UI/Umbraco.Web.UI.csproj * Update dependencies * Fix nullability issues * Fix unit test * Fix nullability in ChangingPasswordModel OldPassword can be null, if we're changing the password with password reset enabled. Additionally, we might as well use the new required keyword instead of supressing null. * Use required keyword instead of supressing null * Fix up pipelines again * fix up spelling-error * Use dotnet cli instead of nuget restore * Fix up another NuGet command * Use dotnet version 7 before building * Include preview versions * Remove condition * Use dotnet 7 before running powershell script * Update templates to .net 7 * Download version 7 before running linux container * Move use dotnet 7 even earlier in E2E process * Remove dotnet 7 * Reintroduce .NET 7 task * Update linux docker container and remove dotnet 7 from yml * Fix up dockerfile with ARG * Fix up docker file with nightly builds of dotnet 7 * Reintroduce dotnet 7 so windows can use it * Use aspnet 7 in docker Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch> Co-authored-by: Zeegaan <nge@umbraco.dk>
2022-08-23 11:31:05 +02:00
var user = await _userManager.FindByIdAsync(userId);
if (user == null) throw new InvalidOperationException("Could not find user");
AuthenticationScheme? authType = (await _signInManager.GetExternalAuthenticationSchemesAsync())
.FirstOrDefault(x => x.Name == unlinkLoginModel.LoginProvider);
if (authType == null)
{
_logger.LogWarning("Could not find external authentication provider registered: {LoginProvider}", unlinkLoginModel.LoginProvider);
}
else
{
BackOfficeExternaLoginProviderScheme? opt = await _externalAuthenticationOptions.GetAsync(authType.Name);
if (opt == null)
{
return BadRequest(
$"Could not find external authentication options registered for provider {unlinkLoginModel.LoginProvider}");
}
if (!opt.ExternalLoginProvider.Options.AutoLinkOptions.AllowManualLinking)
{
// If AllowManualLinking is disabled for this provider we cannot unlink
return BadRequest();
}
}
IdentityResult result = await _userManager.RemoveLoginAsync(
user,
unlinkLoginModel.LoginProvider,
unlinkLoginModel.ProviderKey);
if (result.Succeeded)
{
await _signInManager.SignInAsync(user, true);
return Ok();
}
AddModelErrors(result);
return new ValidationErrorResult(ModelState);
}
[HttpGet]
[AllowAnonymous]
public async Task<double> GetRemainingTimeoutSeconds()
{
// force authentication to occur since this is not an authorized endpoint
AuthenticateResult result = await this.AuthenticateBackOfficeAsync();
if (!result.Succeeded)
{
return 0;
}
var remainingSeconds = result.Principal.GetRemainingAuthSeconds();
if (remainingSeconds <= 30)
{
var username = result.Principal.FindFirst(ClaimTypes.Name)?.Value;
//NOTE: We are using 30 seconds because that is what is coded into angular to force logout to give some headway in
// the timeout process.
_logger.LogInformation(
"User logged will be logged out due to timeout: {Username}, IP Address: {IPAddress}",
username ?? "unknown",
_ipResolver.GetCurrentRequestIpAddress());
}
return remainingSeconds;
}
/// <summary>
/// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest)
/// </summary>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public async Task<bool> IsAuthenticated()
{
// force authentication to occur since this is not an authorized endpoint
AuthenticateResult result = await this.AuthenticateBackOfficeAsync();
return result.Succeeded;
}
/// <summary>
/// Returns the currently logged in Umbraco user
/// </summary>
/// <returns></returns>
/// <remarks>
/// We have the attribute [SetAngularAntiForgeryTokens] applied because this method is called initially to determine if
/// the user
/// is valid before the login screen is displayed. The Auth cookie can be persisted for up to a day but the csrf
/// cookies are only session
/// cookies which means that the auth cookie could be valid but the csrf cookies are no longer there, in that case we
/// need to re-set the csrf cookies.
/// </remarks>
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
[SetAngularAntiForgeryTokens]
[CheckIfUserTicketDataIsStale]
public UserDetail? GetCurrentUser()
{
IUser? user = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
UserDetail? result = _umbracoMapper.Map<UserDetail>(user);
if (result is not null)
{
//set their remaining seconds
result.SecondsUntilTimeout = HttpContext.User.GetRemainingAuthSeconds();
}
return result;
}
/// <summary>
/// When a user is invited they are not approved but we need to resolve the partially logged on (non approved)
/// user.
/// </summary>
/// <returns></returns>
/// <remarks>
/// We cannot user GetCurrentUser since that requires they are approved, this is the same as GetCurrentUser but doesn't
/// require them to be approved
/// </remarks>
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccessWithoutApproval)]
[SetAngularAntiForgeryTokens]
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
public ActionResult<UserDetail?> GetCurrentInvitedUser()
{
IUser? user = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
if (user?.IsApproved ?? false)
{
// if they are approved, than they are no longer invited and we can return an error
return Forbid();
}
UserDetail? result = _umbracoMapper.Map<UserDetail>(user);
if (result is not null)
{
// set their remaining seconds
result.SecondsUntilTimeout = HttpContext.User.GetRemainingAuthSeconds();
}
return result;
}
/// <summary>
/// Logs a user in
/// </summary>
/// <returns></returns>
[SetAngularAntiForgeryTokens]
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
public async Task<ActionResult<UserDetail?>> PostLogin(LoginModel loginModel)
{
// Sign the user in with username/password, this also gives a chance for developers to
// custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker
SignInResult result = await _signInManager.PasswordSignInAsync(
loginModel.Username, loginModel.Password, true, true);
if (result.Succeeded)
{
// return the user detail
return GetUserDetail(_userService.GetByUsername(loginModel.Username));
}
if (result.RequiresTwoFactor)
{
var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(loginModel.Username);
if (twofactorView.IsNullOrWhiteSpace())
{
return new ValidationErrorResult(
$"The registered {typeof(IBackOfficeTwoFactorOptions)} of type {_backOfficeTwoFactorOptions.GetType()} did not return a view for two factor auth ");
}
IUser? attemptedUser = _userService.GetByUsername(loginModel.Username);
// create a with information to display a custom two factor send code view
var verifyResponse =
new ObjectResult(new { twoFactorView = twofactorView, userId = attemptedUser?.Id })
{
StatusCode = StatusCodes.Status402PaymentRequired
};
return verifyResponse;
}
// TODO: We can check for these and respond differently if we think it's important
// result.IsLockedOut
// result.IsNotAllowed
// return BadRequest (400), we don't want to return a 401 because that get's intercepted
// by our angular helper because it thinks that we need to re-perform the request once we are
// authorized and we don't want to return a 403 because angular will show a warning message indicating
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
return BadRequest();
}
/// <summary>
/// Processes a password reset request. Looks for a match on the provided email address
/// and if found sends an email with a link to reset it
/// </summary>
/// <returns></returns>
[SetAngularAntiForgeryTokens]
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
public async Task<IActionResult> PostRequestPasswordReset(RequestPasswordResetModel model)
{
// If this feature is switched off in configuration the UI will be amended to not make the request to reset password available.
// So this is just a server-side secondary check.
if (_securitySettings.AllowPasswordReset == false)
{
return BadRequest();
}
BackOfficeIdentityUser? identityUser = await _userManager.FindByEmailAsync(model.Email);
if (identityUser != null)
{
IUser? user = _userService.GetByEmail(model.Email);
if (user != null)
{
var from = _globalSettings.Smtp?.From;
var code = await _userManager.GeneratePasswordResetTokenAsync(identityUser);
var callbackUrl = ConstructCallbackUrl(identityUser.Id, code);
var message = _textService.Localize("login", "resetPasswordEmailCopyFormat",
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions.GetUserCulture(identityUser.Culture, _textService, _globalSettings),
new[] { identityUser.UserName, callbackUrl });
var subject = _textService.Localize("login", "resetPasswordEmailCopySubject",
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions.GetUserCulture(identityUser.Culture, _textService, _globalSettings));
var mailMessage = new EmailMessage(from, user.Email, subject, message, true);
await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.PasswordReset);
_userManager.NotifyForgotPasswordRequested(User, user.Id.ToString());
}
}
return Ok();
}
/// <summary>
/// Used to retrieve the 2FA providers for code submission
/// </summary>
/// <returns></returns>
[SetAngularAntiForgeryTokens]
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<string>>> Get2FAProviders()
{
BackOfficeIdentityUser? user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
_logger.LogWarning("Get2FAProviders :: No verified user found, returning 404");
return NotFound();
}
IEnumerable<string> userFactors = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key);
Simplified setup of 2FA for users (#12142) * Added functionality to enable 2FA for users.. * Do not use the obsolete ctor in tests * cleanup * Cleanup * Convert User view from overlay to infinite editor * Add support for having additional editors on top of the user (2fa) which overlay does not support * Add controllerAs syntax in the template * Remove unused dependencies * Adjustments to 2fa login view * organize elements * add translations * add a11y helpers * add autocompletion = one-time-code * change to controllerAs syntax * add callback to cancel 2fa and fix error where submit button was not reset when all other validations were * add a cancel/go back button to the 2fa view * replace header with something less obstrusive * move logout button to the footer in the new editor view * change 'edit profile' to an umb-box and move ng-if for password fields out to reduce amount of checks * Add umb-box to external login provider section * add umb-box to user history section * bug: fix bug where notificationsService would not allow new notifications if removeAll had been called * add styling and a11y to configureTwoFactor view - also ensure that the view reloads when changes happen in the custom user view to enable 2fa - ensure that view updates when disabling 2fa - add extra button to show options (disable) for each 2fa provider * add notification when 2fa is disabled * add data-element to support the intro tour also changed a minor selector in the cypress test * correct usage of umb-box with umb-box-content * do not use the .form class twice to prevent double box-shadow * make tranlastion for 2fa placeholder shorter * ensure that field with 2fa provider is always visible when more than 1 provider * move error state of 2fa field to token field * update translation of multiple 2fa providers * move CTA buttons to right side to follow general UI practices * rename options to disable * add disabled state * add helper folders to gitignore so you can work with plugins and custom code without committing it accidentally * move the disable functionality to its own infinite editor view * use properties from umb-control-group correctly * add 'track by' to repeater * make use of umb-control-group * remove unused functions * clean up translations * add Danish translations * copy translations to english * Only return enabled 2fa providers as expected Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2022-04-19 08:33:03 +02:00
return new ObjectResult(userFactors);
}
[SetAngularAntiForgeryTokens]
[AllowAnonymous]
public async Task<IActionResult> PostSend2FACode([FromBody] string provider)
{
if (provider.IsNullOrWhiteSpace())
{
return NotFound();
}
BackOfficeIdentityUser? user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
_logger.LogWarning("PostSend2FACode :: No verified user found, returning 404");
return NotFound();
}
var from = _globalSettings.Smtp?.From;
// Generate the token and send it
var code = await _userManager.GenerateTwoFactorTokenAsync(user, provider);
if (string.IsNullOrWhiteSpace(code))
{
_logger.LogWarning("PostSend2FACode :: Could not generate 2FA code");
return BadRequest("Invalid code");
}
var subject = _textService.Localize("login", "mfaSecurityCodeSubject",
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions.GetUserCulture(user.Culture, _textService, _globalSettings));
var message = _textService.Localize("login", "mfaSecurityCodeMessage",
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions.GetUserCulture(user.Culture, _textService, _globalSettings),
new[] { code });
V11/feature/update to dotnet 7 (#12712) * Update projects to .NET 7 * Fix nullability errors * Fix up pipelines to run 7.0 * Update langversion to preview * Revert "Fix up pipelines to run 7.0" This reverts commit d0fa8d01b8126a4eaa59832a3814a567705419ae. * Fix up pipelines again, this time without indentation changes * Include preview versions * Versions not Version * Fix ModelTypeTests * Fix MemberPasswordHasherTests Microsoft wants to use SHA512 instead of SHA256, so our old hashes will return SuccessRehashNeeded now * Use dotnet cli instead of nuget restore * Update src/Umbraco.Web.UI/Umbraco.Web.UI.csproj * Update dependencies * Fix nullability issues * Fix unit test * Fix nullability in ChangingPasswordModel OldPassword can be null, if we're changing the password with password reset enabled. Additionally, we might as well use the new required keyword instead of supressing null. * Use required keyword instead of supressing null * Fix up pipelines again * fix up spelling-error * Use dotnet cli instead of nuget restore * Fix up another NuGet command * Use dotnet version 7 before building * Include preview versions * Remove condition * Use dotnet 7 before running powershell script * Update templates to .net 7 * Download version 7 before running linux container * Move use dotnet 7 even earlier in E2E process * Remove dotnet 7 * Reintroduce .NET 7 task * Update linux docker container and remove dotnet 7 from yml * Fix up dockerfile with ARG * Fix up docker file with nightly builds of dotnet 7 * Reintroduce dotnet 7 so windows can use it * Use aspnet 7 in docker Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch> Co-authored-by: Zeegaan <nge@umbraco.dk>
2022-08-23 11:31:05 +02:00
if (provider == "Email")
{
var mailMessage = new EmailMessage(from, user.Email, subject, message, true);
await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.TwoFactorAuth);
}
else if (provider == "Phone")
{
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
if (phoneNumber is not null)
{
await _smsSender.SendSmsAsync(phoneNumber, message);
}
}
return Ok();
}
[SetAngularAntiForgeryTokens]
[AllowAnonymous]
public async Task<ActionResult<UserDetail?>> PostVerify2FACode(Verify2FACodeModel model)
{
if (ModelState.IsValid == false)
{
return new ValidationErrorResult(ModelState);
}
BackOfficeIdentityUser? user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
_logger.LogWarning("PostVerify2FACode :: No verified user found, returning 404");
return NotFound();
}
SignInResult result =
await _signInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.IsPersistent, model.RememberClient);
if (result.Succeeded)
{
return GetUserDetail(_userService.GetByUsername(user.UserName));
}
if (result.IsLockedOut)
{
return new ValidationErrorResult("User is locked out");
}
if (result.IsNotAllowed)
{
return new ValidationErrorResult("User is not allowed");
}
return new ValidationErrorResult("Invalid code");
}
/// <summary>
/// Processes a set password request. Validates the request and sets a new password.
/// </summary>
/// <returns></returns>
[SetAngularAntiForgeryTokens]
[AllowAnonymous]
public async Task<IActionResult> PostSetPassword(SetPasswordModel model)
{
BackOfficeIdentityUser? identityUser =
await _userManager.FindByIdAsync(model.UserId.ToString(CultureInfo.InvariantCulture));
V11/feature/update to dotnet 7 (#12712) * Update projects to .NET 7 * Fix nullability errors * Fix up pipelines to run 7.0 * Update langversion to preview * Revert "Fix up pipelines to run 7.0" This reverts commit d0fa8d01b8126a4eaa59832a3814a567705419ae. * Fix up pipelines again, this time without indentation changes * Include preview versions * Versions not Version * Fix ModelTypeTests * Fix MemberPasswordHasherTests Microsoft wants to use SHA512 instead of SHA256, so our old hashes will return SuccessRehashNeeded now * Use dotnet cli instead of nuget restore * Update src/Umbraco.Web.UI/Umbraco.Web.UI.csproj * Update dependencies * Fix nullability issues * Fix unit test * Fix nullability in ChangingPasswordModel OldPassword can be null, if we're changing the password with password reset enabled. Additionally, we might as well use the new required keyword instead of supressing null. * Use required keyword instead of supressing null * Fix up pipelines again * fix up spelling-error * Use dotnet cli instead of nuget restore * Fix up another NuGet command * Use dotnet version 7 before building * Include preview versions * Remove condition * Use dotnet 7 before running powershell script * Update templates to .net 7 * Download version 7 before running linux container * Move use dotnet 7 even earlier in E2E process * Remove dotnet 7 * Reintroduce .NET 7 task * Update linux docker container and remove dotnet 7 from yml * Fix up dockerfile with ARG * Fix up docker file with nightly builds of dotnet 7 * Reintroduce dotnet 7 so windows can use it * Use aspnet 7 in docker Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch> Co-authored-by: Zeegaan <nge@umbraco.dk>
2022-08-23 11:31:05 +02:00
if (identityUser is null)
{
return new ValidationErrorResult("Could not find user");
}
IdentityResult result = await _userManager.ResetPasswordAsync(identityUser, model.ResetCode, model.Password);
if (result.Succeeded)
{
var lockedOut = await _userManager.IsLockedOutAsync(identityUser);
if (lockedOut)
{
_logger.LogInformation(
"User {UserId} is currently locked out, unlocking and resetting AccessFailedCount", model.UserId);
//// var user = await UserManager.FindByIdAsync(model.UserId);
IdentityResult unlockResult =
await _userManager.SetLockoutEndDateAsync(identityUser, DateTimeOffset.Now);
if (unlockResult.Succeeded == false)
{
_logger.LogWarning("Could not unlock for user {UserId} - error {UnlockError}", model.UserId,
unlockResult.Errors.First().Description);
}
IdentityResult resetAccessFailedCountResult =
await _userManager.ResetAccessFailedCountAsync(identityUser);
if (resetAccessFailedCountResult.Succeeded == false)
{
_logger.LogWarning("Could not reset access failed count {UserId} - error {UnlockError}",
model.UserId, unlockResult.Errors.First().Description);
}
}
// They've successfully set their password, we can now update their user account to be confirmed
// if user was only invited, then they have not been approved
// but a successful forgot password flow (e.g. if their token had expired and they did a forgot password instead of request new invite)
// means we have verified their email
if (!await _userManager.IsEmailConfirmedAsync(identityUser))
{
await _userManager.ConfirmEmailAsync(identityUser, model.ResetCode);
}
// invited is not approved, never logged in, invited date present
/*
if (LastLoginDate == default && IsApproved == false && InvitedDate != null)
return UserState.Invited;
*/
if (identityUser != null && !identityUser.IsApproved)
2020-10-23 14:18:53 +11:00
{
IUser? user = _userService.GetByUsername(identityUser.UserName);
// also check InvitedDate and never logged in, otherwise this would allow a disabled user to reactivate their account with a forgot password
if (user?.LastLoginDate == default && user?.InvitedDate != null)
2020-10-23 14:18:53 +11:00
{
user.IsApproved = true;
user.InvitedDate = null;
_userService.Save(user);
}
2020-10-23 14:18:53 +11:00
}
_userManager.NotifyForgotPasswordChanged(User, model.UserId.ToString(CultureInfo.InvariantCulture));
return Ok();
}
return new ValidationErrorResult(
result.Errors.Any() ? result.Errors.First().Description : "Set password failed");
}
2020-08-20 11:54:35 +02:00
/// <summary>
/// Logs the current user out
/// </summary>
/// <returns></returns>
[ValidateAngularAntiForgeryToken]
[AllowAnonymous]
public async Task<IActionResult> PostLogout()
{
// force authentication to occur since this is not an authorized endpoint
AuthenticateResult result = await this.AuthenticateBackOfficeAsync();
if (!result.Succeeded)
{
return Ok();
}
await _signInManager.SignOutAsync();
2022-03-30 15:58:46 +02:00
_logger.LogInformation("User {UserName} from IP address {RemoteIpAddress} has logged out",
User.Identity == null ? "UNKNOWN" : User.Identity.Name, HttpContext.Connection.RemoteIpAddress);
var userId = result.Principal.Identity?.GetUserId();
SignOutSuccessResult args = _userManager.NotifyLogoutSuccess(User, userId);
if (!args.SignOutRedirectUrl.IsNullOrWhiteSpace())
{
return new ObjectResult(new { signOutRedirectUrl = args.SignOutRedirectUrl });
}
return Ok();
}
/// <summary>
/// Return the <see cref="UserDetail" /> for the given <see cref="IUser" />
/// </summary>
/// <param name="user"></param>
/// <param name="principal"></param>
/// <returns></returns>
private UserDetail? GetUserDetail(IUser? user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
UserDetail? userDetail = _umbracoMapper.Map<UserDetail>(user);
if (userDetail is not null)
{
// update the userDetail and set their remaining seconds
userDetail.SecondsUntilTimeout = _globalSettings.TimeOut.TotalSeconds;
}
return userDetail;
}
private string ConstructCallbackUrl(string userId, string code)
{
// Get an mvc helper to get the url
var action = _linkGenerator.GetPathByAction(
nameof(BackOfficeController.ValidatePasswordResetCode),
ControllerExtensions.GetControllerName<BackOfficeController>(),
new { area = Constants.Web.Mvc.BackOfficeArea, u = userId, r = code });
// Construct full URL using configured application URL (which will fall back to current request)
Uri applicationUri = _httpContextAccessor.GetRequiredHttpContext().Request
.GetApplicationUri(_webRoutingSettings);
var callbackUri = new Uri(applicationUri, action);
return callbackUri.ToString();
}
private void AddModelErrors(IdentityResult result, string prefix = "")
{
foreach (IdentityError? error in result.Errors)
{
ModelState.AddModelError(prefix, error.Description);
}
}
}