2017-07-20 11:21:28 +02:00
using System ;
2015-07-23 12:03:50 +02:00
using System.Diagnostics ;
2015-07-01 17:07:29 +02:00
using System.Security.Claims ;
using System.Threading.Tasks ;
using Microsoft.AspNet.Identity ;
using Microsoft.AspNet.Identity.Owin ;
using Microsoft.Owin ;
2015-07-23 12:03:50 +02:00
using Microsoft.Owin.Logging ;
2015-07-01 17:07:29 +02:00
using Microsoft.Owin.Security ;
2019-11-05 12:54:22 +01:00
using Umbraco.Core ;
2015-07-01 17:07:29 +02:00
using Umbraco.Core.Configuration ;
using Umbraco.Core.Models.Identity ;
2018-08-29 01:15:46 +10:00
using Umbraco.Core.Security ;
2020-01-07 13:50:38 +01:00
using Umbraco.Web.Models.Identity ;
2018-08-29 01:15:46 +10:00
using Constants = Umbraco . Core . Constants ;
2015-07-01 17:07:29 +02:00
2018-08-29 01:15:46 +10:00
namespace Umbraco.Web.Security
2015-07-01 17:07:29 +02:00
{
2019-01-27 01:17:32 -05:00
// TODO: In v8 we need to change this to use an int? nullable TKey instead, see notes against overridden TwoFactorSignInAsync
2015-07-01 17:07:29 +02:00
public class BackOfficeSignInManager : SignInManager < BackOfficeIdentityUser , int >
{
2015-07-23 12:03:50 +02:00
private readonly ILogger _logger ;
private readonly IOwinRequest _request ;
2018-04-06 13:51:54 +10:00
private readonly IGlobalSettings _globalSettings ;
2015-07-23 12:03:50 +02:00
2018-04-06 13:51:54 +10:00
public BackOfficeSignInManager ( UserManager < BackOfficeIdentityUser , int > userManager , IAuthenticationManager authenticationManager , ILogger logger , IGlobalSettings globalSettings , IOwinRequest request )
2015-07-01 17:07:29 +02:00
: base ( userManager , authenticationManager )
{
2015-07-23 12:03:50 +02:00
if ( logger = = null ) throw new ArgumentNullException ( "logger" ) ;
if ( request = = null ) throw new ArgumentNullException ( "request" ) ;
_logger = logger ;
_request = request ;
2018-04-06 13:51:54 +10:00
_globalSettings = globalSettings ;
2019-11-05 13:45:42 +01:00
AuthenticationType = Constants . Security . BackOfficeAuthenticationType ;
2015-07-01 17:07:29 +02:00
}
public override Task < ClaimsIdentity > CreateUserIdentityAsync ( BackOfficeIdentityUser user )
{
2018-08-29 01:15:46 +10:00
return ( ( BackOfficeUserManager < BackOfficeIdentityUser > ) UserManager ) . GenerateUserIdentityAsync ( user ) ;
2015-07-01 17:07:29 +02:00
}
2018-04-06 13:51:54 +10:00
public static BackOfficeSignInManager Create ( IdentityFactoryOptions < BackOfficeSignInManager > options , IOwinContext context , IGlobalSettings globalSettings , ILogger logger )
2015-07-01 17:07:29 +02:00
{
2015-07-23 12:03:50 +02:00
return new BackOfficeSignInManager (
2017-05-12 14:49:44 +02:00
context . GetBackOfficeUserManager ( ) ,
2015-07-23 12:03:50 +02:00
context . Authentication ,
logger ,
2018-04-06 13:51:54 +10:00
globalSettings ,
2015-07-23 12:03:50 +02:00
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/>
2016-07-12 13:36:08 +02:00
public override async Task < SignInStatus > PasswordSignInAsync ( string userName , string password , bool isPersistent , bool shouldLockout )
2015-07-23 12:03:50 +02:00
{
2017-05-12 14:49:44 +02:00
var result = await PasswordSignInAsyncImpl ( userName , password , isPersistent , shouldLockout ) ;
2017-07-20 11:21:28 +02:00
2015-07-23 12:03:50 +02:00
switch ( result )
{
case SignInStatus . Success :
2015-11-19 18:12:21 +01:00
_logger . WriteCore ( TraceEventType . Information , 0 ,
2018-04-06 13:51:54 +10:00
$"User: {userName} logged in from IP address {_request.RemoteIpAddress}" , null , null ) ;
2015-07-23 12:03:50 +02:00
break ;
case SignInStatus . LockedOut :
_logger . WriteCore ( TraceEventType . Information , 0 ,
2018-04-06 13:51:54 +10:00
$"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, the user is locked" , null , null ) ;
2015-07-23 12:03:50 +02:00
break ;
case SignInStatus . RequiresVerification :
_logger . WriteCore ( TraceEventType . Information , 0 ,
2018-04-06 13:51:54 +10:00
$"Login attempt requires verification for username {userName} from IP address {_request.RemoteIpAddress}" , null , null ) ;
2015-07-23 12:03:50 +02:00
break ;
case SignInStatus . Failure :
_logger . WriteCore ( TraceEventType . Information , 0 ,
2018-04-06 13:51:54 +10:00
$"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}" , null , null ) ;
2015-07-23 12:03:50 +02:00
break ;
default :
throw new ArgumentOutOfRangeException ( ) ;
}
return result ;
2015-07-01 17:07:29 +02:00
}
2017-05-12 14:49:44 +02:00
/// <summary>
2019-01-26 10:52:19 -05:00
/// Borrowed from Microsoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type
2017-05-12 14:49:44 +02:00
/// </summary>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <param name="isPersistent"></param>
/// <param name="shouldLockout"></param>
/// <returns></returns>
private async Task < SignInStatus > PasswordSignInAsyncImpl ( string userName , string password , bool isPersistent , bool shouldLockout )
{
if ( UserManager = = null )
{
return SignInStatus . Failure ;
}
2017-08-25 17:55:26 +02:00
2017-05-12 14:49:44 +02:00
var user = await UserManager . FindByNameAsync ( userName ) ;
2017-09-23 10:08:18 +02:00
2017-08-25 17:55:26 +02:00
//if the user is null, create an empty one which can be used for auto-linking
2017-09-23 10:08:18 +02:00
if ( user = = null )
2019-12-19 15:53:50 +01:00
user = BackOfficeIdentityUser . CreateNew ( _globalSettings , userName , null , _globalSettings . DefaultUILanguage ) ;
2017-09-23 10:08:18 +02:00
//check the password for the user, this will allow a developer to auto-link
2017-08-25 17:55:26 +02:00
//an account if they have specified an IBackOfficeUserPasswordChecker
2017-05-12 14:49:44 +02:00
if ( await UserManager . CheckPasswordAsync ( user , password ) )
{
2017-08-25 17:55:26 +02:00
//the underlying call to this will query the user by Id which IS cached!
if ( await UserManager . IsLockedOutAsync ( user . Id ) )
{
return SignInStatus . LockedOut ;
}
2019-07-16 19:44:11 +02:00
// 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 ) ;
2019-07-25 07:40:38 +02:00
if ( userIdentity is UmbracoBackOfficeIdentity backOfficeIdentity )
2019-07-16 19:44:11 +02:00
{
2019-07-25 07:40:44 +02:00
if ( backOfficeIdentity . StartContentNodes . Length = = 0 | | backOfficeIdentity . StartMediaNodes . Length = = 0 )
2019-07-16 19:44:11 +02:00
{
_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 ;
}
}
2017-05-12 14:49:44 +02:00
await UserManager . ResetAccessFailedCountAsync ( user . Id ) ;
return await SignInOrTwoFactor ( user , isPersistent ) ;
}
2017-08-25 17:55:26 +02:00
2017-09-19 16:46:49 +02:00
var requestContext = _request . Context ;
2017-08-25 17:55:26 +02:00
if ( user . HasIdentity & & shouldLockout )
2017-05-12 14:49:44 +02:00
{
// 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 ) )
{
2017-09-19 16:46:49 +02:00
//at this point we've just locked the user out after too many failed login attempts
2017-09-23 10:08:18 +02:00
2017-09-19 16:46:49 +02:00
if ( requestContext ! = null )
{
var backofficeUserManager = requestContext . GetBackOfficeUserManager ( ) ;
if ( backofficeUserManager ! = null )
backofficeUserManager . RaiseAccountLockedEvent ( user . Id ) ;
}
2017-05-12 14:49:44 +02:00
return SignInStatus . LockedOut ;
}
}
2017-09-23 10:08:18 +02:00
2017-09-19 16:46:49 +02:00
if ( requestContext ! = null )
{
var backofficeUserManager = requestContext . GetBackOfficeUserManager ( ) ;
if ( backofficeUserManager ! = null )
backofficeUserManager . RaiseInvalidLoginAttemptEvent ( userName ) ;
}
2017-05-12 14:49:44 +02:00
return SignInStatus . Failure ;
2017-09-23 10:08:18 +02:00
}
2017-05-12 14:49:44 +02:00
/// <summary>
2019-01-26 10:52:19 -05:00
/// Borrowed from Microsoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type
2017-05-12 14:49:44 +02:00
/// </summary>
/// <param name="user"></param>
/// <param name="isPersistent"></param>
/// <returns></returns>
private async Task < SignInStatus > SignInOrTwoFactor ( BackOfficeIdentityUser user , bool isPersistent )
{
var id = Convert . ToString ( user . Id ) ;
if ( await UserManager . GetTwoFactorEnabledAsync ( user . Id )
& & ( await UserManager . GetValidTwoFactorProvidersAsync ( user . Id ) ) . Count > 0 )
{
2019-11-05 13:45:42 +01:00
var identity = new ClaimsIdentity ( Constants . Security . BackOfficeTwoFactorAuthenticationType ) ;
2017-05-12 14:49:44 +02:00
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 ;
}
2015-07-01 17:07:29 +02:00
/// <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 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 (
2019-11-05 13:45:42 +01:00
Constants . Security . BackOfficeExternalAuthenticationType ,
Constants . Security . BackOfficeTwoFactorAuthenticationType ) ;
2015-07-01 17:07:29 +02:00
var nowUtc = DateTime . Now . ToUniversalTime ( ) ;
2017-05-12 14:49:44 +02:00
2015-07-01 17:07:29 +02:00
if ( rememberBrowser )
{
var rememberBrowserIdentity = AuthenticationManager . CreateTwoFactorRememberBrowserIdentity ( ConvertIdToString ( user . Id ) ) ;
AuthenticationManager . SignIn ( new AuthenticationProperties ( )
{
IsPersistent = isPersistent ,
AllowRefresh = true ,
IssuedUtc = nowUtc ,
2018-04-06 13:51:54 +10:00
ExpiresUtc = nowUtc . AddMinutes ( _globalSettings . TimeOutInMinutes )
2017-07-20 11:21:28 +02:00
} , userIdentity , rememberBrowserIdentity ) ;
2015-07-01 17:07:29 +02:00
}
else
{
AuthenticationManager . SignIn ( new AuthenticationProperties ( )
{
IsPersistent = isPersistent ,
AllowRefresh = true ,
IssuedUtc = nowUtc ,
2018-04-06 13:51:54 +10:00
ExpiresUtc = nowUtc . AddMinutes ( _globalSettings . TimeOutInMinutes )
2015-07-01 17:07:29 +02:00
} , userIdentity ) ;
}
2015-07-23 12:03:50 +02:00
2017-05-12 14:49:44 +02:00
//track the last login date
user . LastLoginDateUtc = DateTime . UtcNow ;
2017-09-19 16:46:49 +02:00
if ( user . AccessFailedCount > 0 )
//we have successfully logged in, reset the AccessFailedCount
user . AccessFailedCount = 0 ;
2017-05-12 14:49:44 +02:00
await UserManager . UpdateAsync ( user ) ;
2018-03-21 09:06:32 +01:00
//set the current request's principal to the identity just signed in!
_request . User = new ClaimsPrincipal ( userIdentity ) ;
2015-07-23 12:03:50 +02:00
_logger . WriteCore ( TraceEventType . Information , 0 ,
string . Format (
"Login attempt succeeded for username {0} from IP address {1}" ,
user . UserName ,
_request . RemoteIpAddress ) , null , null ) ;
2015-07-01 17:07:29 +02:00
}
2017-05-12 14:49:44 +02:00
/// <summary>
2019-05-06 08:22:03 +02:00
/// Get the user id that has been verified already or int.MinValue if the user has not been verified yet
2017-05-12 14:49:44 +02:00
/// </summary>
/// <returns></returns>
/// <remarks>
/// Replaces the underlying call which is not flexible and doesn't support a custom cookie
/// </remarks>
public new async Task < int > GetVerifiedUserIdAsync ( )
{
2019-11-05 13:45:42 +01:00
var result = await AuthenticationManager . AuthenticateAsync ( Constants . Security . BackOfficeTwoFactorAuthenticationType ) ;
2017-05-12 14:49:44 +02:00
if ( result ! = null & & result . Identity ! = null & & string . IsNullOrEmpty ( result . Identity . GetUserId ( ) ) = = false )
{
return ConvertIdFromString ( result . Identity . GetUserId ( ) ) ;
}
2019-05-06 08:22:03 +02:00
return int . MinValue ;
2017-05-12 14:49:44 +02:00
}
/// <summary>
/// Get the username that has been verified already or null.
/// </summary>
/// <returns></returns>
public async Task < string > GetVerifiedUserNameAsync ( )
{
2019-11-05 13:45:42 +01:00
var result = await AuthenticationManager . AuthenticateAsync ( Constants . Security . BackOfficeTwoFactorAuthenticationType ) ;
2017-05-12 14:49:44 +02:00
if ( result ! = null & & result . Identity ! = null & & string . IsNullOrEmpty ( result . Identity . GetUserName ( ) ) = = false )
{
return result . Identity . GetUserName ( ) ;
}
return null ;
}
2018-03-21 09:06:32 +01:00
/// <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
2019-05-06 08:22:03 +02:00
/// all of this code to check for int.MinValue
2018-03-21 09:06:32 +01:00
/// </remarks>
public override async Task < SignInStatus > TwoFactorSignInAsync ( string provider , string code , bool isPersistent , bool rememberBrowser )
{
var userId = await GetVerifiedUserIdAsync ( ) ;
2019-05-06 08:22:03 +02:00
if ( userId = = int . MinValue )
2018-03-21 09:06:32 +01:00
{
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 ;
}
/// <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
2019-05-06 08:22:03 +02:00
/// all of this code to check for int.MinVale instead.
2018-03-21 09:06:32 +01:00
/// </remarks>
public override async Task < bool > SendTwoFactorCodeAsync ( string provider )
{
var userId = await GetVerifiedUserIdAsync ( ) ;
2019-05-06 08:22:03 +02:00
if ( userId = = int . MinValue )
2018-03-21 09:06:32 +01:00
return false ;
var token = await UserManager . GenerateTwoFactorTokenAsync ( userId , provider ) ;
var identityResult = await UserManager . NotifyTwoFactorTokenAsync ( userId , provider , token ) ;
return identityResult . Succeeded ;
}
2015-07-01 17:07:29 +02:00
}
2017-07-20 11:21:28 +02:00
}