mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Disable cookie access when a user is disabled (#6971)
This commit is contained in:
@@ -220,6 +220,7 @@ namespace BTCPayServer.Tests
|
|||||||
await s.GoToHome();
|
await s.GoToHome();
|
||||||
await s.GoToServer(ServerNavPages.Users);
|
await s.GoToServer(ServerNavPages.Users);
|
||||||
|
|
||||||
|
|
||||||
// Manage user password reset
|
// Manage user password reset
|
||||||
await s.Page.Locator("#SearchTerm").ClearAsync();
|
await s.Page.Locator("#SearchTerm").ClearAsync();
|
||||||
await s.Page.FillAsync("#SearchTerm", user.RegisterDetails.Email);
|
await s.Page.FillAsync("#SearchTerm", user.RegisterDetails.Email);
|
||||||
@@ -233,6 +234,12 @@ namespace BTCPayServer.Tests
|
|||||||
await s.ClickPagePrimary();
|
await s.ClickPagePrimary();
|
||||||
await s.FindAlertMessage(partialText: "Password successfully set");
|
await s.FindAlertMessage(partialText: "Password successfully set");
|
||||||
|
|
||||||
|
var userPage = await s.Browser.NewPageAsync();
|
||||||
|
await using (await s.SwitchPage(userPage, false))
|
||||||
|
{
|
||||||
|
await s.GoToLogin();
|
||||||
|
await s.LogIn(user.Email, user.Password);
|
||||||
|
}
|
||||||
// Manage user status (disable and enable)
|
// Manage user status (disable and enable)
|
||||||
// Disable user
|
// Disable user
|
||||||
await s.Page.Locator("#SearchTerm").ClearAsync();
|
await s.Page.Locator("#SearchTerm").ClearAsync();
|
||||||
@@ -244,6 +251,13 @@ namespace BTCPayServer.Tests
|
|||||||
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .disable-user");
|
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .disable-user");
|
||||||
await s.Page.ClickAsync("#ConfirmContinue");
|
await s.Page.ClickAsync("#ConfirmContinue");
|
||||||
await s.FindAlertMessage(partialText: "User disabled");
|
await s.FindAlertMessage(partialText: "User disabled");
|
||||||
|
|
||||||
|
await using (await s.SwitchPage(userPage, false))
|
||||||
|
{
|
||||||
|
await s.Page.ReloadAsync();
|
||||||
|
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning, partialText: "Your user account is currently disabled");
|
||||||
|
}
|
||||||
|
|
||||||
//Enable user
|
//Enable user
|
||||||
await s.Page.Locator("#SearchTerm").ClearAsync();
|
await s.Page.Locator("#SearchTerm").ClearAsync();
|
||||||
await s.Page.FillAsync("#SearchTerm", user.RegisterDetails.Email);
|
await s.Page.FillAsync("#SearchTerm", user.RegisterDetails.Email);
|
||||||
@@ -255,6 +269,14 @@ namespace BTCPayServer.Tests
|
|||||||
await s.Page.ClickAsync("#ConfirmContinue");
|
await s.Page.ClickAsync("#ConfirmContinue");
|
||||||
await s.FindAlertMessage(partialText: "User enabled");
|
await s.FindAlertMessage(partialText: "User enabled");
|
||||||
|
|
||||||
|
await using (await s.SwitchPage(userPage))
|
||||||
|
{
|
||||||
|
// Can log again
|
||||||
|
await s.LogIn(user.Email, "Password@1!");
|
||||||
|
await s.CreateNewStore();
|
||||||
|
await s.Logout();
|
||||||
|
}
|
||||||
|
|
||||||
// Manage user details (edit)
|
// Manage user details (edit)
|
||||||
await s.Page.Locator("#SearchTerm").ClearAsync();
|
await s.Page.Locator("#SearchTerm").ClearAsync();
|
||||||
await s.Page.FillAsync("#SearchTerm", user.RegisterDetails.Email);
|
await s.Page.FillAsync("#SearchTerm", user.RegisterDetails.Email);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using BTCPayServer.Logging;
|
|||||||
using BTCPayServer.PaymentRequest;
|
using BTCPayServer.PaymentRequest;
|
||||||
using BTCPayServer.Plugins;
|
using BTCPayServer.Plugins;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Storage;
|
using BTCPayServer.Storage;
|
||||||
using Fido2NetLib;
|
using Fido2NetLib;
|
||||||
@@ -87,6 +88,9 @@ namespace BTCPayServer.Hosting
|
|||||||
services.AddDataProtection()
|
services.AddDataProtection()
|
||||||
.SetApplicationName("BTCPay Server")
|
.SetApplicationName("BTCPay Server")
|
||||||
.PersistKeysToFileSystem(new DirectoryInfo(new DataDirectories().Configure(Configuration).DataDir));
|
.PersistKeysToFileSystem(new DirectoryInfo(new DataDirectories().Configure(Configuration).DataDir));
|
||||||
|
|
||||||
|
services.AddScoped<ISecurityStampValidator, BTCPayServerSecurityStampValidator>();
|
||||||
|
services.AddSingleton<BTCPayServerSecurityStampValidator.DisabledUsers>();
|
||||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||||
.AddDefaultTokenProviders()
|
.AddDefaultTokenProviders()
|
||||||
|
|||||||
66
BTCPayServer/Services/BTCPayServerSecurityStampValidator.cs
Normal file
66
BTCPayServer/Services/BTCPayServerSecurityStampValidator.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services;
|
||||||
|
|
||||||
|
public class BTCPayServerSecurityStampValidator(
|
||||||
|
IOptions<SecurityStampValidatorOptions> options,
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
ILoggerFactory logger,
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
BTCPayServerSecurityStampValidator.DisabledUsers disabledUsers)
|
||||||
|
: SecurityStampValidator<ApplicationUser>(options, signInManager, logger)
|
||||||
|
{
|
||||||
|
public class DisabledUsers
|
||||||
|
{
|
||||||
|
ConcurrentDictionary<string, DateTimeOffset> _DisabledUsers = new ConcurrentDictionary<string, DateTimeOffset>();
|
||||||
|
public bool HasAny => !_DisabledUsers.IsEmpty;
|
||||||
|
|
||||||
|
public void Add(string user)
|
||||||
|
{
|
||||||
|
_DisabledUsers.TryAdd(user, DateTimeOffset.UtcNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string user)
|
||||||
|
{
|
||||||
|
_DisabledUsers.TryRemove(user, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(string id) => _DisabledUsers.ContainsKey(id);
|
||||||
|
|
||||||
|
public void Cleanup(TimeSpan validationInterval)
|
||||||
|
{
|
||||||
|
if (_DisabledUsers.IsEmpty)
|
||||||
|
return;
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
foreach (var kv in _DisabledUsers)
|
||||||
|
{
|
||||||
|
if (now - kv.Value > validationInterval)
|
||||||
|
Remove(kv.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ValidateAsync(CookieValidatePrincipalContext context)
|
||||||
|
{
|
||||||
|
if (disabledUsers.HasAny &&
|
||||||
|
context.Principal is not null &&
|
||||||
|
userManager.GetUserId(context.Principal) is string id &&
|
||||||
|
disabledUsers.Contains(id))
|
||||||
|
{
|
||||||
|
context.Properties.IssuedUtc = null;
|
||||||
|
}
|
||||||
|
disabledUsers.Cleanup(Options.ValidationInterval);
|
||||||
|
await base.ValidateAsync(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ namespace BTCPayServer.Services
|
|||||||
private readonly FileService _fileService;
|
private readonly FileService _fileService;
|
||||||
private readonly EventAggregator _eventAggregator;
|
private readonly EventAggregator _eventAggregator;
|
||||||
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
|
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
|
||||||
|
private readonly BTCPayServerSecurityStampValidator.DisabledUsers _disabledUsers;
|
||||||
private readonly ILogger<UserService> _logger;
|
private readonly ILogger<UserService> _logger;
|
||||||
|
|
||||||
public UserService(
|
public UserService(
|
||||||
@@ -32,6 +33,7 @@ namespace BTCPayServer.Services
|
|||||||
FileService fileService,
|
FileService fileService,
|
||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
ApplicationDbContextFactory applicationDbContextFactory,
|
ApplicationDbContextFactory applicationDbContextFactory,
|
||||||
|
BTCPayServerSecurityStampValidator.DisabledUsers disabledUsers,
|
||||||
ILogger<UserService> logger)
|
ILogger<UserService> logger)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
@@ -39,6 +41,7 @@ namespace BTCPayServer.Services
|
|||||||
_fileService = fileService;
|
_fileService = fileService;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_applicationDbContextFactory = applicationDbContextFactory;
|
_applicationDbContextFactory = applicationDbContextFactory;
|
||||||
|
_disabledUsers = disabledUsers;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +97,7 @@ namespace BTCPayServer.Services
|
|||||||
{
|
{
|
||||||
return user.Approved || !user.RequiresApproval;
|
return user.Approved || !user.RequiresApproval;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryCanLogin([NotNullWhen(true)] ApplicationUser? user, [MaybeNullWhen(true)] out string error)
|
public static bool TryCanLogin([NotNullWhen(true)] ApplicationUser? user, [MaybeNullWhen(true)] out string error)
|
||||||
{
|
{
|
||||||
error = null;
|
error = null;
|
||||||
@@ -120,7 +123,7 @@ namespace BTCPayServer.Services
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SetUserApproval(string userId, bool approved, string loginLink)
|
public async Task<bool> SetUserApproval(string userId, bool approved, string loginLink)
|
||||||
{
|
{
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
@@ -130,7 +133,7 @@ namespace BTCPayServer.Services
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Approved = approved;
|
user.Approved = approved;
|
||||||
var succeeded = await userManager.UpdateAsync(user) is { Succeeded: true };
|
var succeeded = await userManager.UpdateAsync(user) is { Succeeded: true };
|
||||||
if (succeeded)
|
if (succeeded)
|
||||||
@@ -145,7 +148,7 @@ namespace BTCPayServer.Services
|
|||||||
|
|
||||||
return succeeded;
|
return succeeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool?> ToggleUser(string userId, DateTimeOffset? lockedOutDeadline)
|
public async Task<bool?> ToggleUser(string userId, DateTimeOffset? lockedOutDeadline)
|
||||||
{
|
{
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
@@ -161,6 +164,17 @@ namespace BTCPayServer.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var res = await userManager.SetLockoutEndDateAsync(user, lockedOutDeadline);
|
var res = await userManager.SetLockoutEndDateAsync(user, lockedOutDeadline);
|
||||||
|
// Without this, the user won't be logged out automatically when his authentication ticket expires
|
||||||
|
if (lockedOutDeadline is not null)
|
||||||
|
{
|
||||||
|
await userManager.UpdateSecurityStampAsync(user);
|
||||||
|
_disabledUsers.Add(userId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_disabledUsers.Remove(userId);
|
||||||
|
}
|
||||||
|
|
||||||
if (res.Succeeded)
|
if (res.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("User {Email} is now {Status}", user.Email, (lockedOutDeadline is null ? "unlocked" : "locked"));
|
_logger.LogInformation("User {Email} is now {Status}", user.Email, (lockedOutDeadline is null ? "unlocked" : "locked"));
|
||||||
|
|||||||
Reference in New Issue
Block a user