V13: Add config to limit concurrent logins (#14989)

* Add config to limit concurrent logins (#14967)

* Add new config options

* Change validation interval + related changes

* Fix typo

* Temp fix

* Set new setting to false for new dotnet projects

* Added logic to update security stamp on sign in + fixed wierd code calling handle signIn twice

* Cleanup

* Adding empty ctors

---------

Co-authored-by: Elitsa <elm@umbraco.dk>

* Set default setting to false + remove it from templates

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Elitsa Marinovska
2023-10-17 12:58:02 +02:00
committed by GitHub
parent 42bc50eccf
commit b0ca3444f4
7 changed files with 163 additions and 18 deletions

View File

@@ -17,6 +17,7 @@ public class SecuritySettings
internal const bool StaticHideDisabledUsersInBackOffice = false;
internal const bool StaticAllowPasswordReset = true;
internal const bool StaticAllowEditInvariantFromNonDefault = false;
internal const bool StaticAllowConcurrentLogins = false;
internal const string StaticAuthCookieName = "UMB_UCONTEXT";
internal const string StaticAllowedUserNameCharacters =
@@ -109,4 +110,10 @@ public class SecuritySettings
[Obsolete("Use ContentSettings.AllowEditFromInvariant instead")]
[DefaultValue(StaticAllowEditInvariantFromNonDefault)]
public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault;
/// <summary>
/// Gets or sets a value indicating whether to allow concurrent logins.
/// </summary>
[DefaultValue(StaticAllowConcurrentLogins)]
public bool AllowConcurrentLogins { get; set; } = StaticAllowConcurrentLogins;
}

View File

@@ -36,8 +36,9 @@ public class BackOfficeSignInManager : UmbracoSignInManager<BackOfficeIdentityUs
ILogger<SignInManager<BackOfficeIdentityUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<BackOfficeIdentityUser> confirmation,
IEventAggregator eventAggregator)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
IEventAggregator eventAggregator,
IOptions<SecuritySettings> securitySettings)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation, securitySettings)
{
_userManager = userManager;
_externalLogins = externalLogins;
@@ -45,7 +46,34 @@ public class BackOfficeSignInManager : UmbracoSignInManager<BackOfficeIdentityUs
_globalSettings = globalSettings.Value;
}
[Obsolete("Use ctor with all params")]
[Obsolete("Use non-obsolete constructor. This is scheduled for removal in V14.")]
public BackOfficeSignInManager(
BackOfficeUserManager userManager,
IHttpContextAccessor contextAccessor,
IBackOfficeExternalLoginProviders externalLogins,
IUserClaimsPrincipalFactory<BackOfficeIdentityUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
IOptions<GlobalSettings> globalSettings,
ILogger<SignInManager<BackOfficeIdentityUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<BackOfficeIdentityUser> confirmation,
IEventAggregator eventAggregator)
: this(
userManager,
contextAccessor,
externalLogins,
claimsFactory,
optionsAccessor,
globalSettings,
logger,
schemes,
confirmation,
eventAggregator,
StaticServiceProvider.Instance.GetRequiredService<IOptions<SecuritySettings>>())
{
}
[Obsolete("Use non-obsolete constructor. This is scheduled for removal in V14.")]
public BackOfficeSignInManager(
BackOfficeUserManager userManager,
IHttpContextAccessor contextAccessor,
@@ -56,7 +84,18 @@ public class BackOfficeSignInManager : UmbracoSignInManager<BackOfficeIdentityUs
ILogger<SignInManager<BackOfficeIdentityUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<BackOfficeIdentityUser> confirmation)
: this(userManager, contextAccessor, externalLogins, claimsFactory, optionsAccessor, globalSettings, logger, schemes, confirmation, StaticServiceProvider.Instance.GetRequiredService<IEventAggregator>())
: this(
userManager,
contextAccessor,
externalLogins,
claimsFactory,
optionsAccessor,
globalSettings,
logger,
schemes,
confirmation,
StaticServiceProvider.Instance.GetRequiredService<IEventAggregator>(),
StaticServiceProvider.Instance.GetRequiredService<IOptions<SecuritySettings>>())
{
}

View File

@@ -1,14 +1,27 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Web.Common.Security;
namespace Umbraco.Cms.Web.BackOffice.Security;
/// <summary>
/// Configures the back office security stamp options
/// Configures the back office security stamp options.
/// </summary>
public class
ConfigureBackOfficeSecurityStampValidatorOptions : IConfigureOptions<BackOfficeSecurityStampValidatorOptions>
public class ConfigureBackOfficeSecurityStampValidatorOptions : IConfigureOptions<BackOfficeSecurityStampValidatorOptions>
{
private readonly SecuritySettings _securitySettings;
public ConfigureBackOfficeSecurityStampValidatorOptions()
: this(StaticServiceProvider.Instance.GetRequiredService<IOptions<SecuritySettings>>())
{
public void Configure(BackOfficeSecurityStampValidatorOptions options)
=> ConfigureSecurityStampOptions.ConfigureOptions(options);
}
public ConfigureBackOfficeSecurityStampValidatorOptions(IOptions<SecuritySettings> securitySettings)
=> _securitySettings = securitySettings.Value;
/// <inheritdoc />
public void Configure(BackOfficeSecurityStampValidatorOptions options)
=> ConfigureSecurityStampOptions.ConfigureOptions(options, _securitySettings);
}

View File

@@ -1,20 +1,44 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.Security;
public class ConfigureSecurityStampOptions : IConfigureOptions<SecurityStampValidatorOptions>
{
private readonly SecuritySettings _securitySettings;
public ConfigureSecurityStampOptions()
: this(StaticServiceProvider.Instance.GetRequiredService<IOptions<SecuritySettings>>())
{
}
public ConfigureSecurityStampOptions(IOptions<SecuritySettings> securitySettings)
=> _securitySettings = securitySettings.Value;
[Obsolete("Use the overload accepting SecuritySettings instead. Scheduled for removal in v14.")]
public static void ConfigureOptions(SecurityStampValidatorOptions options)
=> ConfigureOptions(options, StaticServiceProvider.Instance.GetRequiredService<SecuritySettings>());
/// <summary>
/// Configures security stamp options and ensures any custom claims
/// set on the identity are persisted to the new identity when it's refreshed.
/// </summary>
/// <param name="options"></param>
public static void ConfigureOptions(SecurityStampValidatorOptions options)
/// <param name="options">Options for <see cref="ISecurityStampValidator"/>.</param>
/// <param name="securitySettings">The <see cref="SecuritySettings" /> options.</param>
public static void ConfigureOptions(SecurityStampValidatorOptions options, SecuritySettings securitySettings)
{
options.ValidationInterval = TimeSpan.FromMinutes(30);
// Adjust the security stamp validation interval to a shorter duration
// when concurrent logins are not allowed and the duration has the default interval value
// (currently defaults to 30 minutes), ensuring quicker re-validation.
if (securitySettings.AllowConcurrentLogins is false && options.ValidationInterval == TimeSpan.FromMinutes(30))
{
options.ValidationInterval = TimeSpan.FromSeconds(30);
}
// When refreshing the principal, ensure custom claims that
// might have been set with an external identity continue
@@ -34,6 +58,7 @@ public class ConfigureSecurityStampOptions : IConfigureOptions<SecurityStampVali
};
}
/// <inheritdoc />
public void Configure(SecurityStampValidatorOptions options)
=> ConfigureOptions(options);
=> ConfigureOptions(options, _securitySettings);
}

View File

@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
@@ -30,14 +31,40 @@ public class MemberSignInManager : UmbracoSignInManager<MemberIdentityUser>, IMe
IAuthenticationSchemeProvider schemes,
IUserConfirmation<MemberIdentityUser> confirmation,
IMemberExternalLoginProviders memberExternalLoginProviders,
IEventAggregator eventAggregator)
: base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
IEventAggregator eventAggregator,
IOptions<SecuritySettings> securitySettings)
: base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation, securitySettings)
{
_memberExternalLoginProviders = memberExternalLoginProviders;
_eventAggregator = eventAggregator;
}
[Obsolete("Use ctor with all params")]
[Obsolete("Use non-obsolete constructor. This is scheduled for removal in V14.")]
public MemberSignInManager(
UserManager<MemberIdentityUser> memberManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<MemberIdentityUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<MemberIdentityUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<MemberIdentityUser> confirmation,
IMemberExternalLoginProviders memberExternalLoginProviders,
IEventAggregator eventAggregator)
: this(
memberManager,
contextAccessor,
claimsFactory,
optionsAccessor,
logger,
schemes,
confirmation,
StaticServiceProvider.Instance.GetRequiredService<IMemberExternalLoginProviders>(),
StaticServiceProvider.Instance.GetRequiredService<IEventAggregator>(),
StaticServiceProvider.Instance.GetRequiredService<IOptions<SecuritySettings>>())
{
}
[Obsolete("Use non-obsolete constructor. This is scheduled for removal in V14.")]
public MemberSignInManager(
UserManager<MemberIdentityUser> memberManager,
IHttpContextAccessor contextAccessor,

View File

@@ -2,26 +2,32 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Security;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.Security;
/// <summary>
/// Abstract sign in manager implementation allowing modifying all defeault authentication schemes
/// Abstract sign in manager implementation allowing modifying all default authentication schemes.
/// </summary>
/// <typeparam name="TUser"></typeparam>
public abstract class UmbracoSignInManager<TUser> : SignInManager<TUser>
where TUser : UmbracoIdentityUser
{
private SecuritySettings _securitySettings;
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
protected const string UmbracoSignInMgrLoginProviderKey = "LoginProvider";
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
protected const string UmbracoSignInMgrXsrfKey = "XsrfId";
[Obsolete("Use non-obsolete constructor. This is scheduled for removal in V14.")]
public UmbracoSignInManager(
UserManager<TUser> userManager,
IHttpContextAccessor contextAccessor,
@@ -30,8 +36,30 @@ public abstract class UmbracoSignInManager<TUser> : SignInManager<TUser>
ILogger<SignInManager<TUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<TUser> confirmation)
: this(
userManager,
contextAccessor,
claimsFactory,
optionsAccessor,
logger,
schemes,
confirmation,
StaticServiceProvider.Instance.GetRequiredService<IOptions<SecuritySettings>>())
{
}
public UmbracoSignInManager(
UserManager<TUser> userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<TUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<TUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<TUser> confirmation,
IOptions<SecuritySettings> securitySettingsOptions)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{
_securitySettings = securitySettingsOptions.Value;
}
protected abstract string AuthenticationType { get; }
@@ -47,7 +75,7 @@ public abstract class UmbracoSignInManager<TUser> : SignInManager<TUser>
{
// override to handle logging/events
SignInResult result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
return await HandleSignIn(user, user.UserName, result);
return result;
}
/// <inheritdoc />
@@ -340,6 +368,11 @@ public abstract class UmbracoSignInManager<TUser> : SignInManager<TUser>
await UserManager.UpdateAsync(user);
if (_securitySettings.AllowConcurrentLogins is false)
{
await UserManager.UpdateSecurityStampAsync(user);
}
Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress);
}
else if (result.IsLockedOut)

View File

@@ -70,7 +70,8 @@ public class MemberSignInManagerTests
Mock.Of<IAuthenticationSchemeProvider>(),
Mock.Of<IUserConfirmation<MemberIdentityUser>>(),
Mock.Of<IMemberExternalLoginProviders>(),
Mock.Of<IEventAggregator>());
Mock.Of<IEventAggregator>(),
Mock.Of<IOptions<SecuritySettings>>(x => x.Value == new SecuritySettings()));
}
private static Mock<MemberManager> MockMemberManager()