2021-09-20 11:30:09 +02:00
using System.Globalization ;
2020-11-27 13:35:43 +01:00
using System.Security.Claims ;
2023-09-20 09:02:32 +02:00
using System.Security.Cryptography ;
2022-06-20 08:37:17 +02:00
using Microsoft.AspNetCore.Authentication ;
2020-12-04 02:21:21 +11:00
using Microsoft.AspNetCore.Authorization ;
2020-12-01 17:24:23 +11:00
using Microsoft.AspNetCore.Http ;
2020-11-18 16:12:42 +01:00
using Microsoft.AspNetCore.Identity ;
2020-08-20 22:18:50 +01:00
using Microsoft.AspNetCore.Mvc ;
2020-11-18 16:12:42 +01:00
using Microsoft.AspNetCore.Routing ;
2021-12-21 12:48:35 +01:00
using Microsoft.Extensions.DependencyInjection ;
2020-09-21 09:52:58 +02:00
using Microsoft.Extensions.Logging ;
2020-11-18 16:12:42 +01:00
using Microsoft.Extensions.Options ;
2021-02-09 10:22:42 +01:00
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 ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Models.Membership ;
using Umbraco.Cms.Core.Net ;
using Umbraco.Cms.Core.Security ;
using Umbraco.Cms.Core.Services ;
2022-06-20 08:37:17 +02:00
using Umbraco.Cms.Infrastructure.Security ;
2021-02-10 11:11:18 +01:00
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 ;
2021-04-09 15:24:12 +10:00
using Umbraco.Cms.Web.Common.Models ;
2020-05-25 23:15:32 +10:00
using Umbraco.Extensions ;
2021-03-11 19:35:43 +11:00
using SignInResult = Microsoft . AspNetCore . Identity . SignInResult ;
2020-05-13 16:09:54 +10:00
2022-06-20 08:37:17 +02:00
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
2020-05-13 16:09:54 +10:00
{
2022-06-20 08:37:17 +02:00
// 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 )
2020-05-13 16:09:54 +10:00
{
2022-06-20 08:37:17 +02:00
_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 ;
}
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
/// <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 ) ;
}
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
/// <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 ( ) ;
}
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
var decoded = token . FromUrlBase64 ( ) ;
if ( decoded . IsNullOrWhiteSpace ( ) )
{
return NotFound ( ) ;
}
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
BackOfficeIdentityUser ? identityUser = await _userManager . FindByIdAsync ( id . ToString ( ) ) ;
if ( identityUser = = null )
{
return NotFound ( ) ;
2020-08-06 19:09:05 +10:00
}
2022-06-20 08:37:17 +02:00
IdentityResult result = await _userManager . ConfirmEmailAsync ( identityUser , decoded ! ) ;
if ( result . Succeeded = = false )
2020-10-19 18:48:51 +11:00
{
2022-06-20 08:37:17 +02:00
return ValidationErrorResult . CreateNotificationValidationErrorResult ( result . Errors . ToErrorMessage ( ) ) ;
}
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
await _signInManager . SignOutAsync ( ) ;
2020-10-23 14:18:53 +11:00
2022-06-20 08:37:17 +02:00
await _signInManager . SignInAsync ( identityUser , false ) ;
2020-10-23 14:18:53 +11:00
2022-06-20 08:37:17 +02:00
IUser ? user = _userService . GetUserById ( id ) ;
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
return _umbracoMapper . Map < UserDisplay > ( user ) ;
}
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
[ValidateAngularAntiForgeryToken]
public async Task < IActionResult > PostUnLinkLogin ( UnLinkLoginModel unlinkLoginModel )
{
2022-09-19 16:37:24 +02:00
var userId = User . Identity ? . GetUserId ( ) ;
if ( userId is null )
2022-06-20 08:37:17 +02:00
{
2022-09-19 16:37:24 +02:00
throw new InvalidOperationException ( "Could not find userId" ) ;
2020-10-19 18:48:51 +11:00
}
2022-09-19 16:37:24 +02:00
var user = await _userManager . FindByIdAsync ( userId ) ;
if ( user = = null ) throw new InvalidOperationException ( "Could not find user" ) ;
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
AuthenticationScheme ? authType = ( await _signInManager . GetExternalAuthenticationSchemesAsync ( ) )
. FirstOrDefault ( x = > x . Name = = unlinkLoginModel . LoginProvider ) ;
if ( authType = = null )
2020-06-03 17:47:32 +10:00
{
2022-06-20 08:37:17 +02:00
_logger . LogWarning ( "Could not find external authentication provider registered: {LoginProvider}" , unlinkLoginModel . LoginProvider ) ;
}
else
{
BackOfficeExternaLoginProviderScheme ? opt = await _externalAuthenticationOptions . GetAsync ( authType . Name ) ;
if ( opt = = null )
2020-11-27 13:35:43 +01:00
{
2022-06-20 08:37:17 +02:00
return BadRequest (
$"Could not find external authentication options registered for provider {unlinkLoginModel.LoginProvider}" ) ;
2020-11-27 13:35:43 +01:00
}
2022-06-20 08:37:17 +02:00
if ( ! opt . ExternalLoginProvider . Options . AutoLinkOptions . AllowManualLinking )
2020-06-03 17:47:32 +10:00
{
2022-06-20 08:37:17 +02:00
// If AllowManualLinking is disabled for this provider we cannot unlink
return BadRequest ( ) ;
2020-06-03 17:47:32 +10:00
}
2022-06-20 08:37:17 +02:00
}
IdentityResult result = await _userManager . RemoveLoginAsync (
user ,
unlinkLoginModel . LoginProvider ,
unlinkLoginModel . ProviderKey ) ;
2020-06-03 17:47:32 +10:00
2022-06-20 08:37:17 +02:00
if ( result . Succeeded )
{
await _signInManager . SignInAsync ( user , true ) ;
return Ok ( ) ;
2020-05-19 09:52:58 +02:00
}
2022-06-20 08:37:17 +02:00
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 )
2020-05-19 09:52:58 +02:00
{
2022-06-20 08:37:17 +02:00
return 0 ;
2020-05-19 09:52:58 +02:00
}
2020-05-25 23:15:32 +10:00
2022-06-20 08:37:17 +02:00
var remainingSeconds = result . Principal . GetRemainingAuthSeconds ( ) ;
if ( remainingSeconds < = 30 )
2020-06-03 12:47:40 +10:00
{
2022-06-20 08:37:17 +02:00
var username = result . Principal . FindFirst ( ClaimTypes . Name ) ? . Value ;
2020-06-03 12:47:40 +10:00
2022-06-20 08:37:17 +02:00
//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.
2020-06-03 12:47:40 +10:00
2022-06-20 08:37:17 +02:00
_logger . LogInformation (
"User logged will be logged out due to timeout: {Username}, IP Address: {IPAddress}" ,
username ? ? "unknown" ,
_ipResolver . GetCurrentRequestIpAddress ( ) ) ;
2020-06-03 12:47:40 +10:00
}
2022-06-20 08:37:17 +02:00
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 )
2020-08-06 19:09:05 +10:00
{
2022-06-20 08:37:17 +02:00
//set their remaining seconds
result . SecondsUntilTimeout = HttpContext . User . GetRemainingAuthSeconds ( ) ;
}
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
return result ;
}
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
/// <summary>
/// When a user is invited they are not approved but we need to resolve the partially logged on (non approved)
/// user.
/// </summary>
2023-10-23 10:06:17 +02:00
/// <returns>It returns a 403 error if the logged-in user has already been created.</returns>
2022-06-20 08:37:17 +02:00
/// <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)]
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
2023-10-23 10:06:17 +02:00
[SetAngularAntiForgeryTokens]
[AllowAnonymous] // Needed for users that are invited when they use the link from the mail they may have logged in on a different session, so we don't want to redirect them.
2022-06-20 08:37:17 +02:00
public ActionResult < UserDetail ? > GetCurrentInvitedUser ( )
{
IUser ? user = _backofficeSecurityAccessor . BackOfficeSecurity ? . CurrentUser ;
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
if ( user ? . IsApproved ? ? false )
{
// if they are approved, than they are no longer invited and we can return an error
return Forbid ( ) ;
}
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
UserDetail ? result = _umbracoMapper . Map < UserDetail > ( user ) ;
if ( result is not null )
{
// set their remaining seconds
result . SecondsUntilTimeout = HttpContext . User . GetRemainingAuthSeconds ( ) ;
2020-08-06 19:09:05 +10:00
}
2022-06-20 08:37:17 +02:00
return result ;
}
2023-10-23 10:06:17 +02:00
/// <summary>
/// When a user is invited and they click on the invitation link, they will be partially logged in
/// where they can set their username/password.
/// </summary>
/// <param name="invitePasswordModel">The model for the new password.</param>
/// <returns>The user model for the invited user.</returns>
/// <remarks>
/// This only works when the user is logged in (partially).
/// </remarks>
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccessWithoutApproval)]
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
[SetAngularAntiForgeryTokens]
[AllowAnonymous] // Needed for users that are invited when they use the link from the mail they may have logged in on a different session, so we don't want to redirect them.
public async Task < ActionResult < UserDetail ? > > PostSetInvitedUserPassword ( InvitePasswordModel invitePasswordModel )
{
IUser ? currentUser = _backofficeSecurityAccessor . BackOfficeSecurity ? . CurrentUser ;
if ( currentUser is null )
{
return BadRequest ( "Could not find user" ) ;
}
if ( currentUser . IsApproved )
{
// if they are approved, than they are no longer invited and we can return an error
return Forbid ( ) ;
}
BackOfficeIdentityUser ? user = await _userManager . FindByIdAsync ( currentUser ! . Id . ToString ( ) ) ;
if ( user is null )
{
return BadRequest ( "Could not find identity user" ) ;
}
IdentityResult result = await _userManager . AddPasswordAsync ( user , invitePasswordModel . NewPassword ) ;
if ( result . Succeeded is false )
{
// it wasn't successful, so add the change error to the model state, we've name the property alias _umb_password on the form
// so that is why it is being used here.
ModelState . AddModelError ( "value" , result . Errors . ToErrorMessage ( ) ) ;
return ValidationProblem ( ModelState ) ;
}
if ( _backofficeSecurityAccessor . BackOfficeSecurity ? . CurrentUser is not null )
{
// They've successfully set their password, we can now update their user account to be approved
_backofficeSecurityAccessor . BackOfficeSecurity . CurrentUser . IsApproved = true ;
// They've successfully set their password, and will now get fully logged into the back office, so the lastlogindate is set so the backoffice shows they have logged in
_backofficeSecurityAccessor . BackOfficeSecurity . CurrentUser . LastLoginDate = DateTime . UtcNow ;
_userService . Save ( _backofficeSecurityAccessor . BackOfficeSecurity . CurrentUser ) ;
}
// now we can return their full object since they are now really logged into the back office
UserDetail ? userDisplay =
_umbracoMapper . Map < UserDetail > ( _backofficeSecurityAccessor . BackOfficeSecurity ? . CurrentUser ) ;
if ( userDisplay is not null )
{
userDisplay . SecondsUntilTimeout = HttpContext . User . GetRemainingAuthSeconds ( ) ;
}
return userDisplay ;
}
2022-06-20 08:37:17 +02:00
/// <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 )
2020-05-25 23:15:32 +10:00
{
2022-06-20 08:37:17 +02:00
// return the user detail
return GetUserDetail ( _userService . GetByUsername ( loginModel . Username ) ) ;
}
2020-05-25 23:15:32 +10:00
2022-06-20 08:37:17 +02:00
if ( result . RequiresTwoFactor )
{
var twofactorView = _backOfficeTwoFactorOptions . GetTwoFactorView ( loginModel . Username ) ;
2020-05-25 23:15:32 +10:00
2022-06-20 08:37:17 +02:00
IUser ? attemptedUser = _userService . GetByUsername ( loginModel . Username ) ;
2020-12-01 17:24:23 +11:00
2022-06-20 08:37:17 +02:00
// create a with information to display a custom two factor send code view
var verifyResponse =
new ObjectResult ( new { twoFactorView = twofactorView , userId = attemptedUser ? . Id } )
2020-12-01 17:24:23 +11:00
{
StatusCode = StatusCodes . Status402PaymentRequired
} ;
2022-06-20 08:37:17 +02:00
return verifyResponse ;
}
2020-05-25 23:15:32 +10:00
2022-06-20 08:37:17 +02:00
// TODO: We can check for these and respond differently if we think it's important
// result.IsLockedOut
// result.IsNotAllowed
2021-04-09 15:24:12 +10:00
2022-06-20 08:37:17 +02:00
// 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 )
{
2021-01-12 14:00:14 +01:00
return BadRequest ( ) ;
2020-05-25 23:15:32 +10:00
}
2022-06-20 08:37:17 +02:00
BackOfficeIdentityUser ? identityUser = await _userManager . FindByEmailAsync ( model . Email ) ;
if ( identityUser ! = null )
2020-08-06 19:09:05 +10:00
{
2022-06-20 08:37:17 +02:00
IUser ? user = _userService . GetByEmail ( model . Email ) ;
if ( user ! = null )
2020-08-06 19:09:05 +10:00
{
2022-06-20 08:37:17 +02:00
var from = _globalSettings . Smtp ? . From ;
var code = await _userManager . GeneratePasswordResetTokenAsync ( identityUser ) ;
var callbackUrl = ConstructCallbackUrl ( identityUser . Id , code ) ;
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
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 } ) ;
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
var subject = _textService . Localize ( "login" , "resetPasswordEmailCopySubject" ,
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions . GetUserCulture ( identityUser . Culture , _textService , _globalSettings ) ) ;
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
var mailMessage = new EmailMessage ( from , user . Email , subject , message , true ) ;
2020-08-06 19:09:05 +10:00
2023-01-31 09:04:26 +01:00
await _emailSender . SendAsync ( mailMessage , Constants . Web . EmailTypes . PasswordReset , true ) ;
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
_userManager . NotifyForgotPasswordRequested ( User , user . Id . ToString ( ) ) ;
2020-08-06 19:09:05 +10:00
}
}
2023-09-20 09:02:32 +02:00
await Task . Delay ( RandomNumberGenerator . GetInt32 ( 400 , 2500 ) ) ;
2022-06-20 08:37:17 +02:00
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 )
2020-10-19 18:48:51 +11:00
{
2022-06-20 08:37:17 +02:00
_logger . LogWarning ( "Get2FAProviders :: No verified user found, returning 404" ) ;
return NotFound ( ) ;
}
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
IEnumerable < string > userFactors = await _twoFactorLoginService . GetEnabledTwoFactorProviderNamesAsync ( user . Key ) ;
2022-04-19 08:33:03 +02:00
2022-06-20 08:37:17 +02:00
return new ObjectResult ( userFactors ) ;
}
[SetAngularAntiForgeryTokens]
[AllowAnonymous]
public async Task < IActionResult > PostSend2FACode ( [ FromBody ] string provider )
{
if ( provider . IsNullOrWhiteSpace ( ) )
{
return NotFound ( ) ;
2020-10-19 18:48:51 +11:00
}
2022-06-20 08:37:17 +02:00
BackOfficeIdentityUser ? user = await _signInManager . GetTwoFactorAuthenticationUserAsync ( ) ;
if ( user = = null )
2020-10-19 18:48:51 +11:00
{
2022-06-20 08:37:17 +02:00
_logger . LogWarning ( "PostSend2FACode :: No verified user found, returning 404" ) ;
return NotFound ( ) ;
}
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
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" ) ;
}
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
var subject = _textService . Localize ( "login" , "mfaSecurityCodeSubject" ,
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions . GetUserCulture ( user . Culture , _textService , _globalSettings ) ) ;
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
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 } ) ;
2020-10-19 18:48:51 +11:00
2022-09-19 16:37:24 +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 ) ;
}
}
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
return Ok ( ) ;
}
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
[SetAngularAntiForgeryTokens]
[AllowAnonymous]
public async Task < ActionResult < UserDetail ? > > PostVerify2FACode ( Verify2FACodeModel model )
{
if ( ModelState . IsValid = = false )
{
return new ValidationErrorResult ( ModelState ) ;
2020-10-19 18:48:51 +11:00
}
2022-06-20 08:37:17 +02:00
BackOfficeIdentityUser ? user = await _signInManager . GetTwoFactorAuthenticationUserAsync ( ) ;
if ( user = = null )
2020-10-19 18:48:51 +11:00
{
2022-06-20 08:37:17 +02:00
_logger . LogWarning ( "PostVerify2FACode :: No verified user found, returning 404" ) ;
return NotFound ( ) ;
}
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
SignInResult result =
await _signInManager . TwoFactorSignInAsync ( model . Provider , model . Code , model . IsPersistent , model . RememberClient ) ;
if ( result . Succeeded )
{
2023-11-30 11:32:49 +01:00
return Ok ( GetUserDetail ( _userService . GetByUsername ( user . UserName ) ) ) ;
2022-06-20 08:37:17 +02:00
}
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
if ( result . IsLockedOut )
{
return new ValidationErrorResult ( "User is locked out" ) ;
2020-10-19 18:48:51 +11:00
}
2022-06-20 08:37:17 +02:00
if ( result . IsNotAllowed )
2020-08-06 19:09:05 +10:00
{
2022-06-20 08:37:17 +02:00
return new ValidationErrorResult ( "User is not allowed" ) ;
}
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
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 ) ) ;
2022-09-19 16:37:24 +02:00
if ( identityUser is null )
{
return new ValidationErrorResult ( "Could not find user" ) ;
}
2022-06-20 08:37:17 +02:00
IdentityResult result = await _userManager . ResetPasswordAsync ( identityUser , model . ResetCode , model . Password ) ;
if ( result . Succeeded )
{
var lockedOut = await _userManager . IsLockedOutAsync ( identityUser ) ;
if ( lockedOut )
2020-08-06 19:09:05 +10:00
{
2022-06-20 08:37:17 +02:00
_logger . LogInformation (
"User {UserId} is currently locked out, unlocking and resetting AccessFailedCount" , model . UserId ) ;
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
//// var user = await UserManager.FindByIdAsync(model.UserId);
IdentityResult unlockResult =
await _userManager . SetLockoutEndDateAsync ( identityUser , DateTimeOffset . Now ) ;
if ( unlockResult . Succeeded = = false )
2020-08-06 19:09:05 +10:00
{
2022-06-20 08:37:17 +02:00
_logger . LogWarning ( "Could not unlock for user {UserId} - error {UnlockError}" , model . UserId ,
unlockResult . Errors . First ( ) . Description ) ;
2020-08-06 19:09:05 +10:00
}
2022-06-20 08:37:17 +02:00
IdentityResult resetAccessFailedCountResult =
await _userManager . ResetAccessFailedCountAsync ( identityUser ) ;
if ( resetAccessFailedCountResult . Succeeded = = false )
2020-08-06 19:09:05 +10:00
{
2022-06-20 08:37:17 +02:00
_logger . LogWarning ( "Could not reset access failed count {UserId} - error {UnlockError}" ,
model . UserId , unlockResult . Errors . First ( ) . Description ) ;
2020-08-06 19:09:05 +10:00
}
}
2022-06-20 08:37:17 +02:00
// 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 ) ;
}
2020-06-09 14:36:36 +10:00
2022-06-20 08:37:17 +02:00
// 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
{
2022-06-20 08:37:17 +02: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
{
2022-06-20 08:37:17 +02:00
user . IsApproved = true ;
user . InvitedDate = null ;
_userService . Save ( user ) ;
}
2020-10-23 14:18:53 +11:00
}
2020-06-09 14:36:36 +10:00
2022-06-20 08:37:17 +02:00
_userManager . NotifyForgotPasswordChanged ( User , model . UserId . ToString ( CultureInfo . InvariantCulture ) ) ;
2020-06-09 14:36:36 +10:00
return Ok ( ) ;
}
2022-06-20 08:37:17 +02:00
return new ValidationErrorResult (
result . Errors . Any ( ) ? result . Errors . First ( ) . Description : "Set password failed" ) ;
}
2020-08-20 11:54:35 +02:00
2022-06-20 08:37:17 +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 )
2020-05-25 23:15:32 +10:00
{
2022-06-20 08:37:17 +02:00
return Ok ( ) ;
}
2020-05-25 23:15:32 +10:00
2022-06-20 08:37:17 +02:00
await _signInManager . SignOutAsync ( ) ;
2022-03-30 15:58:46 +02:00
2022-06-20 08:37:17 +02:00
_logger . LogInformation ( "User {UserName} from IP address {RemoteIpAddress} has logged out" ,
2023-05-31 22:46:02 +12:00
result . Principal . Identity = = null ? "UNKNOWN" : result . Principal . Identity . Name , HttpContext . Connection . RemoteIpAddress ) ;
2020-05-25 23:15:32 +10:00
2022-06-20 08:37:17 +02:00
var userId = result . Principal . Identity ? . GetUserId ( ) ;
SignOutSuccessResult args = _userManager . NotifyLogoutSuccess ( User , userId ) ;
if ( ! args . SignOutRedirectUrl . IsNullOrWhiteSpace ( ) )
{
return new ObjectResult ( new { signOutRedirectUrl = args . SignOutRedirectUrl } ) ;
2020-05-25 23:15:32 +10:00
}
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
return Ok ( ) ;
}
/// <summary>
/// Return the <see cref="UserDetail" /> for the given <see cref="IUser" />
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
private UserDetail ? GetUserDetail ( IUser ? user )
{
if ( user = = null )
2020-08-06 19:09:05 +10:00
{
2022-06-20 08:37:17 +02:00
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2020-08-06 19:09:05 +10:00
2022-06-20 08:37:17 +02:00
UserDetail ? userDetail = _umbracoMapper . Map < UserDetail > ( user ) ;
if ( userDetail is not null )
{
// update the userDetail and set their remaining seconds
userDetail . SecondsUntilTimeout = _globalSettings . TimeOut . TotalSeconds ;
2020-08-06 19:09:05 +10:00
}
2020-10-19 18:48:51 +11:00
2022-06-20 08:37:17 +02:00
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 )
2020-10-19 18:48:51 +11:00
{
2022-06-20 08:37:17 +02:00
ModelState . AddModelError ( prefix , error . Description ) ;
2020-10-19 18:48:51 +11:00
}
2020-05-13 16:09:54 +10:00
}
}