2020-08-20 22:18:50 +01:00
using System ;
2020-06-22 10:08:08 +02:00
using System.Collections.Generic ;
2020-08-06 19:09:05 +10:00
using System.Linq ;
2020-05-25 23:15:32 +10:00
using System.Net ;
using System.Threading.Tasks ;
2020-08-31 13:56:25 +02:00
using Microsoft.AspNetCore.Routing ;
2020-08-20 22:18:50 +01:00
using Microsoft.AspNetCore.Authentication ;
using Microsoft.AspNetCore.Mvc ;
using Microsoft.Extensions.Options ;
2020-09-21 09:52:58 +02:00
using Microsoft.Extensions.Logging ;
2020-06-09 14:36:36 +10:00
using Umbraco.Core ;
2020-05-25 23:15:32 +10:00
using Umbraco.Core.BackOffice ;
2020-08-20 22:18:50 +01:00
using Umbraco.Core.Configuration.Models ;
2020-05-25 23:15:32 +10:00
using Umbraco.Core.Mapping ;
2020-08-06 19:09:05 +10:00
using Umbraco.Core.Models ;
2020-05-25 23:15:32 +10:00
using Umbraco.Core.Models.Membership ;
2020-09-22 10:01:00 +02:00
using Umbraco.Core.Security ;
2020-05-25 23:15:32 +10:00
using Umbraco.Core.Services ;
using Umbraco.Extensions ;
2020-06-03 17:47:32 +10:00
using Umbraco.Net ;
2020-06-03 12:47:40 +10:00
using Umbraco.Web.BackOffice.Filters ;
2020-08-06 19:09:05 +10:00
using Umbraco.Web.Common.ActionsResults ;
2020-05-13 16:09:54 +10:00
using Umbraco.Web.Common.Attributes ;
2020-05-19 09:52:58 +02:00
using Umbraco.Web.Common.Controllers ;
2020-05-25 23:15:32 +10:00
using Umbraco.Web.Common.Exceptions ;
2020-05-13 16:09:54 +10:00
using Umbraco.Web.Common.Filters ;
2020-06-02 13:28:30 +10:00
using Umbraco.Web.Common.Security ;
2020-05-25 23:15:32 +10:00
using Umbraco.Web.Models ;
using Umbraco.Web.Models.ContentEditing ;
2020-05-19 09:52:58 +02:00
using Umbraco.Web.Security ;
2020-05-14 17:04:16 +10:00
using Constants = Umbraco . Core . Constants ;
2020-10-19 18:48:51 +11:00
using Microsoft.AspNetCore.Identity ;
2020-10-23 14:18:53 +11:00
using Umbraco.Web.Editors.Filters ;
2020-05-13 16:09:54 +10:00
namespace Umbraco.Web.BackOffice.Controllers
{
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-05-14 17:04:16 +10:00
[IsBackOffice] // TODO: This could be applied with our Application Model conventions
2020-05-25 23:15:32 +10:00
public class AuthenticationController : UmbracoApiControllerBase
2020-05-13 16:09:54 +10:00
{
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-05-25 23:15:32 +10:00
private readonly BackOfficeSignInManager _signInManager ;
private readonly IUserService _userService ;
2020-08-06 19:09:05 +10:00
private readonly ILocalizedTextService _textService ;
2020-05-25 23:15:32 +10:00
private readonly UmbracoMapper _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 ;
2020-08-06 19:09:05 +10:00
private readonly Core . Hosting . IHostingEnvironment _hostingEnvironment ;
private readonly IRequestAccessor _requestAccessor ;
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-05-19 09:52:58 +02:00
2020-05-25 23:15:32 +10:00
// TODO: We need to import the logic from Umbraco.Web.Editors.AuthenticationController
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
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-05-25 23:15:32 +10:00
BackOfficeSignInManager signInManager ,
IUserService userService ,
2020-08-06 19:09:05 +10:00
ILocalizedTextService textService ,
2020-05-25 23:15:32 +10:00
UmbracoMapper 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 ,
2020-08-06 19:09:05 +10:00
Core . Hosting . IHostingEnvironment hostingEnvironment ,
2020-08-31 13:39:29 +02:00
IRequestAccessor requestAccessor ,
2020-10-23 14:18:53 +11:00
LinkGenerator linkGenerator ,
IBackOfficeExternalLoginProviders externalAuthenticationOptions )
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 ;
_requestAccessor = requestAccessor ;
2020-08-31 13:39:29 +02:00
_linkGenerator = linkGenerator ;
2020-10-23 14:18:53 +11:00
_externalAuthenticationOptions = externalAuthenticationOptions ;
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>
/// <returns></returns>
2020-11-12 11:01:19 +01:00
[UmbracoBackOfficeAuthorize]
2020-06-22 10:08:08 +02:00
public IDictionary < string , object > GetPasswordConfig ( int userId )
{
2020-10-21 16:51:00 +11:00
return _passwordConfiguration . GetConfiguration ( userId ! = _backofficeSecurityAccessor . BackOfficeSecurity . CurrentUser . Id ) ;
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-10-23 14:18:53 +11:00
[DenyLocalLoginAuthorization]
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 )
{
throw HttpResponseException . CreateNotificationValidationErrorResponse ( result . Errors . ToErrorMessage ( ) ) ;
}
await _signInManager . SignOutAsync ( ) ;
await _signInManager . SignInAsync ( identityUser , false ) ;
var user = _userService . GetUserById ( id ) ;
return _umbracoMapper . Map < UserDisplay > ( user ) ;
}
2020-11-12 13:10:19 +01:00
[UmbracoBackOfficeAuthorize]
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
ExternalSignInAutoLinkOptions autoLinkOptions = null ;
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
{
autoLinkOptions = _externalAuthenticationOptions . Get ( authType . Name ) ;
if ( ! autoLinkOptions . AllowManualLinking )
{
// If AllowManualLinking is disabled for this provider we cannot unlink
return BadRequest ( ) ;
}
}
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 ) ;
throw HttpResponseException . CreateValidationErrorResponse ( ModelState ) ;
}
}
2020-06-03 17:47:32 +10:00
[HttpGet]
public double GetRemainingTimeoutSeconds ( )
{
var backOfficeIdentity = HttpContext . User . GetUmbracoIdentity ( ) ;
var remainingSeconds = HttpContext . User . GetRemainingAuthSeconds ( ) ;
if ( remainingSeconds < = 30 & & backOfficeIdentity ! = null )
{
//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}" ,
backOfficeIdentity . Name ,
_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]
public bool IsAuthenticated ( )
{
2020-10-21 16:51:00 +11:00
var attempt = _backofficeSecurityAccessor . BackOfficeSecurity . AuthorizeRequest ( ) ;
2020-05-19 09:52:58 +02:00
if ( attempt = = ValidateRequestAttempt . Success )
{
return true ;
}
return false ;
}
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-12 11:01:19 +01:00
[UmbracoBackOfficeAuthorize]
2020-06-13 10:42:07 +02:00
[SetAngularAntiForgeryTokens]
2020-06-03 12:47:40 +10:00
//[CheckIfUserTicketDataIsStale] // TODO: Migrate this, though it will need to be done differently at the cookie auth level
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
result . SecondsUntilTimeout = HttpContext . User . GetRemainingAuthSeconds ( ) ;
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-12 11:01:19 +01:00
[UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: false, requireApproval: false)]
2020-08-06 19:09:05 +10:00
[SetAngularAntiForgeryTokens]
2020-10-23 14:18:53 +11:00
[DenyLocalLoginAuthorization]
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-10-23 14:18:53 +11:00
[DenyLocalLoginAuthorization]
2020-05-25 23:15:32 +10:00
public async Task < 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
var result = await _signInManager . PasswordSignInAsync (
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 )
{
throw new NotImplementedException ( "Implement MFA/2FA, we need to have some IOptions or similar to configure this" ) ;
//var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions;
//if (twofactorOptions == null)
//{
// throw new HttpResponseException(
// Request.CreateErrorResponse(
// HttpStatusCode.BadRequest,
// "UserManager does not implement " + typeof(IUmbracoBackOfficeTwoFactorOptions)));
//}
//var twofactorView = twofactorOptions.GetTwoFactorView(
// owinContext,
// UmbracoContext,
// loginModel.Username);
//if (twofactorView.IsNullOrWhiteSpace())
//{
// throw new HttpResponseException(
// Request.CreateErrorResponse(
// HttpStatusCode.BadRequest,
// typeof(IUmbracoBackOfficeTwoFactorOptions) + ".GetTwoFactorView returned an empty string"));
//}
//var attemptedUser = Services.UserService.GetByUsername(loginModel.Username);
//// create a with information to display a custom two factor send code view
//var verifyResponse = Request.CreateResponse(HttpStatusCode.PaymentRequired, new
//{
// twoFactorView = twofactorView,
// userId = attemptedUser.Id
//});
//_userManager.RaiseLoginRequiresVerificationEvent(User, attemptedUser.Id);
//return verifyResponse;
}
// 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.
throw new HttpResponseException ( HttpStatusCode . BadRequest ) ;
}
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-10-23 14:18:53 +11:00
[DenyLocalLoginAuthorization]
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 ) ;
2020-10-19 18:48:51 +11: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 } ) ;
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-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
await _emailSender . SendAsync ( mailMessage ) ;
_userManager . RaiseForgotPasswordRequestedEvent ( User , user . Id ) ;
}
}
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]
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]
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" ) ;
}
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 } ) ;
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
await _emailSender . SendAsync ( mailMessage ) ;
}
else if ( provider = = "Phone" )
{
await _smsSender . SendSmsAsync ( await _userManager . GetPhoneNumberAsync ( user ) , message ) ;
}
return Ok ( ) ;
}
[SetAngularAntiForgeryTokens]
public async Task < ActionResult < UserDetail > > PostVerify2FACode ( Verify2FACodeModel model )
{
if ( ModelState . IsValid = = false )
{
throw HttpResponseException . CreateValidationErrorResponse ( ModelState ) ;
}
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 )
{
throw HttpResponseException . CreateValidationErrorResponse ( "User is locked out" ) ;
}
if ( result . IsNotAllowed )
{
throw HttpResponseException . CreateValidationErrorResponse ( "User is not allowed" ) ;
2020-10-22 13:37:47 +02:00
}
2020-10-19 18:48:51 +11:00
throw HttpResponseException . CreateValidationErrorResponse ( "Invalid code" ) ;
}
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]
public async Task < IActionResult > PostSetPassword ( SetPasswordModel model )
{
var identityUser = await _userManager . FindByIdAsync ( model . UserId . ToString ( ) ) ;
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 ) ;
}
}
_userManager . RaiseForgotPasswordChangedSuccessEvent ( User , model . UserId ) ;
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-06-09 14:36:36 +10:00
public IActionResult PostLogout ( )
{
2020-10-23 14:18:53 +11:00
HttpContext . SignOutAsync ( Constants . Security . BackOfficeAuthenticationType ) ;
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-10-23 14:18:53 +11:00
var userId = int . Parse ( User . Identity . GetUserId ( ) ) ;
var args = _userManager . RaiseLogoutSuccessEvent ( User , userId ) ;
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
userDetail . SecondsUntilTimeout = TimeSpan . FromMinutes ( _globalSettings . TimeOutInMinutes ) . TotalSeconds ;
return userDetail ;
}
2020-08-06 19:09:05 +10:00
private string ConstructCallbackUrl ( int userId , string code )
{
// Get an mvc helper to get the url
2020-08-31 13:39:29 +02: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
} ) ;
// Construct full URL using configured application URL (which will fall back to request)
var applicationUri = _requestAccessor . GetApplicationUrl ( ) ;
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
}
}