Initial side by side implementation
This commit is contained in:
37
src/Umbraco.Web/ClaimsIdentityExtensions.cs
Normal file
37
src/Umbraco.Web/ClaimsIdentityExtensions.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Umbraco.Web
|
||||
{
|
||||
public static class ClaimsIdentityExtensions
|
||||
{
|
||||
public static string GetUserId(this IIdentity identity)
|
||||
{
|
||||
if (identity == null) throw new ArgumentNullException(nameof(identity));
|
||||
|
||||
string userId = null;
|
||||
if (identity is ClaimsIdentity claimsIdentity)
|
||||
{
|
||||
userId = claimsIdentity.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
||||
?? claimsIdentity.FindFirst("sub")?.Value;
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
public static string GetUserName(this IIdentity identity)
|
||||
{
|
||||
if (identity == null) throw new ArgumentNullException(nameof(identity));
|
||||
|
||||
string username = null;
|
||||
if (identity is ClaimsIdentity claimsIdentity)
|
||||
{
|
||||
username = claimsIdentity.FindFirst(ClaimTypes.Name)?.Value
|
||||
?? claimsIdentity.FindFirst("preferred_username")?.Value;
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
using System;
|
||||
using System.Web;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Security;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
@@ -66,6 +63,17 @@ namespace Umbraco.Web
|
||||
?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeSignInManager)} from the {typeof(IOwinContext)}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the back office sign in manager out of OWIN
|
||||
/// </summary>
|
||||
/// <param name="owinContext"></param>
|
||||
/// <returns></returns>
|
||||
public static BackOfficeSignInManager2 GetBackOfficeSignInManager2(this IOwinContext owinContext)
|
||||
{
|
||||
return owinContext.Get<BackOfficeSignInManager2>()
|
||||
?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeSignInManager2)} from the {typeof(IOwinContext)}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the back office user manager out of OWIN
|
||||
/// </summary>
|
||||
@@ -84,6 +92,38 @@ namespace Umbraco.Web
|
||||
?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeUserManager<BackOfficeIdentityUser>)} from the {typeof (IOwinContext)}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the back office user manager out of OWIN
|
||||
/// </summary>
|
||||
/// <param name="owinContext"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This is required because to extract the user manager we need to user a custom service since owin only deals in generics and
|
||||
/// developers could register their own user manager types
|
||||
/// </remarks>
|
||||
public static BackOfficeUserManager2<BackOfficeIdentityUser> GetBackOfficeUserManager2(this IOwinContext owinContext)
|
||||
{
|
||||
var marker = owinContext.Get<IBackOfficeUserManagerMarker2>(BackOfficeUserManager2.OwinMarkerKey)
|
||||
?? throw new NullReferenceException($"No {typeof (IBackOfficeUserManagerMarker2)}, i.e. no Umbraco back-office, has been registered with Owin.");
|
||||
|
||||
return marker.GetManager(owinContext)
|
||||
?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeUserManager<BackOfficeIdentityUser>)} from the {typeof (IOwinContext)}.");
|
||||
}
|
||||
|
||||
// TODO: SB: OWIN DI
|
||||
|
||||
/// <summary>
|
||||
/// Adapted from Microsoft.AspNet.Identity.Owin.OwinContextExtensions
|
||||
/// </summary>
|
||||
public static T Get<T>(this IOwinContext context)
|
||||
{
|
||||
if (context == null) throw new ArgumentNullException(nameof(context));
|
||||
return context.Get<T>(GetKey(typeof(T)));
|
||||
}
|
||||
|
||||
private static string GetKey(Type t)
|
||||
{
|
||||
return "AspNet.Identity.Owin:" + t.AssemblyQualifiedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,19 +16,6 @@ using Constants = Umbraco.Core.Constants;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
// TODO: SB: Create new custom BackOfficeSignInManager2 (rename pending)
|
||||
public class BackOfficeSignInManager2
|
||||
{
|
||||
// Create
|
||||
// CreateUserIdentityAsync
|
||||
// PasswordSignInAsync
|
||||
// SignInAsync
|
||||
// GetVerifiedUserIdAsync
|
||||
// GetVerifiedUserNameAsync
|
||||
// TwoFactorSignInAsync
|
||||
// SendTwoFactorCodeAsync
|
||||
}
|
||||
|
||||
// TODO: In v8 we need to change this to use an int? nullable TKey instead, see notes against overridden TwoFactorSignInAsync
|
||||
public class BackOfficeSignInManager : SignInManager<BackOfficeIdentityUser, int>
|
||||
{
|
||||
|
||||
354
src/Umbraco.Web/Security/BackOfficeSignInManager2.cs
Normal file
354
src/Umbraco.Web/Security/BackOfficeSignInManager2.cs
Normal file
@@ -0,0 +1,354 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Logging;
|
||||
using Microsoft.Owin.Security;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom sign in manager due to SignInManager not being .NET Standard.
|
||||
/// Can be removed once the web project moves to .NET Core.
|
||||
/// </summary>
|
||||
public class BackOfficeSignInManager2
|
||||
{
|
||||
private readonly BackOfficeUserManager2<BackOfficeIdentityUser> _userManager;
|
||||
private readonly IAuthenticationManager _authenticationManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IOwinRequest _request;
|
||||
|
||||
public BackOfficeSignInManager2(
|
||||
BackOfficeUserManager2<BackOfficeIdentityUser> userManager,
|
||||
IAuthenticationManager authenticationManager,
|
||||
ILogger logger,
|
||||
IGlobalSettings globalSettings,
|
||||
IOwinRequest request)
|
||||
{
|
||||
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
|
||||
_authenticationManager = authenticationManager ?? throw new ArgumentNullException(nameof(authenticationManager));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings));
|
||||
_request = request ?? throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
public Task<ClaimsIdentity> CreateUserIdentityAsync(BackOfficeIdentityUser user)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static BackOfficeSignInManager2 Create(IOwinContext context, IGlobalSettings globalSettings, ILogger logger)
|
||||
{
|
||||
return new BackOfficeSignInManager2(
|
||||
context.GetBackOfficeUserManager2(),
|
||||
context.Authentication,
|
||||
logger,
|
||||
globalSettings,
|
||||
context.Request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign in the user in using the user name and password
|
||||
/// </summary>
|
||||
/// <param name="userName"/><param name="password"/><param name="isPersistent"/><param name="shouldLockout"/>
|
||||
/// <returns/>
|
||||
public async Task<SignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)
|
||||
{
|
||||
var result = await PasswordSignInAsyncImpl(userName, password, isPersistent, shouldLockout);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"User: {userName} logged in from IP address {_request.RemoteIpAddress}", null, null);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, the user is locked", null, null);
|
||||
}
|
||||
else if (result.RequiresTwoFactor)
|
||||
{
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"Login attempt requires verification for username {userName} from IP address {_request.RemoteIpAddress}", null, null);
|
||||
}
|
||||
else if (!result.Succeeded || result.IsNotAllowed)
|
||||
{
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}", null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Borrowed from Microsoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type
|
||||
/// </summary>
|
||||
/// <param name="userName"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <param name="isPersistent"></param>
|
||||
/// <param name="shouldLockout"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<SignInResult> PasswordSignInAsyncImpl(string userName, string password, bool isPersistent, bool shouldLockout)
|
||||
{
|
||||
var user = await _userManager.FindByNameAsync(userName);
|
||||
|
||||
//if the user is null, create an empty one which can be used for auto-linking
|
||||
if (user == null) user = BackOfficeIdentityUser.CreateNew(_globalSettings, userName, null, _globalSettings.DefaultUILanguage);
|
||||
|
||||
//check the password for the user, this will allow a developer to auto-link
|
||||
//an account if they have specified an IBackOfficeUserPasswordChecker
|
||||
if (await _userManager.CheckPasswordAsync(user, password))
|
||||
{
|
||||
//the underlying call to this will query the user by Id which IS cached!
|
||||
if (await _userManager.IsLockedOutAsync(user))
|
||||
{
|
||||
return SignInResult.LockedOut;
|
||||
}
|
||||
|
||||
// We need to verify that the user belongs to one or more groups that define content and media start nodes.
|
||||
// To do so we have to create the user claims identity and validate the calculated start nodes.
|
||||
var userIdentity = await CreateUserIdentityAsync(user);
|
||||
if (userIdentity is UmbracoBackOfficeIdentity backOfficeIdentity)
|
||||
{
|
||||
if (backOfficeIdentity.StartContentNodes.Length == 0 || backOfficeIdentity.StartMediaNodes.Length == 0)
|
||||
{
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, no content and/or media start nodes could be found for any of the user's groups", null, null);
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
await _userManager.ResetAccessFailedCountAsync(user);
|
||||
return await SignInOrTwoFactor(user, isPersistent);
|
||||
}
|
||||
|
||||
var requestContext = _request.Context;
|
||||
|
||||
if (user.HasIdentity && shouldLockout)
|
||||
{
|
||||
// If lockout is requested, increment access failed count which might lock out the user
|
||||
await _userManager.AccessFailedAsync(user);
|
||||
if (await _userManager.IsLockedOutAsync(user))
|
||||
{
|
||||
//at this point we've just locked the user out after too many failed login attempts
|
||||
|
||||
if (requestContext != null)
|
||||
{
|
||||
var backofficeUserManager = requestContext.GetBackOfficeUserManager();
|
||||
if (backofficeUserManager != null) backofficeUserManager.RaiseAccountLockedEvent(user.Id);
|
||||
}
|
||||
|
||||
return SignInResult.LockedOut;
|
||||
}
|
||||
}
|
||||
|
||||
if (requestContext != null)
|
||||
{
|
||||
var backofficeUserManager = requestContext.GetBackOfficeUserManager();
|
||||
if (backofficeUserManager != null)
|
||||
backofficeUserManager.RaiseInvalidLoginAttemptEvent(userName);
|
||||
}
|
||||
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Borrowed from Microsoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="isPersistent"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<SignInResult> SignInOrTwoFactor(BackOfficeIdentityUser user, bool isPersistent)
|
||||
{
|
||||
var id = Convert.ToString(user.Id);
|
||||
if (await _userManager.GetTwoFactorEnabledAsync(user)
|
||||
&& (await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0)
|
||||
{
|
||||
var identity = new ClaimsIdentity(Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id));
|
||||
identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, user.UserName));
|
||||
_authenticationManager.SignIn(identity);
|
||||
return SignInResult.TwoFactorRequired;
|
||||
}
|
||||
await SignInAsync(user, isPersistent, false);
|
||||
return SignInResult.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a user identity and then signs the identity using the AuthenticationManager
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="isPersistent"></param>
|
||||
/// <param name="rememberBrowser"></param>
|
||||
/// <returns></returns>
|
||||
public async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, bool rememberBrowser)
|
||||
{
|
||||
var userIdentity = await CreateUserIdentityAsync(user);
|
||||
|
||||
// Clear any partial cookies from external or two factor partial sign ins
|
||||
_authenticationManager.SignOut(
|
||||
Constants.Security.BackOfficeExternalAuthenticationType,
|
||||
Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
|
||||
var nowUtc = DateTime.Now.ToUniversalTime();
|
||||
|
||||
if (rememberBrowser)
|
||||
{
|
||||
var rememberBrowserIdentity = _authenticationManager.CreateTwoFactorRememberBrowserIdentity(ConvertIdToString(user.Id));
|
||||
_authenticationManager.SignIn(new AuthenticationProperties()
|
||||
{
|
||||
IsPersistent = isPersistent,
|
||||
AllowRefresh = true,
|
||||
IssuedUtc = nowUtc,
|
||||
ExpiresUtc = nowUtc.AddMinutes(_globalSettings.TimeOutInMinutes)
|
||||
}, userIdentity, rememberBrowserIdentity);
|
||||
}
|
||||
else
|
||||
{
|
||||
_authenticationManager.SignIn(new AuthenticationProperties()
|
||||
{
|
||||
IsPersistent = isPersistent,
|
||||
AllowRefresh = true,
|
||||
IssuedUtc = nowUtc,
|
||||
ExpiresUtc = nowUtc.AddMinutes(_globalSettings.TimeOutInMinutes)
|
||||
}, userIdentity);
|
||||
}
|
||||
|
||||
//track the last login date
|
||||
user.LastLoginDateUtc = DateTime.UtcNow;
|
||||
if (user.AccessFailedCount > 0)
|
||||
//we have successfully logged in, reset the AccessFailedCount
|
||||
user.AccessFailedCount = 0;
|
||||
await _userManager.UpdateAsync(user);
|
||||
|
||||
//set the current request's principal to the identity just signed in!
|
||||
_request.User = new ClaimsPrincipal(userIdentity);
|
||||
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
string.Format(
|
||||
"Login attempt succeeded for username {0} from IP address {1}",
|
||||
user.UserName,
|
||||
_request.RemoteIpAddress), null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the user id that has been verified already or int.MinValue if the user has not been verified yet
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// Replaces the underlying call which is not flexible and doesn't support a custom cookie
|
||||
/// </remarks>
|
||||
public async Task<int> GetVerifiedUserIdAsync()
|
||||
{
|
||||
var result = await _authenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
if (result != null && result.Identity != null && string.IsNullOrEmpty(result.Identity.GetUserId()) == false)
|
||||
{
|
||||
return ConvertIdFromString(result.Identity.GetUserId());
|
||||
}
|
||||
return int.MinValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the username that has been verified already or null.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<string> GetVerifiedUserNameAsync()
|
||||
{
|
||||
var result = await _authenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
if (result != null && result.Identity != null && string.IsNullOrEmpty(result.Identity.GetUserName()) == false)
|
||||
{
|
||||
return result.Identity.GetUserName();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Two factor verification step
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="isPersistent"></param>
|
||||
/// <param name="rememberBrowser"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This is implemented because we cannot override GetVerifiedUserIdAsync and instead we have to shadow it
|
||||
/// so due to this and because we are using an INT as the TKey and not an object, it can never be null. Adding to that
|
||||
/// the default(int) value returned by the base class is always a valid user (i.e. the admin) so we just have to duplicate
|
||||
/// all of this code to check for int.MinValue
|
||||
/// </remarks>
|
||||
public async Task<SignInResult> TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberBrowser)
|
||||
{
|
||||
var userId = await GetVerifiedUserIdAsync();
|
||||
if (userId == int.MinValue)
|
||||
{
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
var user = await _userManager.FindByIdAsync(ConvertIdToString(userId));
|
||||
if (user == null)
|
||||
{
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
if (await _userManager.IsLockedOutAsync(user))
|
||||
{
|
||||
return SignInResult.LockedOut;
|
||||
}
|
||||
if (await _userManager.VerifyTwoFactorTokenAsync(user, provider, code))
|
||||
{
|
||||
// When token is verified correctly, clear the access failed count used for lockout
|
||||
await _userManager.ResetAccessFailedCountAsync(user);
|
||||
await SignInAsync(user, isPersistent, rememberBrowser);
|
||||
return SignInResult.Success;
|
||||
}
|
||||
|
||||
// If the token is incorrect, record the failure which also may cause the user to be locked out
|
||||
await _userManager.AccessFailedAsync(user);
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
/// <summary>Send a two factor code to a user</summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This is implemented because we cannot override GetVerifiedUserIdAsync and instead we have to shadow it
|
||||
/// so due to this and because we are using an INT as the TKey and not an object, it can never be null. Adding to that
|
||||
/// the default(int) value returned by the base class is always a valid user (i.e. the admin) so we just have to duplicate
|
||||
/// all of this code to check for int.MinVale instead.
|
||||
/// </remarks>
|
||||
public async Task<bool> SendTwoFactorCodeAsync(string provider)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
/*var userId = await GetVerifiedUserIdAsync();
|
||||
if (userId == int.MinValue)
|
||||
return false;
|
||||
|
||||
var token = await _userManager.GenerateTwoFactorTokenAsync(userId, provider);
|
||||
|
||||
|
||||
var identityResult = await _userManager.NotifyTwoFactorTokenAsync(userId, provider, token);
|
||||
return identityResult.Succeeded;*/
|
||||
}
|
||||
|
||||
private string ConvertIdToString(int id)
|
||||
{
|
||||
return Convert.ToString(id, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private int ConvertIdFromString(string id)
|
||||
{
|
||||
return id == null ? default(int) : (int) Convert.ChangeType(id, typeof(int), CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
615
src/Umbraco.Web/Security/BackOfficeUserManager2.cs
Normal file
615
src/Umbraco.Web/Security/BackOfficeUserManager2.cs
Normal file
@@ -0,0 +1,615 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
public class BackOfficeUserManager2 : BackOfficeUserManager2<BackOfficeIdentityUser>
|
||||
{
|
||||
public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker";
|
||||
|
||||
public BackOfficeUserManager2(
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IUserStore<BackOfficeIdentityUser> store,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IPasswordHasher<BackOfficeIdentityUser> passwordHasher,
|
||||
IEnumerable<IUserValidator<BackOfficeIdentityUser>> userValidators,
|
||||
IEnumerable<IPasswordValidator<BackOfficeIdentityUser>> passwordValidators,
|
||||
ILookupNormalizer keyNormalizer,
|
||||
IdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
: base(passwordConfiguration, ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
|
||||
{
|
||||
InitUserManager(this, passwordConfiguration);
|
||||
}
|
||||
|
||||
#region Static Create methods
|
||||
|
||||
// TODO: SB: Static Create methods for OWIN
|
||||
|
||||
/// <summary>
|
||||
/// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="userService"></param>
|
||||
/// <param name="entityService"></param>
|
||||
/// <param name="externalLoginService"></param>
|
||||
/// <param name="passwordConfiguration"></param>
|
||||
/// <param name="contentSectionConfig"></param>
|
||||
/// <param name="globalSettings"></param>
|
||||
/// <returns></returns>
|
||||
public static BackOfficeUserManager2 Create(
|
||||
IUserService userService,
|
||||
IEntityService entityService,
|
||||
IExternalLoginService externalLoginService,
|
||||
IGlobalSettings globalSettings,
|
||||
UmbracoMapper mapper,
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IPasswordHasher<BackOfficeIdentityUser> passwordHasher,
|
||||
IEnumerable<IUserValidator<BackOfficeIdentityUser>> userValidators,
|
||||
IEnumerable<IPasswordValidator<BackOfficeIdentityUser>> passwordValidators,
|
||||
ILookupNormalizer keyNormalizer,
|
||||
IdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
{
|
||||
var store = new BackOfficeUserStore2(userService, entityService, externalLoginService, globalSettings, mapper);
|
||||
return new BackOfficeUserManager2(
|
||||
passwordConfiguration,
|
||||
ipResolver,
|
||||
store,
|
||||
optionsAccessor,
|
||||
passwordHasher,
|
||||
userValidators,
|
||||
passwordValidators,
|
||||
keyNormalizer,
|
||||
errors,
|
||||
services,
|
||||
logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static BackOfficeUserManager2 Create(
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IUserStore<BackOfficeIdentityUser> store,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IPasswordHasher<BackOfficeIdentityUser> passwordHasher,
|
||||
IEnumerable<IUserValidator<BackOfficeIdentityUser>> userValidators,
|
||||
IEnumerable<IPasswordValidator<BackOfficeIdentityUser>> passwordValidators,
|
||||
ILookupNormalizer keyNormalizer,
|
||||
IdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
{
|
||||
return new BackOfficeUserManager2(
|
||||
passwordConfiguration,
|
||||
ipResolver,
|
||||
store,
|
||||
optionsAccessor,
|
||||
passwordHasher,
|
||||
userValidators,
|
||||
passwordValidators,
|
||||
keyNormalizer,
|
||||
errors,
|
||||
services,
|
||||
logger);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class BackOfficeUserManager2<T> : UserManager<T>
|
||||
where T : BackOfficeIdentityUser
|
||||
{
|
||||
private PasswordGenerator _passwordGenerator;
|
||||
|
||||
public BackOfficeUserManager2(
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IUserStore<T> store,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IPasswordHasher<T> passwordHasher,
|
||||
IEnumerable<IUserValidator<T>> userValidators,
|
||||
IEnumerable<IPasswordValidator<T>> passwordValidators,
|
||||
ILookupNormalizer keyNormalizer,
|
||||
IdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
ILogger<UserManager<T>> logger)
|
||||
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
|
||||
{
|
||||
PasswordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration));
|
||||
IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver));
|
||||
}
|
||||
|
||||
#region What we do not currently support
|
||||
// TODO: We could support this - but a user claims will mostly just be what is in the auth cookie
|
||||
public override bool SupportsUserClaim => false;
|
||||
|
||||
// TODO: Support this
|
||||
public override bool SupportsQueryableUsers => false;
|
||||
|
||||
/// <summary>
|
||||
/// Developers will need to override this to support custom 2 factor auth
|
||||
/// </summary>
|
||||
public override bool SupportsUserTwoFactor => false;
|
||||
|
||||
// TODO: Support this
|
||||
public override bool SupportsUserPhoneNumber => false;
|
||||
#endregion
|
||||
|
||||
// TODO: SB: INIT
|
||||
/// <summary>
|
||||
/// Initializes the user manager with the correct options
|
||||
/// </summary>
|
||||
/// <param name="manager"></param>
|
||||
/// <param name="passwordConfig"></param>
|
||||
/// <returns></returns>
|
||||
protected void InitUserManager(
|
||||
BackOfficeUserManager2<T> manager,
|
||||
IPasswordConfiguration passwordConfig)
|
||||
// IDataProtectionProvider dataProtectionProvider
|
||||
{
|
||||
// Configure validation logic for usernames
|
||||
manager.UserValidators.Clear();
|
||||
manager.UserValidators.Add(new BackOfficeUserValidator2<T>());
|
||||
manager.Options.User.RequireUniqueEmail = true;
|
||||
|
||||
// Configure validation logic for passwords
|
||||
manager.PasswordValidators.Clear();
|
||||
manager.PasswordValidators.Add(new PasswordValidator<T>());
|
||||
manager.Options.Password.RequiredLength = passwordConfig.RequiredLength;
|
||||
manager.Options.Password.RequireNonAlphanumeric = passwordConfig.RequireNonLetterOrDigit;
|
||||
manager.Options.Password.RequireDigit = passwordConfig.RequireDigit;
|
||||
manager.Options.Password.RequireLowercase = passwordConfig.RequireLowercase;
|
||||
manager.Options.Password.RequireUppercase = passwordConfig.RequireUppercase;
|
||||
|
||||
//use a custom hasher based on our membership provider
|
||||
manager.PasswordHasher = GetDefaultPasswordHasher(passwordConfig);
|
||||
|
||||
// TODO: SB: manager.Options.Tokens using OWIN data protector
|
||||
/*if (dataProtectionProvider != null)
|
||||
{
|
||||
manager.UserTokenProvider = new DataProtectorTokenProvider<T, int>(dataProtectionProvider.Create("ASP.NET Identity"))
|
||||
{
|
||||
TokenLifespan = TimeSpan.FromDays(3)
|
||||
};
|
||||
}*/
|
||||
|
||||
manager.Options.Lockout.AllowedForNewUsers = true;
|
||||
manager.Options.Lockout.MaxFailedAccessAttempts = passwordConfig.MaxFailedAccessAttemptsBeforeLockout;
|
||||
//NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked
|
||||
// or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are
|
||||
// locked out or not.
|
||||
manager.Options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to validate a user's session
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<bool> ValidateSessionIdAsync(int userId, string sessionId)
|
||||
{
|
||||
var userSessionStore = Store as IUserSessionStore2<T, int>;
|
||||
//if this is not set, for backwards compat (which would be super rare), we'll just approve it
|
||||
if (userSessionStore == null) return true;
|
||||
|
||||
return await userSessionStore.ValidateSessionIdAsync(userId, sessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will determine which password hasher to use based on what is defined in config
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual IPasswordHasher<T> GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration)
|
||||
{
|
||||
//we can use the user aware password hasher (which will be the default and preferred way)
|
||||
return new UserAwarePasswordHasher2<T>(new PasswordSecurity(passwordConfiguration));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets/sets the default back office user password checker
|
||||
/// </summary>
|
||||
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; }
|
||||
public IPasswordConfiguration PasswordConfiguration { get; }
|
||||
public IIpResolver IpResolver { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to generate a password for a user based on the current password validator
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string GeneratePassword()
|
||||
{
|
||||
if (_passwordGenerator == null) _passwordGenerator = new PasswordGenerator(PasswordConfiguration);
|
||||
var password = _passwordGenerator.GeneratePassword();
|
||||
return password;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values
|
||||
/// </remarks>
|
||||
public override async Task<bool> IsLockedOutAsync(T user)
|
||||
{
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
if (user.IsApproved == false) return true;
|
||||
|
||||
return await base.IsLockedOutAsync(user);
|
||||
}
|
||||
|
||||
#region Overrides for password logic
|
||||
|
||||
/// <summary>
|
||||
/// Logic used to validate a username and password
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// By default this uses the standard ASP.Net Identity approach which is:
|
||||
/// * Get password store
|
||||
/// * Call VerifyPasswordAsync with the password store + user + password
|
||||
/// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password
|
||||
///
|
||||
/// In some cases people want simple custom control over the username/password check, for simplicity
|
||||
/// sake, developers would like the users to simply validate against an LDAP directory but the user
|
||||
/// data remains stored inside of Umbraco.
|
||||
/// See: http://issues.umbraco.org/issue/U4-7032 for the use cases.
|
||||
///
|
||||
/// We've allowed this check to be overridden with a simple callback so that developers don't actually
|
||||
/// have to implement/override this class.
|
||||
/// </remarks>
|
||||
public override async Task<bool> CheckPasswordAsync(T user, string password)
|
||||
{
|
||||
if (BackOfficeUserPasswordChecker != null)
|
||||
{
|
||||
var result = await BackOfficeUserPasswordChecker.CheckPasswordAsync(user, password);
|
||||
|
||||
if (user.HasIdentity == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//if the result indicates to not fallback to the default, then return true if the credentials are valid
|
||||
if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker)
|
||||
{
|
||||
return result == BackOfficeUserPasswordCheckerResult.ValidCredentials;
|
||||
}
|
||||
}
|
||||
|
||||
//we cannot proceed if the user passed in does not have an identity
|
||||
if (user.HasIdentity == false)
|
||||
return false;
|
||||
|
||||
//use the default behavior
|
||||
return await base.CheckPasswordAsync(user, password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <param name="newPassword"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// We use this because in the back office the only way an admin can change another user's password without first knowing their password
|
||||
/// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset
|
||||
/// </remarks>
|
||||
public async Task<IdentityResult> ChangePasswordWithResetAsync(int userId, string token, string newPassword)
|
||||
{
|
||||
var user = await base.FindByIdAsync(userId.ToString());
|
||||
var result = await base.ResetPasswordAsync(user, token, newPassword);
|
||||
if (result.Succeeded) RaisePasswordChangedEvent(userId);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async Task<IdentityResult> ChangePasswordAsync(T user, string currentPassword, string newPassword)
|
||||
{
|
||||
var result = await base.ChangePasswordAsync(user, currentPassword, newPassword);
|
||||
if (result.Succeeded) RaisePasswordChangedEvent(user.Id);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to determine how to hash the password
|
||||
/// </summary>
|
||||
/// <param name="store"></param>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
protected override async Task<PasswordVerificationResult> VerifyPasswordAsync(IUserPasswordStore<T> store, T user, string password)
|
||||
{
|
||||
var userAwarePasswordHasher = PasswordHasher;
|
||||
if (userAwarePasswordHasher == null)
|
||||
return await base.VerifyPasswordAsync(store, user, password);
|
||||
|
||||
var hash = await store.GetPasswordHashAsync(user, CancellationToken.None);
|
||||
return userAwarePasswordHasher.VerifyHashedPassword(user, hash, password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to determine how to hash the password
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="newPassword"></param>
|
||||
/// <param name="validatePassword"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used)
|
||||
/// </remarks>
|
||||
protected override async Task<IdentityResult> UpdatePasswordHash(T user, string newPassword, bool validatePassword)
|
||||
{
|
||||
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
|
||||
|
||||
if (validatePassword)
|
||||
{
|
||||
var validate = await ValidatePasswordAsync(user, newPassword);
|
||||
if (!validate.Succeeded)
|
||||
{
|
||||
return validate;
|
||||
}
|
||||
}
|
||||
|
||||
var passwordStore = Store as IUserPasswordStore<T>;
|
||||
if (passwordStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>));
|
||||
|
||||
var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null;
|
||||
await passwordStore.SetPasswordHashAsync(user, hash, CancellationToken);
|
||||
await UpdateSecurityStampInternal(user);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is copied from the underlying .NET base class since they decided to not expose it
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
private async Task UpdateSecurityStampInternal(T user)
|
||||
{
|
||||
if (SupportsUserSecurityStamp == false) return;
|
||||
await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is copied from the underlying .NET base class since they decided to not expose it
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private IUserSecurityStampStore<T> GetSecurityStore()
|
||||
{
|
||||
var store = Store as IUserSecurityStampStore<T>;
|
||||
if (store == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
|
||||
return store;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is copied from the underlying .NET base class since they decided to not expose it
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static string NewSecurityStamp()
|
||||
{
|
||||
return Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override async Task<IdentityResult> SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd)
|
||||
{
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var result = await base.SetLockoutEndDateAsync(user, lockoutEnd);
|
||||
|
||||
// The way we unlock is by setting the lockoutEnd date to the current datetime
|
||||
if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow)
|
||||
{
|
||||
RaiseAccountLockedEvent(user.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
RaiseAccountUnlockedEvent(user.Id);
|
||||
//Resets the login attempt fails back to 0 when unlock is clicked
|
||||
await ResetAccessFailedCountAsync(user);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async Task<IdentityResult> ResetAccessFailedCountAsync(T user)
|
||||
{
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var lockoutStore = (IUserLockoutStore<T>)Store;
|
||||
var accessFailedCount = await GetAccessFailedCountAsync(user);
|
||||
|
||||
if (accessFailedCount == 0)
|
||||
return IdentityResult.Success;
|
||||
|
||||
await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None);
|
||||
//raise the event now that it's reset
|
||||
RaiseResetAccessFailedCountEvent(user.Id);
|
||||
return await UpdateAsync(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the Microsoft ASP.NET user management method
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns>
|
||||
/// returns a Async Task<IdentityResult />
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Doesn't set fail attempts back to 0
|
||||
/// </remarks>
|
||||
public override async Task<IdentityResult> AccessFailedAsync(T user)
|
||||
{
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var lockoutStore = Store as IUserLockoutStore<T>;
|
||||
if (lockoutStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>));
|
||||
|
||||
var count = await lockoutStore.IncrementAccessFailedCountAsync(user, CancellationToken.None);
|
||||
|
||||
if (count >= Options.Lockout.MaxFailedAccessAttempts)
|
||||
{
|
||||
await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan),
|
||||
CancellationToken.None);
|
||||
//NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0
|
||||
//here we are persisting the value for the back office
|
||||
}
|
||||
|
||||
var result = await UpdateAsync(user);
|
||||
|
||||
//Slightly confusing: this will return a Success if we successfully update the AccessFailed count
|
||||
if (result.Succeeded) RaiseLoginFailedEvent(user.Id);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal void RaiseAccountLockedEvent(int userId)
|
||||
{
|
||||
OnAccountLocked(new IdentityAuditEventArgs(AuditEvent.AccountLocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId));
|
||||
}
|
||||
|
||||
internal void RaiseAccountUnlockedEvent(int userId)
|
||||
{
|
||||
OnAccountUnlocked(new IdentityAuditEventArgs(AuditEvent.AccountUnlocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId));
|
||||
}
|
||||
|
||||
internal void RaiseForgotPasswordRequestedEvent(int userId)
|
||||
{
|
||||
OnForgotPasswordRequested(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordRequested, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId));
|
||||
}
|
||||
|
||||
internal void RaiseForgotPasswordChangedSuccessEvent(int userId)
|
||||
{
|
||||
OnForgotPasswordChangedSuccess(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordChangedSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId));
|
||||
}
|
||||
|
||||
internal void RaiseLoginFailedEvent(int userId)
|
||||
{
|
||||
OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId));
|
||||
}
|
||||
|
||||
internal void RaiseInvalidLoginAttemptEvent(string username)
|
||||
{
|
||||
OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), username, string.Format("Attempted login for username '{0}' failed", username)));
|
||||
}
|
||||
|
||||
internal void RaiseLoginRequiresVerificationEvent(int userId)
|
||||
{
|
||||
OnLoginRequiresVerification(new IdentityAuditEventArgs(AuditEvent.LoginRequiresVerification, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId));
|
||||
}
|
||||
|
||||
internal void RaiseLoginSuccessEvent(int userId)
|
||||
{
|
||||
OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId));
|
||||
}
|
||||
|
||||
internal void RaiseLogoutSuccessEvent(int userId)
|
||||
{
|
||||
OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId));
|
||||
}
|
||||
|
||||
internal void RaisePasswordChangedEvent(int userId)
|
||||
{
|
||||
OnPasswordChanged(new IdentityAuditEventArgs(AuditEvent.PasswordChanged, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId));
|
||||
}
|
||||
|
||||
internal void RaiseResetAccessFailedCountEvent(int userId)
|
||||
{
|
||||
OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId));
|
||||
}
|
||||
|
||||
public static event EventHandler AccountLocked;
|
||||
public static event EventHandler AccountUnlocked;
|
||||
public static event EventHandler ForgotPasswordRequested;
|
||||
public static event EventHandler ForgotPasswordChangedSuccess;
|
||||
public static event EventHandler LoginFailed;
|
||||
public static event EventHandler LoginRequiresVerification;
|
||||
public static event EventHandler LoginSuccess;
|
||||
public static event EventHandler LogoutSuccess;
|
||||
public static event EventHandler PasswordChanged;
|
||||
public static event EventHandler PasswordReset;
|
||||
public static event EventHandler ResetAccessFailedCount;
|
||||
|
||||
protected virtual void OnAccountLocked(IdentityAuditEventArgs e)
|
||||
{
|
||||
if (AccountLocked != null) AccountLocked(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e)
|
||||
{
|
||||
if (AccountUnlocked != null) AccountUnlocked(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e)
|
||||
{
|
||||
if (ForgotPasswordRequested != null) ForgotPasswordRequested(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e)
|
||||
{
|
||||
if (ForgotPasswordChangedSuccess != null) ForgotPasswordChangedSuccess(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnLoginFailed(IdentityAuditEventArgs e)
|
||||
{
|
||||
if (LoginFailed != null) LoginFailed(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e)
|
||||
{
|
||||
if (LoginRequiresVerification != null) LoginRequiresVerification(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnLoginSuccess(IdentityAuditEventArgs e)
|
||||
{
|
||||
if (LoginSuccess != null) LoginSuccess(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnLogoutSuccess(IdentityAuditEventArgs e)
|
||||
{
|
||||
if (LogoutSuccess != null) LogoutSuccess(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnPasswordChanged(IdentityAuditEventArgs e)
|
||||
{
|
||||
if (PasswordChanged != null) PasswordChanged(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnPasswordReset(IdentityAuditEventArgs e)
|
||||
{
|
||||
if (PasswordReset != null) PasswordReset(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e)
|
||||
{
|
||||
if (ResetAccessFailedCount != null) ResetAccessFailedCount(this, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/Umbraco.Web/Security/BackOfficeUserValidator2.cs
Normal file
20
src/Umbraco.Web/Security/BackOfficeUserValidator2.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
public class BackOfficeUserValidator2<T> : UserValidator<T>
|
||||
where T : BackOfficeIdentityUser
|
||||
{
|
||||
public override async Task<IdentityResult> ValidateAsync(UserManager<T> manager, T user)
|
||||
{
|
||||
// Don't validate if the user's email or username hasn't changed otherwise it's just wasting SQL queries.
|
||||
if (user.IsPropertyDirty("Email") || user.IsPropertyDirty("UserName"))
|
||||
{
|
||||
return await base.ValidateAsync(manager, user);
|
||||
}
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,13 @@ namespace Umbraco.Web.Security
|
||||
{
|
||||
BackOfficeUserManager<BackOfficeIdentityUser> GetManager(IOwinContext owin);
|
||||
}
|
||||
/// <summary>
|
||||
/// This interface is only here due to the fact that IOwinContext Get / Set only work in generics, if they worked
|
||||
/// with regular 'object' then we wouldn't have to use this work around but because of that we have to use this
|
||||
/// class to resolve the 'real' type of the registered user manager
|
||||
/// </summary>
|
||||
internal interface IBackOfficeUserManagerMarker2
|
||||
{
|
||||
BackOfficeUserManager2<BackOfficeIdentityUser> GetManager(IOwinContext owin);
|
||||
}
|
||||
}
|
||||
|
||||
40
src/Umbraco.Web/Security/UserAwarePasswordHasher2.cs
Normal file
40
src/Umbraco.Web/Security/UserAwarePasswordHasher2.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
public class UserAwarePasswordHasher2<T> : IPasswordHasher<T>
|
||||
where T : BackOfficeIdentityUser
|
||||
{
|
||||
private readonly PasswordSecurity _passwordSecurity;
|
||||
|
||||
public UserAwarePasswordHasher2(PasswordSecurity passwordSecurity)
|
||||
{
|
||||
_passwordSecurity = passwordSecurity;
|
||||
}
|
||||
|
||||
public string HashPassword(string password)
|
||||
{
|
||||
return _passwordSecurity.HashPasswordForStorage(password);
|
||||
}
|
||||
|
||||
public string HashPassword(T user, string password)
|
||||
{
|
||||
// TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
|
||||
//NOTE: For now this just falls back to the hashing we are currently using
|
||||
|
||||
return HashPassword(password);
|
||||
}
|
||||
|
||||
public PasswordVerificationResult VerifyHashedPassword(T user, string hashedPassword, string providedPassword)
|
||||
{
|
||||
// TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
|
||||
//NOTE: For now this just falls back to the hashing we are currently using
|
||||
|
||||
return _passwordSecurity.VerifyPassword(providedPassword, hashedPassword)
|
||||
? PasswordVerificationResult.Success
|
||||
: PasswordVerificationResult.Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,7 @@
|
||||
<Compile Include="AspNet\AspNetHostingEnvironment.cs" />
|
||||
<Compile Include="AspNet\FrameworkMarchal.cs" />
|
||||
<Compile Include="Cache\WebCachingAppCache.cs" />
|
||||
<Compile Include="ClaimsIdentityExtensions.cs" />
|
||||
<Compile Include="Compose\AuditEventsComponent.cs" />
|
||||
<Compile Include="Compose\AuditEventsComposer.cs" />
|
||||
<Compile Include="Compose\BackOfficeUserAuditEventsComponent.cs" />
|
||||
@@ -197,9 +198,12 @@
|
||||
<Compile Include="Runtime\AspNetUmbracoBootPermissionChecker.cs" />
|
||||
<Compile Include="Scheduling\SchedulerComponent.cs" />
|
||||
<Compile Include="Scheduling\SchedulerComposer.cs" />
|
||||
<Compile Include="Security\BackOfficeSignInManager2.cs" />
|
||||
<Compile Include="Security\BackOfficeUserManager2.cs" />
|
||||
<Compile Include="Security\BackOfficeUserStore.cs" />
|
||||
<Compile Include="Security\BackOfficeUserStore2.cs" />
|
||||
<Compile Include="Security\BackOfficeUserValidator.cs" />
|
||||
<Compile Include="Security\BackOfficeUserValidator2.cs" />
|
||||
<Compile Include="Security\ConfiguredPasswordValidator.cs" />
|
||||
<Compile Include="Security\EmailService.cs" />
|
||||
<Compile Include="Security\IUserAwarePasswordHasher.cs" />
|
||||
@@ -211,6 +215,7 @@
|
||||
<Compile Include="Security\UmbracoEmailMessage.cs" />
|
||||
<Compile Include="Security\UmbracoMembershipProviderBase.cs" />
|
||||
<Compile Include="Security\UserAwarePasswordHasher.cs" />
|
||||
<Compile Include="Security\UserAwarePasswordHasher2.cs" />
|
||||
<Compile Include="StringExtensions.cs" />
|
||||
<Compile Include="Trees\TreeCollectionBuilder.cs" />
|
||||
<Compile Include="UmbracoContext.cs" />
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Web.WebApi.Filters;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Web.WebApi.Filters;using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
@@ -31,7 +30,7 @@ namespace Umbraco.Web.WebApi
|
||||
[EnableDetailedErrors]
|
||||
public abstract class UmbracoAuthorizedApiController : UmbracoApiController
|
||||
{
|
||||
private BackOfficeUserManager<BackOfficeIdentityUser> _userManager;
|
||||
private BackOfficeUserManager2<BackOfficeIdentityUser> _userManager;
|
||||
|
||||
protected UmbracoAuthorizedApiController()
|
||||
{
|
||||
@@ -45,7 +44,7 @@ namespace Umbraco.Web.WebApi
|
||||
/// <summary>
|
||||
/// Gets the user manager.
|
||||
/// </summary>
|
||||
protected BackOfficeUserManager<BackOfficeIdentityUser> UserManager
|
||||
=> _userManager ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager());
|
||||
protected BackOfficeUserManager2<BackOfficeIdentityUser> UserManager
|
||||
=> _userManager ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager2());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user