2020-05-13 18:30:57 +01:00
using System.Data ;
2021-09-20 11:22:51 +02:00
using System.Globalization ;
2020-05-13 18:30:57 +01:00
using Microsoft.AspNetCore.Identity ;
2022-01-19 09:21:50 +01:00
using Microsoft.Extensions.DependencyInjection ;
2020-08-21 14:52:47 +01:00
using Microsoft.Extensions.Options ;
2021-03-05 15:36:27 +01:00
using Umbraco.Cms.Core.Cache ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Configuration.Models ;
2022-11-29 11:22:57 +00:00
using Umbraco.Cms.Core.DependencyInjection ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Mapping ;
using Umbraco.Cms.Core.Models ;
using Umbraco.Cms.Core.Models.Membership ;
2021-02-15 11:41:12 +01:00
using Umbraco.Cms.Core.Scoping ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Services ;
2021-02-09 11:26:22 +01:00
using Umbraco.Extensions ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
namespace Umbraco.Cms.Core.Security ;
/// <summary>
/// The user store for back office users
/// </summary>
public class BackOfficeUserStore : UmbracoUserStore < BackOfficeIdentityUser , IdentityRole < string > > ,
IUserSessionStore < BackOfficeIdentityUser >
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
private readonly AppCaches _appCaches ;
private readonly IEntityService _entityService ;
private readonly IExternalLoginWithKeyService _externalLoginService ;
private readonly GlobalSettings _globalSettings ;
private readonly IUmbracoMapper _mapper ;
private readonly ICoreScopeProvider _scopeProvider ;
private readonly ITwoFactorLoginService _twoFactorLoginService ;
private readonly IUserService _userService ;
2020-12-04 01:38:36 +11:00
2020-12-04 12:44:27 +11:00
/// <summary>
2022-06-02 08:18:31 +02:00
/// Initializes a new instance of the <see cref="BackOfficeUserStore" /> class.
2020-12-04 12:44:27 +11:00
/// </summary>
2022-06-02 08:18:31 +02:00
[ActivatorUtilitiesConstructor]
public BackOfficeUserStore (
ICoreScopeProvider scopeProvider ,
IUserService userService ,
IEntityService entityService ,
IExternalLoginWithKeyService externalLoginService ,
IOptionsSnapshot < GlobalSettings > globalSettings ,
IUmbracoMapper mapper ,
BackOfficeErrorDescriber describer ,
AppCaches appCaches ,
ITwoFactorLoginService twoFactorLoginService )
: base ( describer )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
_scopeProvider = scopeProvider ;
_userService = userService ? ? throw new ArgumentNullException ( nameof ( userService ) ) ;
_entityService = entityService ;
_externalLoginService = externalLoginService ? ? throw new ArgumentNullException ( nameof ( externalLoginService ) ) ;
_globalSettings = globalSettings . Value ;
_mapper = mapper ;
_appCaches = appCaches ;
_twoFactorLoginService = twoFactorLoginService ;
_userService = userService ;
_externalLoginService = externalLoginService ;
}
2022-04-19 08:33:03 +02:00
2022-06-02 08:18:31 +02:00
[Obsolete("Use non obsolete ctor")]
public BackOfficeUserStore (
ICoreScopeProvider scopeProvider ,
IUserService userService ,
IEntityService entityService ,
IExternalLoginWithKeyService externalLoginService ,
IOptions < GlobalSettings > globalSettings ,
IUmbracoMapper mapper ,
BackOfficeErrorDescriber describer ,
AppCaches appCaches )
: this (
scopeProvider ,
userService ,
entityService ,
externalLoginService ,
StaticServiceProvider . Instance . GetRequiredService < IOptionsSnapshot < GlobalSettings > > ( ) ,
mapper ,
describer ,
appCaches ,
StaticServiceProvider . Instance . GetRequiredService < ITwoFactorLoginService > ( ) )
{
}
/// <inheritdoc />
public Task < bool > ValidateSessionIdAsync ( string? userId , string? sessionId )
{
if ( Guid . TryParse ( sessionId , out Guid guidSessionId ) )
{
return Task . FromResult ( _userService . ValidateLoginSession ( UserIdToInt ( userId ) , guidSessionId ) ) ;
2022-04-19 08:33:03 +02:00
}
2022-06-02 08:18:31 +02:00
return Task . FromResult ( false ) ;
}
/// <inheritdoc />
public override async Task < bool > GetTwoFactorEnabledAsync (
BackOfficeIdentityUser user ,
CancellationToken cancellationToken = default )
{
if ( ! int . TryParse ( user . Id , NumberStyles . Integer , CultureInfo . InvariantCulture , out var intUserId ) )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
return await base . GetTwoFactorEnabledAsync ( user , cancellationToken ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return await _twoFactorLoginService . IsTwoFactorEnabledAsync ( user . Key ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override Task < IdentityResult > CreateAsync (
BackOfficeIdentityUser user ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2020-05-13 18:30:57 +01:00
2022-09-19 16:37:24 +02:00
if ( user . Email is null | | user . UserName is null )
{
throw new InvalidOperationException ( "Email and UserName is required." ) ;
}
// the password must be 'something' it could be empty if authenticating
// with an external provider so we'll just generate one and prefix it, the
// prefix will help us determine if the password hasn't actually been specified yet.
// this will hash the guid with a salt so should be nicely random
var aspHasher = new PasswordHasher < BackOfficeIdentityUser > ( ) ;
var emptyPasswordValue = Constants . Security . EmptyPasswordPrefix +
2022-06-02 08:18:31 +02:00
aspHasher . HashPassword ( user , Guid . NewGuid ( ) . ToString ( "N" ) ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
var userEntity = new User ( _globalSettings , user . Name , user . Email , user . UserName , emptyPasswordValue )
{
Language = user . Culture ? ? _globalSettings . DefaultUILanguage ,
StartContentIds = user . StartContentIds ? ? new int [ ] { } ,
StartMediaIds = user . StartMediaIds ? ? new int [ ] { } ,
IsLockedOut = user . IsLockedOut ,
} ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = user . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Logins ) ) ;
var isTokensPropertyDirty = user . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . LoginTokens ) ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
UpdateMemberProperties ( userEntity , user ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
_userService . Save ( userEntity ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( ! userEntity . HasIdentity )
{
throw new DataException ( "Could not create the user, check logs for details" ) ;
}
2020-10-23 10:10:02 +11:00
2022-06-02 08:18:31 +02:00
// re-assign id
user . Id = UserIdToString ( userEntity . Id ) ;
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
if ( isLoginsPropertyDirty )
{
_externalLoginService . Save (
userEntity . Key ,
user . Logins . Select ( x = > new ExternalLogin (
x . LoginProvider ,
x . ProviderKey ,
x . UserData ) ) ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
if ( isTokensPropertyDirty )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
_externalLoginService . Save (
userEntity . Key ,
user . LoginTokens . Select ( x = > new ExternalLoginToken (
x . LoginProvider ,
x . Name ,
x . Value ) ) ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . FromResult ( IdentityResult . Success ) ;
}
/// <inheritdoc />
public override Task < IdentityResult > UpdateAsync (
BackOfficeIdentityUser user ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2020-10-23 10:10:02 +11:00
2022-06-02 08:18:31 +02:00
if ( ! int . TryParse ( user . Id , NumberStyles . Integer , CultureInfo . InvariantCulture , out var asInt ) )
{
throw new InvalidOperationException ( "The user id must be an integer to work with the Umbraco" ) ;
}
2020-10-23 10:10:02 +11:00
2022-06-02 08:18:31 +02:00
using ( ICoreScope scope = _scopeProvider . CreateCoreScope ( ) )
{
IUser ? found = _userService . GetUserById ( asInt ) ;
if ( found ! = null )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = user . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Logins ) ) ;
var isTokensPropertyDirty = user . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . LoginTokens ) ) ;
if ( UpdateMemberProperties ( found , user ) )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
_userService . Save ( found ) ;
2020-05-13 18:30:57 +01:00
}
2020-12-01 18:14:37 +11:00
2022-06-02 08:18:31 +02:00
if ( isLoginsPropertyDirty )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
_externalLoginService . Save (
found . Key ,
user . Logins . Select ( x = > new ExternalLogin (
x . LoginProvider ,
x . ProviderKey ,
x . UserData ) ) ) ;
2020-05-13 18:30:57 +01:00
}
2020-12-01 18:14:37 +11:00
2022-06-02 08:18:31 +02:00
if ( isTokensPropertyDirty )
{
_externalLoginService . Save (
found . Key ,
user . LoginTokens . Select ( x = > new ExternalLoginToken (
x . LoginProvider ,
x . Name ,
x . Value ) ) ) ;
}
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
scope . Complete ( ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
return Task . FromResult ( IdentityResult . Success ) ;
}
/// <inheritdoc />
public override Task < IdentityResult > DeleteAsync (
BackOfficeIdentityUser user ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
var userId = UserIdToInt ( user . Id ) ;
IUser ? found = _userService . GetUserById ( userId ) ;
if ( found ! = null )
{
_userService . Delete ( found ) ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
_externalLoginService . DeleteUserLogins ( userId . ToGuid ( ) ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . FromResult ( IdentityResult . Success ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
public override Task < BackOfficeIdentityUser ? > FindByNameAsync ( string userName , CancellationToken cancellationToken = default )
2022-06-02 08:18:31 +02:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
IUser ? user = _userService . GetByUsername ( userName ) ;
if ( user = = null )
2020-05-13 18:30:57 +01:00
{
2022-09-19 16:37:24 +02:00
return Task . FromResult < BackOfficeIdentityUser ? > ( null ) ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
BackOfficeIdentityUser ? result = AssignLoginsCallback ( _mapper . Map < BackOfficeIdentityUser > ( user ) ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . FromResult ( result ) ! ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
protected override Task < BackOfficeIdentityUser ? > FindUserAsync ( string userId , CancellationToken cancellationToken )
2022-06-02 08:18:31 +02:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
IUser ? user = _userService . GetUserById ( UserIdToInt ( userId ) ) ;
if ( user = = null )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
return Task . FromResult ( ( BackOfficeIdentityUser ? ) null ) ! ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . FromResult ( AssignLoginsCallback ( _mapper . Map < BackOfficeIdentityUser > ( user ) ) ) ! ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
public override Task < BackOfficeIdentityUser ? > FindByEmailAsync (
2022-06-02 08:18:31 +02:00
string email ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
IUser ? user = _userService . GetByEmail ( email ) ;
BackOfficeIdentityUser ? result = user = = null
? null
: _mapper . Map < BackOfficeIdentityUser > ( user ) ;
2022-09-19 16:37:24 +02:00
return Task . FromResult ( AssignLoginsCallback ( result ) ) ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
public override async Task SetPasswordHashAsync ( BackOfficeIdentityUser user , string? passwordHash , CancellationToken cancellationToken = default )
2022-06-02 08:18:31 +02:00
{
await base . SetPasswordHashAsync ( user , passwordHash , cancellationToken ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
// Clear this so that it's reset at the repository level
user . PasswordConfig = null ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override Task AddLoginAsync ( BackOfficeIdentityUser user , UserLoginInfo login , CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
Security stamp implementation for members (#10140)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Updates user manager to correctly validate password hashing and injects the IBackOfficeUserPasswordChecker
* Merges PR
* Fixes up build and notes
* Implements security stamp and email confirmed for members, cleans up a bunch of repo/service level member groups stuff, shares user store code between members and users and fixes the user identity object so we arent' tracking both groups and roles.
* Security stamp for members is now working
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* merge changes
* oops
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* oops didn't mean to comit this
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 17:13:40 +10:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( user ) ) ;
Security stamp implementation for members (#10140)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Updates user manager to correctly validate password hashing and injects the IBackOfficeUserPasswordChecker
* Merges PR
* Fixes up build and notes
* Implements security stamp and email confirmed for members, cleans up a bunch of repo/service level member groups stuff, shares user store code between members and users and fixes the user identity object so we arent' tracking both groups and roles.
* Security stamp for members is now working
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* merge changes
* oops
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* oops didn't mean to comit this
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 17:13:40 +10:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( login = = null )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( login ) ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
ICollection < IIdentityUserLogin > logins = user . Logins ;
var instance = new IdentityUserLogin ( login . LoginProvider , login . ProviderKey , user . Id ) ;
IdentityUserLogin userLogin = instance ;
logins . Add ( userLogin ) ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
return Task . CompletedTask ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override Task RemoveLoginAsync ( BackOfficeIdentityUser user , string loginProvider , string providerKey , CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
IIdentityUserLogin ? userLogin =
user . Logins . SingleOrDefault ( l = > l . LoginProvider = = loginProvider & & l . ProviderKey = = providerKey ) ;
if ( userLogin ! = null )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
user . Logins . Remove ( userLogin ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . CompletedTask ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override Task < IList < UserLoginInfo > > GetLoginsAsync (
BackOfficeIdentityUser user ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
return Task . FromResult ( ( IList < UserLoginInfo > ) user . Logins
. Select ( l = > new UserLoginInfo ( l . LoginProvider , l . ProviderKey , l . LoginProvider ) ) . ToList ( ) ) ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
/// <summary>
/// Lists all users of a given role.
/// </summary>
/// <remarks>
/// Identity Role names are equal to Umbraco UserGroup alias.
/// </remarks>
public override Task < IList < BackOfficeIdentityUser > > GetUsersInRoleAsync (
string normalizedRoleName ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( normalizedRoleName = = null )
{
throw new ArgumentNullException ( nameof ( normalizedRoleName ) ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
IUserGroup ? userGroup = _userService . GetUserGroupByAlias ( normalizedRoleName ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
IEnumerable < IUser > users = _userService . GetAllInGroup ( userGroup ? . Id ) ;
IList < BackOfficeIdentityUser > backOfficeIdentityUsers =
users . Select ( x = > _mapper . Map < BackOfficeIdentityUser > ( x ) ) . Where ( x = > x ! = null ) . ToList ( ) ! ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . FromResult ( backOfficeIdentityUsers ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <summary>
/// Overridden to support Umbraco's own data storage requirements
/// </summary>
/// <remarks>
/// The base class's implementation of this calls into FindTokenAsync and AddUserTokenAsync, both methods will only
/// work with ORMs that are change
/// tracking ORMs like EFCore.
/// </remarks>
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
public override Task SetTokenAsync ( BackOfficeIdentityUser user , string loginProvider , string name , string? value , CancellationToken cancellationToken )
2022-06-02 08:18:31 +02:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( user = = null )
2020-12-04 12:44:27 +11:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
IIdentityUserToken ? token = user . LoginTokens . FirstOrDefault ( x = >
x . LoginProvider . InvariantEquals ( loginProvider ) & & x . Name . InvariantEquals ( name ) ) ;
2020-12-04 12:44:27 +11:00
2022-08-16 15:47:01 +02:00
// We have to remove token and then re-add to ensure that LoginTokens are dirty, which is required for them to save
// This is because we're using an observable collection, which only cares about added/removed items.
if ( token is not null )
2022-06-02 08:18:31 +02:00
{
2022-08-17 14:36:09 +02:00
// The token hasn't changed, so there's no reason for us to re-add it.
if ( token . Value = = value )
2020-12-04 12:44:27 +11:00
{
2022-08-17 14:36:09 +02:00
return Task . CompletedTask ;
2020-12-04 12:44:27 +11:00
}
2020-05-13 18:30:57 +01:00
2022-08-16 15:47:01 +02:00
user . LoginTokens . Remove ( token ) ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-08-16 15:47:01 +02:00
user . LoginTokens . Add ( new IdentityUserToken ( loginProvider , name , value , user . Id ) ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . CompletedTask ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
protected override async Task < IdentityUserLogin < string > ? > FindUserLoginAsync ( string userId , string loginProvider , string providerKey , CancellationToken cancellationToken )
2022-06-02 08:18:31 +02:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2020-05-13 18:30:57 +01:00
2022-09-19 16:37:24 +02:00
BackOfficeIdentityUser ? user = await FindUserAsync ( userId , cancellationToken ) ;
if ( user ? . Id is null )
2020-05-13 18:30:57 +01:00
{
2022-09-19 16:37:24 +02:00
return null ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
IList < UserLoginInfo > logins = await GetLoginsAsync ( user , cancellationToken ) ;
UserLoginInfo ? found =
logins . FirstOrDefault ( x = > x . ProviderKey = = providerKey & & x . LoginProvider = = loginProvider ) ;
if ( found = = null )
2020-05-13 18:30:57 +01:00
{
2022-09-19 16:37:24 +02:00
return null ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
return new IdentityUserLogin < string >
2020-12-04 12:44:27 +11:00
{
2022-06-02 08:18:31 +02:00
LoginProvider = found . LoginProvider ,
ProviderKey = found . ProviderKey ,
ProviderDisplayName = found . ProviderDisplayName , // TODO: We don't store this value so it will be null
UserId = user . Id ,
} ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
protected override Task < IdentityUserLogin < string > ? > FindUserLoginAsync ( string loginProvider , string providerKey , CancellationToken cancellationToken )
2022-06-02 08:18:31 +02:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
var logins = _externalLoginService . Find ( loginProvider , providerKey ) . ToList ( ) ;
if ( logins . Count = = 0 )
{
2022-09-19 16:37:24 +02:00
return Task . FromResult < IdentityUserLogin < string > ? > ( null ) ;
2020-12-04 12:44:27 +11:00
}
2022-06-02 08:18:31 +02:00
IIdentityUserLogin found = logins [ 0 ] ;
2022-09-19 16:37:24 +02:00
return Task . FromResult < IdentityUserLogin < string > ? > ( new IdentityUserLogin < string >
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
LoginProvider = found . LoginProvider ,
ProviderKey = found . ProviderKey ,
ProviderDisplayName = null , // TODO: We don't store this value so it will be null
UserId = found . UserId ,
} ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
protected override Task < IdentityRole < string > ? > FindRoleAsync (
2022-06-02 08:18:31 +02:00
string normalizedRoleName ,
CancellationToken cancellationToken )
{
IUserGroup ? group = _userService . GetUserGroupByAlias ( normalizedRoleName ) ;
2022-09-19 16:37:24 +02:00
if ( group ? . Name is null )
2022-06-02 08:18:31 +02:00
{
2022-09-19 16:37:24 +02:00
return Task . FromResult < IdentityRole < string > ? > ( null ) ;
2020-05-13 18:30:57 +01:00
}
2022-09-19 16:37:24 +02:00
return Task . FromResult < IdentityRole < string > ? > ( new IdentityRole < string > ( group . Name )
{
Id = group . Alias ,
} ) ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
protected override async Task < IdentityUserRole < string > ? > FindUserRoleAsync ( string userId , string roleId , CancellationToken cancellationToken )
2022-06-02 08:18:31 +02:00
{
2022-09-19 16:37:24 +02:00
BackOfficeIdentityUser ? user = await FindUserAsync ( userId , cancellationToken ) ;
2022-06-02 08:18:31 +02:00
if ( user = = null )
{
return null ! ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
IdentityUserRole < string > ? found = user . Roles . FirstOrDefault ( x = > x . RoleId . InvariantEquals ( roleId ) ) ;
2022-09-19 16:37:24 +02:00
return found ;
2022-06-02 08:18:31 +02:00
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
private BackOfficeIdentityUser ? AssignLoginsCallback ( BackOfficeIdentityUser ? user )
{
if ( user ! = null )
{
var userId = UserIdToInt ( user . Id ) . ToGuid ( ) ;
user . SetLoginsCallback (
new Lazy < IEnumerable < IIdentityUserLogin > ? > ( ( ) = > _externalLoginService . GetExternalLogins ( userId ) ) ) ;
user . SetTokensCallback (
new Lazy < IEnumerable < IIdentityUserToken > ? > ( ( ) = > _externalLoginService . GetExternalLoginTokens ( userId ) ) ) ;
}
2021-07-26 11:51:54 -06:00
2022-06-02 08:18:31 +02:00
return user ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
private bool UpdateMemberProperties ( IUser user , BackOfficeIdentityUser identityUser )
{
var anythingChanged = false ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
// don't assign anything if nothing has changed as this will trigger the track changes of the model
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . LastLoginDateUtc ) )
| | ( user . LastLoginDate ! = default & & identityUser . LastLoginDateUtc . HasValue = = false )
| | ( identityUser . LastLoginDateUtc . HasValue & &
user . LastLoginDate ? . ToUniversalTime ( ) ! = identityUser . LastLoginDateUtc . Value ) )
{
anythingChanged = true ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
// if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
DateTime ? dt = identityUser . LastLoginDateUtc ? . ToLocalTime ( ) ;
user . LastLoginDate = dt ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . InviteDateUtc ) )
| | user . InvitedDate ? . ToUniversalTime ( ) ! = identityUser . InviteDateUtc )
{
anythingChanged = true ;
user . InvitedDate = identityUser . InviteDateUtc ? . ToLocalTime ( ) ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . LastPasswordChangeDateUtc ) )
| | ( user . LastPasswordChangeDate . HasValue & & user . LastPasswordChangeDate . Value ! = default & &
identityUser . LastPasswordChangeDateUtc . HasValue = = false )
| | ( identityUser . LastPasswordChangeDateUtc . HasValue & & user . LastPasswordChangeDate ? . ToUniversalTime ( ) ! =
identityUser . LastPasswordChangeDateUtc . Value ) )
{
anythingChanged = true ;
user . LastPasswordChangeDate = identityUser . LastPasswordChangeDateUtc ? . ToLocalTime ( ) ? ? DateTime . Now ;
}
2021-09-21 08:56:10 -06:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . EmailConfirmed ) )
| | ( user . EmailConfirmedDate . HasValue & & user . EmailConfirmedDate . Value ! = default & &
identityUser . EmailConfirmed = = false )
| | ( ( user . EmailConfirmedDate . HasValue = = false | | user . EmailConfirmedDate . Value = = default ) & &
identityUser . EmailConfirmed ) )
{
anythingChanged = true ;
user . EmailConfirmedDate = identityUser . EmailConfirmed ? DateTime . Now : null ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Name ) )
& & user . Name ! = identityUser . Name & & identityUser . Name . IsNullOrWhiteSpace ( ) = = false )
{
anythingChanged = true ;
user . Name = identityUser . Name ? ? string . Empty ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Email ) )
& & user . Email ! = identityUser . Email & & identityUser . Email . IsNullOrWhiteSpace ( ) = = false )
{
anythingChanged = true ;
2022-09-19 16:37:24 +02:00
user . Email = identityUser . Email ! ;
2022-06-02 08:18:31 +02:00
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . AccessFailedCount ) )
& & user . FailedPasswordAttempts ! = identityUser . AccessFailedCount )
{
anythingChanged = true ;
user . FailedPasswordAttempts = identityUser . AccessFailedCount ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( user . IsApproved ! = identityUser . IsApproved )
{
anythingChanged = true ;
user . IsApproved = identityUser . IsApproved ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( user . IsLockedOut ! = identityUser . IsLockedOut )
{
anythingChanged = true ;
user . IsLockedOut = identityUser . IsLockedOut ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( user . IsLockedOut )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
// need to set the last lockout date
user . LastLockoutDate = DateTime . Now ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . UserName ) )
& & user . Username ! = identityUser . UserName & & identityUser . UserName . IsNullOrWhiteSpace ( ) = = false )
{
anythingChanged = true ;
2022-09-19 16:37:24 +02:00
user . Username = identityUser . UserName ! ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . PasswordHash ) )
& & user . RawPasswordValue ! = identityUser . PasswordHash & &
identityUser . PasswordHash . IsNullOrWhiteSpace ( ) = = false )
{
anythingChanged = true ;
user . RawPasswordValue = identityUser . PasswordHash ;
user . PasswordConfiguration = identityUser . PasswordConfig ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Culture ) )
& & user . Language ! = identityUser . Culture & & identityUser . Culture . IsNullOrWhiteSpace ( ) = = false )
{
anythingChanged = true ;
user . Language = identityUser . Culture ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . StartMediaIds ) )
& & user . StartMediaIds . UnsortedSequenceEqual ( identityUser . StartMediaIds ) = = false )
{
anythingChanged = true ;
user . StartMediaIds = identityUser . StartMediaIds ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . StartContentIds ) )
& & user . StartContentIds . UnsortedSequenceEqual ( identityUser . StartContentIds ) = = false )
{
anythingChanged = true ;
user . StartContentIds = identityUser . StartContentIds ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( user . SecurityStamp ! = identityUser . SecurityStamp )
{
anythingChanged = true ;
user . SecurityStamp = identityUser . SecurityStamp ;
}
Security stamp implementation for members (#10140)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Updates user manager to correctly validate password hashing and injects the IBackOfficeUserPasswordChecker
* Merges PR
* Fixes up build and notes
* Implements security stamp and email confirmed for members, cleans up a bunch of repo/service level member groups stuff, shares user store code between members and users and fixes the user identity object so we arent' tracking both groups and roles.
* Security stamp for members is now working
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* merge changes
* oops
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* oops didn't mean to comit this
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 17:13:40 +10:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Roles ) ) )
{
var identityUserRoles = identityUser . Roles . Select ( x = > x . RoleId ) . Where ( x = > x is not null ) . ToArray ( ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
anythingChanged = true ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
// clear out the current groups (need to ToArray since we are modifying the iterator)
user . ClearGroups ( ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
// go lookup all these groups
IReadOnlyUserGroup [ ] groups = _userService . GetUserGroupsByAlias ( identityUserRoles )
. Select ( x = > x . ToReadOnlyGroup ( ) ) . ToArray ( ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
// use all of the ones assigned and add them
foreach ( IReadOnlyUserGroup group in groups )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
user . AddGroup ( group ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
// re-assign
identityUser . SetGroups ( groups ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
// we should re-set the calculated start nodes
identityUser . CalculatedMediaStartNodeIds = user . CalculateMediaStartNodeIds ( _entityService , _appCaches ) ;
identityUser . CalculatedContentStartNodeIds = user . CalculateContentStartNodeIds ( _entityService , _appCaches ) ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
// reset all changes
identityUser . ResetDirtyProperties ( false ) ;
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
return anythingChanged ;
}
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
/// <summary>
2022-09-19 16:37:24 +02:00
/// Overridden to support Umbraco's own data storage requirements
2022-06-02 08:18:31 +02:00
/// </summary>
/// <remarks>
2022-09-19 16:37:24 +02:00
/// The base class's implementation of this calls into FindTokenAsync, RemoveUserTokenAsync and AddUserTokenAsync, both methods will only work with ORMs that are change
/// tracking ORMs like EFCore.
2022-06-02 08:18:31 +02:00
/// </remarks>
/// <inheritdoc />
public override Task RemoveTokenAsync ( BackOfficeIdentityUser user , string loginProvider , string name , CancellationToken cancellationToken )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
2021-03-11 19:35:43 +11:00
}
2022-06-02 08:18:31 +02:00
IIdentityUserToken ? token = user . LoginTokens . FirstOrDefault ( x = >
x . LoginProvider . InvariantEquals ( loginProvider ) & & x . Name . InvariantEquals ( name ) ) ;
if ( token ! = null )
2021-03-11 19:35:43 +11:00
{
2022-06-02 08:18:31 +02:00
user . LoginTokens . Remove ( token ) ;
}
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
return Task . CompletedTask ;
}
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
/// <summary>
/// Overridden to support Umbraco's own data storage requirements
/// </summary>
/// <remarks>
/// The base class's implementation of this calls into FindTokenAsync, RemoveUserTokenAsync and AddUserTokenAsync, both
/// methods will only work with ORMs that are change
/// tracking ORMs like EFCore.
/// </remarks>
/// <inheritdoc />
public override Task < string? > GetTokenAsync ( BackOfficeIdentityUser user , string loginProvider , string name , CancellationToken cancellationToken )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
if ( user = = null )
2021-03-11 19:35:43 +11:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
IIdentityUserToken ? token = user . LoginTokens . FirstOrDefault ( x = >
x . LoginProvider . InvariantEquals ( loginProvider ) & & x . Name . InvariantEquals ( name ) ) ;
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
return Task . FromResult ( token ? . Value ) ;
2020-05-13 18:30:57 +01:00
}
}