From 32879e09a84c8aafa07b1ddd1366a5027d338c0a Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 28 Dec 2024 16:35:21 +0100 Subject: [PATCH] Make UserRefreshToken lifetime configurable via admin (#498) --- .../Main/Pages/Settings/Server.razor | 16 ++++++++++++++++ src/AliasVault.Api/AliasVault.Api.csproj | 1 + .../Controllers/AuthController.cs | 16 +++++++--------- src/AliasVault.Api/Program.cs | 2 ++ .../Models/ServerSettingsModel.cs | 12 ++++++++++++ .../Services/ServerSettingsService.cs | 12 ++++++++++++ .../Common/ClientPlaywrightTest.cs | 6 ++++++ .../WebApplicationApiFactoryFixture.cs | 18 ++++++++++++++++++ .../Tests/Client/Shard4/AuthTests.cs | 7 ++++--- 9 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/AliasVault.Admin/Main/Pages/Settings/Server.razor b/src/AliasVault.Admin/Main/Pages/Settings/Server.razor index d9ed32d2d..c02472c9f 100644 --- a/src/AliasVault.Admin/Main/Pages/Settings/Server.razor +++ b/src/AliasVault.Admin/Main/Pages/Settings/Server.razor @@ -20,6 +20,22 @@
+
+

Authentication Settings

+
+
+ + +

Determines how long the user stays logged in after inactivity. Used when "Remember me" is not checked during login.

+
+
+ + +

Determines how long the user stays logged in after inactivity. Used when "Remember me" is checked during login.

+
+
+
+

Data Retention

diff --git a/src/AliasVault.Api/AliasVault.Api.csproj b/src/AliasVault.Api/AliasVault.Api.csproj index 05644857f..193138a50 100644 --- a/src/AliasVault.Api/AliasVault.Api.csproj +++ b/src/AliasVault.Api/AliasVault.Api.csproj @@ -34,6 +34,7 @@ + diff --git a/src/AliasVault.Api/Controllers/AuthController.cs b/src/AliasVault.Api/Controllers/AuthController.cs index 98e929c0d..c9d7c732d 100644 --- a/src/AliasVault.Api/Controllers/AuthController.cs +++ b/src/AliasVault.Api/Controllers/AuthController.cs @@ -20,6 +20,7 @@ using AliasVault.Shared.Models.WebApi; using AliasVault.Shared.Models.WebApi.Auth; using AliasVault.Shared.Models.WebApi.PasswordChange; using AliasVault.Shared.Providers.Time; +using AliasVault.Shared.Server.Services; using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -40,10 +41,11 @@ using SecureRemotePassword; /// ITimeProvider instance. This returns the time which can be mutated for testing. /// AuthLoggingService instance. This is used to log auth attempts to the database. /// Config instance. +/// ServerSettingsService instance. [Route("v{version:apiVersion}/[controller]")] [ApiController] [ApiVersion("1")] -public class AuthController(IDbContextFactory dbContextFactory, UserManager userManager, SignInManager signInManager, IConfiguration configuration, IMemoryCache cache, ITimeProvider timeProvider, AuthLoggingService authLoggingService, Config config) : ControllerBase +public class AuthController(IDbContextFactory dbContextFactory, UserManager userManager, SignInManager signInManager, IConfiguration configuration, IMemoryCache cache, ITimeProvider timeProvider, AuthLoggingService authLoggingService, Config config, ServerSettingsService settingsService) : ControllerBase { /// /// Error message for invalid username or password. @@ -688,18 +690,14 @@ public class AuthController(IDbContextFactory dbContextFac private async Task GenerateNewTokensForUser(AliasVaultUser user, bool extendedLifetime = false) { await using var context = await dbContextFactory.CreateDbContextAsync(); + var settings = await settingsService.GetAllSettingsAsync(); await Semaphore.WaitAsync(); try { - // Determine the refresh token lifetime. - // - 4 hours by default. - // - 7 days if "remember me" was checked during login. - var refreshTokenLifetime = TimeSpan.FromHours(4); - if (extendedLifetime) - { - refreshTokenLifetime = TimeSpan.FromDays(7); - } + // Use server settings for refresh token lifetime. + var refreshTokenLifetimeHours = extendedLifetime ? settings.RefreshTokenLifetimeLong : settings.RefreshTokenLifetimeShort; + var refreshTokenLifetime = TimeSpan.FromHours(refreshTokenLifetimeHours); // Return new refresh token. return await GenerateRefreshToken(user, refreshTokenLifetime); diff --git a/src/AliasVault.Api/Program.cs b/src/AliasVault.Api/Program.cs index 95b3c69e2..4a156ea63 100644 --- a/src/AliasVault.Api/Program.cs +++ b/src/AliasVault.Api/Program.cs @@ -15,6 +15,7 @@ using AliasVault.Auth; using AliasVault.Cryptography.Server; using AliasVault.Logging; using AliasVault.Shared.Providers.Time; +using AliasVault.Shared.Server.Services; using Asp.Versioning; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; @@ -38,6 +39,7 @@ builder.Services.AddAliasVaultDataProtection("AliasVault.Api"); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddHttpContextAccessor(); builder.Services.AddLogging(logging => diff --git a/src/Shared/AliasVault.Shared.Server/Models/ServerSettingsModel.cs b/src/Shared/AliasVault.Shared.Server/Models/ServerSettingsModel.cs index 7186a870c..5946bfa2f 100644 --- a/src/Shared/AliasVault.Shared.Server/Models/ServerSettingsModel.cs +++ b/src/Shared/AliasVault.Shared.Server/Models/ServerSettingsModel.cs @@ -41,4 +41,16 @@ public class ServerSettingsModel /// Gets or sets the task runner days. Defaults to all days of the week. /// public List TaskRunnerDays { get; set; } = [1, 2, 3, 4, 5, 6, 7]; + + /// + /// Gets or sets the short refresh token lifetime in hours. Defaults to 8 hours. + /// Used when "Remember me" is not checked. + /// + public int RefreshTokenLifetimeShort { get; set; } = 8; + + /// + /// Gets or sets the long refresh token lifetime in hours. Defaults to 336 hours / 14 days. + /// Used when "Remember me" is checked. + /// + public int RefreshTokenLifetimeLong { get; set; } = 336; } diff --git a/src/Shared/AliasVault.Shared.Server/Services/ServerSettingsService.cs b/src/Shared/AliasVault.Shared.Server/Services/ServerSettingsService.cs index 5ac7622f1..bb4570d9c 100644 --- a/src/Shared/AliasVault.Shared.Server/Services/ServerSettingsService.cs +++ b/src/Shared/AliasVault.Shared.Server/Services/ServerSettingsService.cs @@ -145,6 +145,16 @@ public class ServerSettingsService(IDbContextFactory dbCon } } + if (int.TryParse(settings.GetValueOrDefault("RefreshTokenLifetimeShort"), out var shortLifetime)) + { + model.RefreshTokenLifetimeShort = shortLifetime; + } + + if (int.TryParse(settings.GetValueOrDefault("RefreshTokenLifetimeLong"), out var longLifetime)) + { + model.RefreshTokenLifetimeLong = longLifetime; + } + return model; } @@ -161,5 +171,7 @@ public class ServerSettingsService(IDbContextFactory dbCon await SetSettingAsync("MaxEmailsPerUser", model.MaxEmailsPerUser.ToString()); await SetSettingAsync("MaintenanceTime", model.MaintenanceTime.ToString("HH:mm", CultureInfo.InvariantCulture)); await SetSettingAsync("TaskRunnerDays", string.Join(",", model.TaskRunnerDays)); + await SetSettingAsync("RefreshTokenLifetimeShort", model.RefreshTokenLifetimeShort.ToString()); + await SetSettingAsync("RefreshTokenLifetimeLong", model.RefreshTokenLifetimeLong.ToString()); } } diff --git a/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs b/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs index 87927c339..a099662d8 100644 --- a/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs +++ b/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs @@ -9,6 +9,7 @@ namespace AliasVault.E2ETests.Common; using AliasServerDb; using AliasVault.Shared.Providers.Time; +using AliasVault.Shared.Server.Services; using Microsoft.Playwright; /// @@ -45,6 +46,11 @@ public class ClientPlaywrightTest : PlaywrightTest /// protected AliasServerDbContext ApiDbContext => _apiFactory.GetDbContext(); + /// + /// Gets the server settings service for the WebAPI project. + /// + protected ServerSettingsService ApiServerSettings => _apiFactory.GetServerSettings(); + /// /// Gets or sets the base URL where the WebAPI project runs on including random port. /// diff --git a/src/Tests/AliasVault.E2ETests/Infrastructure/WebApplicationApiFactoryFixture.cs b/src/Tests/AliasVault.E2ETests/Infrastructure/WebApplicationApiFactoryFixture.cs index b7103104b..0a9f16398 100644 --- a/src/Tests/AliasVault.E2ETests/Infrastructure/WebApplicationApiFactoryFixture.cs +++ b/src/Tests/AliasVault.E2ETests/Infrastructure/WebApplicationApiFactoryFixture.cs @@ -10,6 +10,7 @@ namespace AliasVault.E2ETests.Infrastructure; using System.Data.Common; using AliasServerDb; using AliasVault.Shared.Providers.Time; +using AliasVault.Shared.Server.Services; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Mvc.Testing; @@ -35,6 +36,11 @@ public class WebApplicationApiFactoryFixture : WebApplicationFactor /// private IDbContextFactory _dbContextFactory = null!; + /// + /// The ServerSettingsService instance that is created for the test. + /// + private IServiceScope? _scope; + /// /// The cached DbContext instance that can be used during the test. /// @@ -74,12 +80,24 @@ public class WebApplicationApiFactoryFixture : WebApplicationFactor return _dbContext; } + /// + /// Gets the ServerSettingsService instance for mutating server settings in tests. + /// + /// ServerSettingsService instance. + public ServerSettingsService GetServerSettings() + { + _scope?.Dispose(); + _scope = Services.CreateScope(); + return _scope.ServiceProvider.GetRequiredService(); + } + /// /// Disposes the DbConnection instance. /// /// ValueTask. public override ValueTask DisposeAsync() { + _scope?.Dispose(); _dbConnection.Dispose(); GC.SuppressFinalize(this); return base.DisposeAsync(); diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard4/AuthTests.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard4/AuthTests.cs index bd88a2060..9435b459e 100644 --- a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard4/AuthTests.cs +++ b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard4/AuthTests.cs @@ -31,12 +31,13 @@ public class AuthTests : ClientPlaywrightTest var authLogEntry = await ApiDbContext.AuthLogs.FirstOrDefaultAsync(x => x.Username == TestUserUsername && x.EventType == AuthEventType.Register); Assert.That(authLogEntry, Is.Not.Null, "Auth log entry not found in database after registration."); - // Check if the refresh token is stored in the database and its expiration date is set 7 days in the future + // Check if the refresh token is stored in the database and its expiration date is set to the long lifetime // after registration. The registration page does not have a "Remember me" checkbox, but it is assumed that - // the device is trusted so the refresh token will be valid for the extended duration: 7 days. + // the device is trusted so the refresh token will be valid for the extended duration. + var settings = await ApiServerSettings.GetAllSettingsAsync(); var refreshToken = await ApiDbContext.AliasVaultUserRefreshTokens.FirstOrDefaultAsync(); Assert.That(refreshToken, Is.Not.Null, "Refresh token not found in database after login."); - Assert.That(refreshToken.ExpireDate, Is.EqualTo(refreshToken.CreatedAt.AddDays(7)), "Refresh token expiration date is not 7 days in the future while rememberMe checkbox was checked."); + Assert.That(refreshToken.ExpireDate, Is.EqualTo(refreshToken.CreatedAt.AddHours(settings.RefreshTokenLifetimeLong)), "Refresh token expiration date does not match the configured long lifetime while rememberMe was checked."); } ///