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:
committed by
GitHub
parent
42bc50eccf
commit
b0ca3444f4
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>>())
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ConfigureBackOfficeSecurityStampValidatorOptions(IOptions<SecuritySettings> securitySettings)
|
||||
=> _securitySettings = securitySettings.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(BackOfficeSecurityStampValidatorOptions options)
|
||||
=> ConfigureSecurityStampOptions.ConfigureOptions(options);
|
||||
=> ConfigureSecurityStampOptions.ConfigureOptions(options, _securitySettings);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user