2020-12-04 02:21:21 +11:00
using System ;
2020-06-22 10:08:08 +02:00
using System.Collections.Generic ;
2021-09-20 11:30:09 +02:00
using System.Globalization ;
2020-08-06 19:09:05 +10:00
using System.Linq ;
2020-11-27 13:35:43 +01:00
using System.Security.Claims ;
2020-05-25 23:15:32 +10:00
using System.Threading.Tasks ;
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 ;
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 ;
2021-12-21 12:48:35 +01:00
using Umbraco.Cms.Web.Common.DependencyInjection ;
2021-02-10 11:42:04 +01:00
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-02-09 10:22:42 +01:00
using Constants = Umbraco . Cms . Core . Constants ;
2021-03-11 19:35:43 +11:00
using SignInResult = Microsoft . AspNetCore . Identity . SignInResult ;
2020-05-13 16:09:54 +10:00
2021-02-10 11:11:18 +01:00
namespace Umbraco.Cms.Web.BackOffice.Controllers
2020-05-13 16:09:54 +10:00
{
2020-10-22 13:37:47 +02:00
// See
2020-10-19 18:48:51 +11:00
// 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
2020-05-14 21:12:41 +10:00
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)] // TODO: Maybe this could be applied with our Application Model conventions
2020-05-13 16:09:54 +10:00
//[ValidationFilter] // TODO: I don't actually think this is required with our custom Application Model conventions applied
2020-06-08 13:14:23 +02:00
[AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions
2020-11-27 13:35:22 +01:00
[IsBackOffice]
2021-09-07 12:10:58 +02:00
[DisableBrowserCache]
2020-05-25 23:15:32 +10:00
public class AuthenticationController : UmbracoApiControllerBase
2020-05-13 16:09:54 +10:00
{
2020-12-02 14:28:16 +11: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.
2020-10-21 16:51:00 +11:00
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor ;
2020-09-22 14:44:41 +02:00
private readonly IBackOfficeUserManager _userManager ;
2020-11-30 22:46:05 +11:00
private readonly IBackOfficeSignInManager _signInManager ;
2020-05-25 23:15:32 +10:00
private readonly IUserService _userService ;
2020-08-06 19:09:05 +10:00
private readonly ILocalizedTextService _textService ;
2021-04-20 19:34:18 +02:00
private readonly IUmbracoMapper _umbracoMapper ;
2020-08-21 14:52:47 +01:00
private readonly GlobalSettings _globalSettings ;
2020-08-20 22:18:50 +01:00
private readonly SecuritySettings _securitySettings ;
2020-09-21 09:52:58 +02:00
private readonly ILogger < AuthenticationController > _logger ;
2020-06-03 17:47:32 +10:00
private readonly IIpResolver _ipResolver ;
2020-08-20 22:18:50 +01:00
private readonly UserPasswordConfigurationSettings _passwordConfiguration ;
2020-08-06 19:09:05 +10:00
private readonly IEmailSender _emailSender ;
2020-10-19 18:48:51 +11:00
private readonly ISmsSender _smsSender ;
2021-02-09 10:22:42 +01:00
private readonly IHostingEnvironment _hostingEnvironment ;
2020-08-31 13:39:29 +02:00
private readonly LinkGenerator _linkGenerator ;
2020-10-23 14:18:53 +11:00
private readonly IBackOfficeExternalLoginProviders _externalAuthenticationOptions ;
2020-12-01 17:24:23 +11:00
private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions ;
2021-12-21 12:48:35 +01:00
private readonly IHttpContextAccessor _httpContextAccessor ;
private readonly WebRoutingSettings _webRoutingSettings ;
2020-05-19 09:52:58 +02:00
2020-05-27 18:27:49 +10:00
// TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here
2021-12-21 12:48:35 +01:00
[ActivatorUtilitiesConstructor]
2020-05-25 23:15:32 +10:00
public AuthenticationController (
2020-10-21 16:51:00 +11:00
IBackOfficeSecurityAccessor backofficeSecurityAccessor ,
2020-09-22 14:44:41 +02:00
IBackOfficeUserManager backOfficeUserManager ,
2020-11-30 22:46:05 +11:00
IBackOfficeSignInManager signInManager ,
2020-05-25 23:15:32 +10:00
IUserService userService ,
2020-08-06 19:09:05 +10:00
ILocalizedTextService textService ,
2021-04-20 19:34:18 +02:00
IUmbracoMapper umbracoMapper ,
2020-08-23 23:36:48 +02:00
IOptions < GlobalSettings > globalSettings ,
IOptions < SecuritySettings > securitySettings ,
2020-09-21 09:52:58 +02:00
ILogger < AuthenticationController > logger ,
2020-06-22 10:08:08 +02:00
IIpResolver ipResolver ,
2020-08-23 23:36:48 +02:00
IOptions < UserPasswordConfigurationSettings > passwordConfiguration ,
2020-08-06 19:09:05 +10:00
IEmailSender emailSender ,
2020-10-19 18:48:51 +11:00
ISmsSender smsSender ,
2021-02-09 10:22:42 +01:00
IHostingEnvironment hostingEnvironment ,
2020-10-23 14:18:53 +11:00
LinkGenerator linkGenerator ,
2020-12-01 17:24:23 +11:00
IBackOfficeExternalLoginProviders externalAuthenticationOptions ,
2021-12-21 12:48:35 +01:00
IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions ,
IHttpContextAccessor httpContextAccessor ,
IOptions < WebRoutingSettings > webRoutingSettings )
2020-05-19 09:52:58 +02:00
{
2020-09-22 10:01:00 +02:00
_backofficeSecurityAccessor = backofficeSecurityAccessor ;
2020-05-25 23:15:32 +10:00
_userManager = backOfficeUserManager ;
_signInManager = signInManager ;
_userService = userService ;
2020-08-06 19:09:05 +10:00
_textService = textService ;
2020-05-25 23:15:32 +10:00
_umbracoMapper = umbracoMapper ;
2020-08-21 14:52:47 +01:00
_globalSettings = globalSettings . Value ;
2020-08-20 22:18:50 +01:00
_securitySettings = securitySettings . Value ;
2020-06-03 17:47:32 +10:00
_logger = logger ;
_ipResolver = ipResolver ;
2020-08-20 22:18:50 +01:00
_passwordConfiguration = passwordConfiguration . Value ;
2020-08-06 19:09:05 +10:00
_emailSender = emailSender ;
2020-10-19 18:48:51 +11:00
_smsSender = smsSender ;
2020-08-06 19:09:05 +10:00
_hostingEnvironment = hostingEnvironment ;
2020-08-31 13:39:29 +02:00
_linkGenerator = linkGenerator ;
2020-10-23 14:18:53 +11:00
_externalAuthenticationOptions = externalAuthenticationOptions ;
2020-12-01 17:24:23 +11:00
_backOfficeTwoFactorOptions = backOfficeTwoFactorOptions ;
2021-12-21 12:48:35 +01:00
_httpContextAccessor = httpContextAccessor ;
_webRoutingSettings = webRoutingSettings . Value ;
}
[Obsolete("Use constructor that also takes IHttpAccessor and IOptions<WebRoutingSettings>, scheduled for removal in V11")]
public AuthenticationController (
IBackOfficeSecurityAccessor backofficeSecurityAccessor ,
IBackOfficeUserManager backOfficeUserManager ,
IBackOfficeSignInManager signInManager ,
IUserService userService ,
ILocalizedTextService textService ,
IUmbracoMapper umbracoMapper ,
IOptions < GlobalSettings > globalSettings ,
IOptions < SecuritySettings > securitySettings ,
ILogger < AuthenticationController > logger ,
IIpResolver ipResolver ,
IOptions < UserPasswordConfigurationSettings > passwordConfiguration ,
IEmailSender emailSender ,
ISmsSender smsSender ,
IHostingEnvironment hostingEnvironment ,
LinkGenerator linkGenerator ,
IBackOfficeExternalLoginProviders externalAuthenticationOptions ,
IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions )
: this (
backofficeSecurityAccessor ,
backOfficeUserManager ,
signInManager ,
userService ,
textService ,
umbracoMapper ,
globalSettings ,
securitySettings ,
logger ,
ipResolver ,
passwordConfiguration ,
emailSender ,
smsSender ,
hostingEnvironment ,
linkGenerator ,
externalAuthenticationOptions ,
backOfficeTwoFactorOptions ,
StaticServiceProvider . Instance . GetRequiredService < IHttpContextAccessor > ( ) ,
StaticServiceProvider . Instance . GetRequiredService < IOptions < WebRoutingSettings > > ( ) )
{
2020-06-22 10:08:08 +02:00
}
/// <summary>
/// Returns the configuration for the backoffice user membership provider - used to configure the change password dialog
/// </summary>
2021-01-12 16:15:19 +01:00
[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.
2020-06-22 10:08:08 +02:00
public IDictionary < string , object > GetPasswordConfig ( int userId )
{
2021-01-12 16:15:19 +01:00
Attempt < int > currentUserId = _backofficeSecurityAccessor . BackOfficeSecurity . GetUserId ( ) ;
return _passwordConfiguration . GetConfiguration (
currentUserId . Success
? currentUserId . Result ! = userId
: true ) ;
2020-06-03 17:47:32 +10:00
}
2020-08-06 19:09:05 +10: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]
2020-11-19 23:53:04 +11:00
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
2020-08-06 19:09:05 +10:00
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 ( ) ;
var identityUser = await _userManager . FindByIdAsync ( id . ToString ( ) ) ;
if ( identityUser = = null )
return NotFound ( ) ;
var result = await _userManager . ConfirmEmailAsync ( identityUser , decoded ) ;
if ( result . Succeeded = = false )
{
2020-12-22 16:36:07 +01:00
return ValidationErrorResult . CreateNotificationValidationErrorResult ( result . Errors . ToErrorMessage ( ) ) ;
2020-08-06 19:09:05 +10:00
}
await _signInManager . SignOutAsync ( ) ;
await _signInManager . SignInAsync ( identityUser , false ) ;
var user = _userService . GetUserById ( id ) ;
return _umbracoMapper . Map < UserDisplay > ( user ) ;
}
2020-11-20 15:32:36 +11:00
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
2020-10-19 18:48:51 +11:00
[ValidateAngularAntiForgeryToken]
2020-10-23 14:18:53 +11:00
public async Task < IActionResult > PostUnLinkLogin ( UnLinkLoginModel unlinkLoginModel )
2020-10-19 18:48:51 +11:00
{
var user = await _userManager . FindByIdAsync ( User . Identity . GetUserId ( ) ) ;
if ( user = = null ) throw new InvalidOperationException ( "Could not find user" ) ;
2020-10-23 14:18:53 +11:00
var 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
{
2021-07-27 16:12:23 -06:00
BackOfficeExternaLoginProviderScheme opt = await _externalAuthenticationOptions . GetAsync ( authType . Name ) ;
2020-11-27 13:33:01 +01:00
if ( opt = = null )
2020-10-23 14:18:53 +11:00
{
2020-11-27 13:33:01 +01:00
return BadRequest ( $"Could not find external authentication options registered for provider {unlinkLoginModel.LoginProvider}" ) ;
}
else
{
2021-07-27 16:12:23 -06:00
if ( ! opt . ExternalLoginProvider . Options . AutoLinkOptions . AllowManualLinking )
2020-11-27 13:33:01 +01:00
{
// If AllowManualLinking is disabled for this provider we cannot unlink
return BadRequest ( ) ;
}
2020-10-23 14:18:53 +11:00
}
}
2020-10-19 18:48:51 +11:00
var result = await _userManager . RemoveLoginAsync (
user ,
unlinkLoginModel . LoginProvider ,
unlinkLoginModel . ProviderKey ) ;
if ( result . Succeeded )
{
await _signInManager . SignInAsync ( user , true ) ;
return Ok ( ) ;
}
else
{
AddModelErrors ( result ) ;
2021-06-25 10:29:18 -06:00
return new ValidationErrorResult ( ModelState ) ;
2020-10-19 18:48:51 +11:00
}
}
2020-06-03 17:47:32 +10:00
[HttpGet]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-11-27 13:35:43 +01:00
public async Task < double > GetRemainingTimeoutSeconds ( )
2020-06-03 17:47:32 +10:00
{
2020-11-27 13:35:43 +01:00
// force authentication to occur since this is not an authorized endpoint
2020-12-02 15:49:28 +11:00
var result = await this . AuthenticateBackOfficeAsync ( ) ;
2020-11-27 13:35:43 +01:00
if ( ! result . Succeeded )
{
return 0 ;
}
2020-11-27 13:36:09 +01:00
var remainingSeconds = result . Principal . GetRemainingAuthSeconds ( ) ;
2020-11-27 13:35:43 +01:00
if ( remainingSeconds < = 30 )
2020-06-03 17:47:32 +10:00
{
2020-11-27 13:35:43 +01:00
var username = result . Principal . FindFirst ( ClaimTypes . Name ) ? . Value ;
2020-06-03 17:47:32 +10: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-09-15 08:45:40 +02:00
_logger . LogInformation (
2020-06-03 17:47:32 +10:00
"User logged will be logged out due to timeout: {Username}, IP Address: {IPAddress}" ,
2020-11-27 13:35:43 +01:00
username ? ? "unknown" ,
2020-06-03 17:47:32 +10:00
_ipResolver . GetCurrentRequestIpAddress ( ) ) ;
}
return remainingSeconds ;
2020-05-19 09:52:58 +02:00
}
/// <summary>
/// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest)
/// </summary>
/// <returns></returns>
[HttpGet]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-11-27 13:35:43 +01:00
public async Task < bool > IsAuthenticated ( )
2020-05-19 09:52:58 +02:00
{
2020-11-27 13:35:43 +01:00
// force authentication to occur since this is not an authorized endpoint
2020-12-02 15:49:28 +11:00
var result = await this . AuthenticateBackOfficeAsync ( ) ;
2020-11-27 13:35:43 +01:00
return result . Succeeded ;
2020-05-19 09:52:58 +02:00
}
2020-05-25 23:15:32 +10:00
2020-06-03 12:47:40 +10:00
/// <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>
2020-11-20 15:32:36 +11:00
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
2020-06-13 10:42:07 +02:00
[SetAngularAntiForgeryTokens]
2020-11-27 13:33:01 +01:00
[CheckIfUserTicketDataIsStale]
2020-06-03 12:47:40 +10:00
public UserDetail GetCurrentUser ( )
{
2020-10-21 16:51:00 +11:00
var user = _backofficeSecurityAccessor . BackOfficeSecurity . CurrentUser ;
2020-06-03 12:47:40 +10:00
var result = _umbracoMapper . Map < UserDetail > ( user ) ;
//set their remaining seconds
2021-09-17 08:29:34 +02:00
result . SecondsUntilTimeout = HttpContext . User . GetRemainingAuthSeconds ( ) ;
2020-06-03 12:47:40 +10:00
return result ;
}
2020-08-06 19:09:05 +10:00
/// <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>
2020-11-20 15:32:36 +11:00
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccessWithoutApproval)]
2020-08-06 19:09:05 +10:00
[SetAngularAntiForgeryTokens]
2020-11-19 23:53:04 +11:00
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
2020-08-06 19:09:05 +10:00
public ActionResult < UserDetail > GetCurrentInvitedUser ( )
{
2020-10-21 16:51:00 +11:00
var user = _backofficeSecurityAccessor . BackOfficeSecurity . CurrentUser ;
2020-08-06 19:09:05 +10:00
if ( user . IsApproved )
{
// if they are approved, than they are no longer invited and we can return an error
return Forbid ( ) ;
}
var result = _umbracoMapper . Map < UserDetail > ( user ) ;
// set their remaining seconds
result . SecondsUntilTimeout = HttpContext . User . GetRemainingAuthSeconds ( ) ;
return result ;
}
2020-05-25 23:15:32 +10:00
/// <summary>
/// Logs a user in
/// </summary>
/// <returns></returns>
2020-06-13 10:42:07 +02:00
[SetAngularAntiForgeryTokens]
2020-11-19 23:53:04 +11:00
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
2020-12-01 17:24:23 +11:00
public async Task < ActionResult < UserDetail > > PostLogin ( LoginModel loginModel )
2020-05-25 23:15:32 +10:00
{
// 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
2021-03-11 19:35:43 +11:00
SignInResult result = await _signInManager . PasswordSignInAsync (
2020-05-25 23:15:32 +10:00
loginModel . Username , loginModel . Password , isPersistent : true , lockoutOnFailure : true ) ;
if ( result . Succeeded )
{
2020-05-27 18:27:49 +10:00
// return the user detail
return GetUserDetail ( _userService . GetByUsername ( loginModel . Username ) ) ;
2020-05-25 23:15:32 +10:00
}
if ( result . RequiresTwoFactor )
{
2020-12-01 17:24:23 +11:00
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 " ) ;
}
2021-03-11 19:35:43 +11:00
IUser attemptedUser = _userService . GetByUsername ( loginModel . Username ) ;
2020-12-01 17:24:23 +11:00
// 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
} ;
2020-12-01 18:14:37 +11:00
return verifyResponse ;
2020-05-25 23:15:32 +10:00
}
2021-04-09 15:24:12 +10:00
// TODO: We can check for these and respond differently if we think it's important
// result.IsLockedOut
// result.IsNotAllowed
2020-05-25 23:15:32 +10: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.
2021-01-12 14:00:14 +01:00
return BadRequest ( ) ;
2020-05-25 23:15:32 +10:00
}
2020-08-06 19:09:05 +10:00
/// <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]
2020-11-19 23:53:04 +11:00
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
2020-08-06 19:09:05 +10:00
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 ( ) ;
var identityUser = await _userManager . FindByEmailAsync ( model . Email ) ;
if ( identityUser ! = null )
{
var user = _userService . GetByEmail ( model . Email ) ;
if ( user ! = null )
{
2020-10-22 15:08:07 +02:00
var from = _globalSettings . Smtp . From ;
2020-08-06 19:09:05 +10:00
var code = await _userManager . GeneratePasswordResetTokenAsync ( identityUser ) ;
var callbackUrl = ConstructCallbackUrl ( identityUser . Id , code ) ;
2021-07-05 20:58:04 +02:00
var message = _textService . Localize ( "login" , "resetPasswordEmailCopyFormat" ,
2020-08-06 19:09:05 +10:00
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions . GetUserCulture ( identityUser . Culture , _textService , _globalSettings ) ,
new [ ] { identityUser . UserName , callbackUrl } ) ;
2021-07-05 20:58:04 +02:00
var subject = _textService . Localize ( "login" , "resetPasswordEmailCopySubject" ,
2020-08-06 19:09:05 +10:00
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions . GetUserCulture ( identityUser . Culture , _textService , _globalSettings ) ) ;
2020-10-23 12:37:23 +02:00
var mailMessage = new EmailMessage ( from , user . Email , subject , message , true ) ;
2020-08-06 19:09:05 +10:00
2021-09-01 09:02:41 +01:00
await _emailSender . SendAsync ( mailMessage , Constants . Web . EmailTypes . PasswordReset ) ;
2020-08-06 19:09:05 +10:00
2021-02-26 16:37:34 +01:00
_userManager . NotifyForgotPasswordRequested ( User , user . Id . ToString ( ) ) ;
2020-08-06 19:09:05 +10:00
}
}
return Ok ( ) ;
}
2020-10-19 18:48:51 +11:00
/// <summary>
/// Used to retrieve the 2FA providers for code submission
/// </summary>
/// <returns></returns>
2020-10-22 13:37:47 +02:00
[SetAngularAntiForgeryTokens]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-10-22 13:37:47 +02:00
public async Task < ActionResult < IEnumerable < string > > > Get2FAProviders ( )
2020-10-19 18:48:51 +11:00
{
var user = await _signInManager . GetTwoFactorAuthenticationUserAsync ( ) ;
if ( user = = null )
{
_logger . LogWarning ( "Get2FAProviders :: No verified user found, returning 404" ) ;
2020-10-22 13:37:47 +02:00
return NotFound ( ) ;
2020-10-19 18:48:51 +11:00
}
var userFactors = await _userManager . GetValidTwoFactorProvidersAsync ( user ) ;
2020-10-22 13:37:47 +02:00
return new ObjectResult ( userFactors ) ;
2020-10-19 18:48:51 +11:00
}
[SetAngularAntiForgeryTokens]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-10-19 18:48:51 +11:00
public async Task < IActionResult > PostSend2FACode ( [ FromBody ] string provider )
{
if ( provider . IsNullOrWhiteSpace ( ) )
return NotFound ( ) ;
var user = await _signInManager . GetTwoFactorAuthenticationUserAsync ( ) ;
if ( user = = null )
{
_logger . LogWarning ( "PostSend2FACode :: No verified user found, returning 404" ) ;
return NotFound ( ) ;
}
2020-10-26 14:30:59 +01:00
var from = _globalSettings . Smtp . From ;
2020-10-19 18:48:51 +11:00
// 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" ) ;
}
2021-07-05 20:58:04 +02:00
var subject = _textService . Localize ( "login" , "mfaSecurityCodeSubject" ,
2020-10-19 18:48:51 +11:00
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions . GetUserCulture ( user . Culture , _textService , _globalSettings ) ) ;
2021-07-05 20:58:04 +02:00
var message = _textService . Localize ( "login" , "mfaSecurityCodeMessage" ,
2020-10-19 18:48:51 +11:00
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions . GetUserCulture ( user . Culture , _textService , _globalSettings ) ,
new [ ] { code } ) ;
if ( provider = = "Email" )
{
2020-10-26 14:30:59 +01:00
var mailMessage = new EmailMessage ( from , user . Email , subject , message , true ) ;
2020-10-19 18:48:51 +11:00
2021-09-01 09:02:41 +01:00
await _emailSender . SendAsync ( mailMessage , Constants . Web . EmailTypes . TwoFactorAuth ) ;
2020-10-19 18:48:51 +11:00
}
else if ( provider = = "Phone" )
{
await _smsSender . SendSmsAsync ( await _userManager . GetPhoneNumberAsync ( user ) , message ) ;
}
return Ok ( ) ;
}
[SetAngularAntiForgeryTokens]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-10-19 18:48:51 +11:00
public async Task < ActionResult < UserDetail > > PostVerify2FACode ( Verify2FACodeModel model )
{
if ( ModelState . IsValid = = false )
{
2021-06-25 10:29:18 -06:00
return new ValidationErrorResult ( ModelState ) ;
2020-10-19 18:48:51 +11:00
}
var user = await _signInManager . GetTwoFactorAuthenticationUserAsync ( ) ;
if ( user = = null )
{
_logger . LogWarning ( "PostVerify2FACode :: No verified user found, returning 404" ) ;
return NotFound ( ) ;
}
var result = await _signInManager . TwoFactorSignInAsync ( model . Provider , model . Code , model . IsPersistent , model . RememberClient ) ;
if ( result . Succeeded )
{
return GetUserDetail ( _userService . GetByUsername ( user . UserName ) ) ;
}
if ( result . IsLockedOut )
{
2020-12-02 12:22:08 +11:00
return new ValidationErrorResult ( "User is locked out" ) ;
2020-10-19 18:48:51 +11:00
}
if ( result . IsNotAllowed )
{
2020-12-02 12:22:08 +11:00
return new ValidationErrorResult ( "User is not allowed" ) ;
2020-10-22 13:37:47 +02:00
}
2020-10-19 18:48:51 +11:00
2020-12-02 12:22:08 +11:00
return new ValidationErrorResult ( "Invalid code" ) ;
2020-10-19 18:48:51 +11:00
}
2020-08-06 19:09:05 +10:00
/// <summary>
/// Processes a set password request. Validates the request and sets a new password.
/// </summary>
/// <returns></returns>
[SetAngularAntiForgeryTokens]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-08-06 19:09:05 +10:00
public async Task < IActionResult > PostSetPassword ( SetPasswordModel model )
{
2021-09-20 11:30:09 +02:00
var identityUser = await _userManager . FindByIdAsync ( model . UserId . ToString ( CultureInfo . InvariantCulture ) ) ;
2020-08-06 19:09:05 +10:00
var result = await _userManager . ResetPasswordAsync ( identityUser , model . ResetCode , model . Password ) ;
if ( result . Succeeded )
{
var lockedOut = await _userManager . IsLockedOutAsync ( identityUser ) ;
if ( lockedOut )
{
2020-09-15 08:45:40 +02:00
_logger . LogInformation ( "User {UserId} is currently locked out, unlocking and resetting AccessFailedCount" , model . UserId ) ;
2020-08-06 19:09:05 +10:00
//// var user = await UserManager.FindByIdAsync(model.UserId);
var unlockResult = await _userManager . SetLockoutEndDateAsync ( identityUser , DateTimeOffset . Now ) ;
if ( unlockResult . Succeeded = = false )
{
2020-09-16 09:58:07 +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
}
var resetAccessFailedCountResult = await _userManager . ResetAccessFailedCountAsync ( identityUser ) ;
if ( resetAccessFailedCountResult . Succeeded = = false )
{
2020-09-16 09:58:07 +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
}
}
// 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 )
{
var 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 )
{
user . IsApproved = true ;
user . InvitedDate = null ;
_userService . Save ( user ) ;
}
}
2021-09-20 11:30:09 +02:00
_userManager . NotifyForgotPasswordChanged ( User , model . UserId . ToString ( CultureInfo . InvariantCulture ) ) ;
2020-08-06 19:09:05 +10:00
return Ok ( ) ;
}
return new ValidationErrorResult ( result . Errors . Any ( ) ? result . Errors . First ( ) . Description : "Set password failed" ) ;
}
2020-06-09 14:36:36 +10:00
/// <summary>
/// Logs the current user out
/// </summary>
/// <returns></returns>
2020-06-13 10:42:07 +02:00
[ValidateAngularAntiForgeryToken]
2020-12-02 14:28:16 +11:00
[AllowAnonymous]
2020-11-27 13:36:09 +01:00
public async Task < IActionResult > PostLogout ( )
2020-06-09 14:36:36 +10:00
{
2020-11-27 13:36:09 +01:00
// force authentication to occur since this is not an authorized endpoint
2020-12-02 15:49:28 +11:00
var result = await this . AuthenticateBackOfficeAsync ( ) ;
2020-11-27 13:36:09 +01:00
if ( ! result . Succeeded ) return Ok ( ) ;
await _signInManager . SignOutAsync ( ) ;
2020-06-09 14:36:36 +10:00
2020-09-15 08:45:40 +02:00
_logger . LogInformation ( "User {UserName} from IP address {RemoteIpAddress} has logged out" , User . Identity = = null ? "UNKNOWN" : User . Identity . Name , HttpContext . Connection . RemoteIpAddress ) ;
2020-06-09 14:36:36 +10:00
2020-12-04 12:44:27 +11:00
var userId = result . Principal . Identity . GetUserId ( ) ;
2021-02-26 16:37:34 +01:00
var args = _userManager . NotifyLogoutSuccess ( User , userId ) ;
2020-10-23 14:18:53 +11:00
if ( ! args . SignOutRedirectUrl . IsNullOrWhiteSpace ( ) )
{
return new ObjectResult ( new
{
signOutRedirectUrl = args . SignOutRedirectUrl
} ) ;
}
2020-06-09 14:36:36 +10:00
return Ok ( ) ;
}
2020-08-20 11:54:35 +02:00
2020-05-25 23:15:32 +10:00
/// <summary>
2020-05-27 18:27:49 +10:00
/// Return the <see cref="UserDetail"/> for the given <see cref="IUser"/>
2020-05-25 23:15:32 +10:00
/// </summary>
/// <param name="user"></param>
/// <param name="principal"></param>
/// <returns></returns>
2020-05-27 18:27:49 +10:00
private UserDetail GetUserDetail ( IUser user )
2020-05-25 23:15:32 +10:00
{
2020-05-27 18:27:49 +10:00
if ( user = = null ) throw new ArgumentNullException ( nameof ( user ) ) ;
2020-05-25 23:15:32 +10:00
var userDetail = _umbracoMapper . Map < UserDetail > ( user ) ;
// update the userDetail and set their remaining seconds
2021-03-05 15:36:27 +01:00
userDetail . SecondsUntilTimeout = _globalSettings . TimeOut . TotalSeconds ;
2020-05-25 23:15:32 +10:00
return userDetail ;
}
2020-08-06 19:09:05 +10:00
2020-12-04 12:44:27 +11:00
private string ConstructCallbackUrl ( string userId , string code )
2020-08-06 19:09:05 +10:00
{
// Get an mvc helper to get the url
2020-12-04 12:44:27 +11:00
var action = _linkGenerator . GetPathByAction (
nameof ( BackOfficeController . ValidatePasswordResetCode ) ,
ControllerExtensions . GetControllerName < BackOfficeController > ( ) ,
2020-08-06 19:09:05 +10:00
new
{
2020-08-31 13:39:29 +02:00
area = Constants . Web . Mvc . BackOfficeArea ,
2020-08-06 19:09:05 +10:00
u = userId ,
r = code
} ) ;
2022-01-21 12:40:18 +01:00
// Construct full URL using configured application URL (which will fall back to current request)
2021-12-21 12:48:35 +01:00
Uri applicationUri = _httpContextAccessor . GetRequiredHttpContext ( ) . Request . GetApplicationUri ( _webRoutingSettings ) ;
2020-08-06 19:09:05 +10:00
var callbackUri = new Uri ( applicationUri , action ) ;
return callbackUri . ToString ( ) ;
}
2020-10-19 18:48:51 +11:00
private void AddModelErrors ( IdentityResult result , string prefix = "" )
{
foreach ( var error in result . Errors )
{
ModelState . AddModelError ( prefix , error . Description ) ;
}
}
2020-05-13 16:09:54 +10:00
}
}