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>
This commit is contained in:
Bjarke Berg
2023-10-17 10:23:52 +02:00
committed by GitHub
parent ea4f07bd08
commit 0a8b12793e
8 changed files with 166 additions and 18 deletions

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)