using System; using System.Diagnostics; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Microsoft.Owin.Logging; using Microsoft.Owin.Security; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Identity; using Umbraco.Core.Security; using Umbraco.Web.Models.Identity; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Security { // TODO: In v8 we need to change this to use an int? nullable TKey instead, see notes against overridden TwoFactorSignInAsync public class BackOfficeSignInManager : SignInManager { private readonly ILogger _logger; private readonly IOwinRequest _request; private readonly IGlobalSettings _globalSettings; public BackOfficeSignInManager(UserManager userManager, IAuthenticationManager authenticationManager, ILogger logger, IGlobalSettings globalSettings, IOwinRequest request) : base(userManager, authenticationManager) { if (logger == null) throw new ArgumentNullException("logger"); if (request == null) throw new ArgumentNullException("request"); _logger = logger; _request = request; _globalSettings = globalSettings; AuthenticationType = Constants.Security.BackOfficeAuthenticationType; } public override Task CreateUserIdentityAsync(BackOfficeIdentityUser user) { return ((BackOfficeUserManager)UserManager).GenerateUserIdentityAsync(user); } public static BackOfficeSignInManager Create(IdentityFactoryOptions options, IOwinContext context, IGlobalSettings globalSettings, ILogger logger) { return new BackOfficeSignInManager( context.GetBackOfficeUserManager(), context.Authentication, logger, globalSettings, context.Request); } /// /// Sign in the user in using the user name and password /// /// /// public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout) { var result = await PasswordSignInAsyncImpl(userName, password, isPersistent, shouldLockout); switch (result) { case SignInStatus.Success: _logger.WriteCore(TraceEventType.Information, 0, $"User: {userName} logged in from IP address {_request.RemoteIpAddress}", null, null); break; case SignInStatus.LockedOut: _logger.WriteCore(TraceEventType.Information, 0, $"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, the user is locked", null, null); break; case SignInStatus.RequiresVerification: _logger.WriteCore(TraceEventType.Information, 0, $"Login attempt requires verification for username {userName} from IP address {_request.RemoteIpAddress}", null, null); break; case SignInStatus.Failure: _logger.WriteCore(TraceEventType.Information, 0, $"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}", null, null); break; default: throw new ArgumentOutOfRangeException(); } return result; } /// /// Borrowed from Microsoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type /// /// /// /// /// /// private async Task PasswordSignInAsyncImpl(string userName, string password, bool isPersistent, bool shouldLockout) { if (UserManager == null) { return SignInStatus.Failure; } 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.Id)) { return SignInStatus.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 SignInStatus.Failure; } } await UserManager.ResetAccessFailedCountAsync(user.Id); 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.Id); if (await UserManager.IsLockedOutAsync(user.Id)) { //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 SignInStatus.LockedOut; } } if (requestContext != null) { var backofficeUserManager = requestContext.GetBackOfficeUserManager(); if (backofficeUserManager != null) backofficeUserManager.RaiseInvalidLoginAttemptEvent(userName); } return SignInStatus.Failure; } /// /// Borrowed from Microsoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type /// /// /// /// private async Task SignInOrTwoFactor(BackOfficeIdentityUser user, bool isPersistent) { var id = Convert.ToString(user.Id); if (await UserManager.GetTwoFactorEnabledAsync(user.Id) && (await UserManager.GetValidTwoFactorProvidersAsync(user.Id)).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 SignInStatus.RequiresVerification; } await SignInAsync(user, isPersistent, false); return SignInStatus.Success; } /// /// Creates a user identity and then signs the identity using the AuthenticationManager /// /// /// /// /// public override 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); } /// /// Get the user id that has been verified already or int.MinValue if the user has not been verified yet /// /// /// /// Replaces the underlying call which is not flexible and doesn't support a custom cookie /// public new async Task 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; } /// /// Get the username that has been verified already or null. /// /// public async Task 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; } /// /// Two factor verification step /// /// /// /// /// /// /// /// 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 /// public override async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberBrowser) { var userId = await GetVerifiedUserIdAsync(); if (userId == int.MinValue) { return SignInStatus.Failure; } var user = await UserManager.FindByIdAsync(userId); if (user == null) { return SignInStatus.Failure; } if (await UserManager.IsLockedOutAsync(user.Id)) { return SignInStatus.LockedOut; } if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, provider, code)) { // When token is verified correctly, clear the access failed count used for lockout await UserManager.ResetAccessFailedCountAsync(user.Id); await SignInAsync(user, isPersistent, rememberBrowser); return SignInStatus.Success; } // If the token is incorrect, record the failure which also may cause the user to be locked out await UserManager.AccessFailedAsync(user.Id); return SignInStatus.Failure; } /// Send a two factor code to a user /// /// /// /// 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. /// public override async Task SendTwoFactorCodeAsync(string provider) { 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; } } }