mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-19 13:57:18 -04:00
Make UserRefreshToken lifetime configurable via admin (#498)
This commit is contained in:
@@ -20,6 +20,22 @@
|
||||
</PageHeader>
|
||||
|
||||
<div class="px-4">
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Authentication Settings</h3>
|
||||
<div class="grid gap-4 mb-4 sm:grid-cols-2 sm:gap-6 sm:mb-5">
|
||||
<div>
|
||||
<label for="refreshTokenShort" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Short Refresh Token Lifetime (hours)</label>
|
||||
<input type="number" @bind="Settings.RefreshTokenLifetimeShort" id="refreshTokenShort" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Determines how long the user stays logged in after inactivity. Used when "Remember me" is not checked during login.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="refreshTokenLong" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Long Refresh Token Lifetime (hours)</label>
|
||||
<input type="number" @bind="Settings.RefreshTokenLifetimeLong" id="refreshTokenLong" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Determines how long the user stays logged in after inactivity. Used when "Remember me" is checked during login.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Data Retention</h3>
|
||||
<div class="grid gap-4 mb-4 sm:grid-cols-2 sm:gap-6 sm:mb-5">
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Databases\AliasServerDb\AliasServerDb.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.Shared.Server\AliasVault.Shared.Server.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.Shared\AliasVault.Shared.csproj" />
|
||||
<ProjectReference Include="..\Utilities\AliasVault.Auth\AliasVault.Auth.csproj" />
|
||||
<ProjectReference Include="..\Utilities\AliasVault.Logging\AliasVault.Logging.csproj" />
|
||||
|
||||
@@ -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;
|
||||
/// <param name="timeProvider">ITimeProvider instance. This returns the time which can be mutated for testing.</param>
|
||||
/// <param name="authLoggingService">AuthLoggingService instance. This is used to log auth attempts to the database.</param>
|
||||
/// <param name="config">Config instance.</param>
|
||||
/// <param name="settingsService">ServerSettingsService instance.</param>
|
||||
[Route("v{version:apiVersion}/[controller]")]
|
||||
[ApiController]
|
||||
[ApiVersion("1")]
|
||||
public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager, SignInManager<AliasVaultUser> signInManager, IConfiguration configuration, IMemoryCache cache, ITimeProvider timeProvider, AuthLoggingService authLoggingService, Config config) : ControllerBase
|
||||
public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager, SignInManager<AliasVaultUser> signInManager, IConfiguration configuration, IMemoryCache cache, ITimeProvider timeProvider, AuthLoggingService authLoggingService, Config config, ServerSettingsService settingsService) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Error message for invalid username or password.
|
||||
@@ -688,18 +690,14 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
private async Task<TokenModel> 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);
|
||||
|
||||
@@ -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<ITimeProvider, SystemTimeProvider>();
|
||||
builder.Services.AddScoped<TimeValidationJwtBearerEvents>();
|
||||
builder.Services.AddScoped<AuthLoggingService>();
|
||||
builder.Services.AddScoped<ServerSettingsService>();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
builder.Services.AddLogging(logging =>
|
||||
|
||||
@@ -41,4 +41,16 @@ public class ServerSettingsModel
|
||||
/// Gets or sets the task runner days. Defaults to all days of the week.
|
||||
/// </summary>
|
||||
public List<int> TaskRunnerDays { get; set; } = [1, 2, 3, 4, 5, 6, 7];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the short refresh token lifetime in hours. Defaults to 8 hours.
|
||||
/// Used when "Remember me" is not checked.
|
||||
/// </summary>
|
||||
public int RefreshTokenLifetimeShort { get; set; } = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the long refresh token lifetime in hours. Defaults to 336 hours / 14 days.
|
||||
/// Used when "Remember me" is checked.
|
||||
/// </summary>
|
||||
public int RefreshTokenLifetimeLong { get; set; } = 336;
|
||||
}
|
||||
|
||||
@@ -145,6 +145,16 @@ public class ServerSettingsService(IDbContextFactory<AliasServerDbContext> 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<AliasServerDbContext> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace AliasVault.E2ETests.Common;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Providers.Time;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
/// <summary>
|
||||
@@ -45,6 +46,11 @@ public class ClientPlaywrightTest : PlaywrightTest
|
||||
/// </summary>
|
||||
protected AliasServerDbContext ApiDbContext => _apiFactory.GetDbContext();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server settings service for the WebAPI project.
|
||||
/// </summary>
|
||||
protected ServerSettingsService ApiServerSettings => _apiFactory.GetServerSettings();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the base URL where the WebAPI project runs on including random port.
|
||||
/// </summary>
|
||||
|
||||
@@ -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<TEntryPoint> : WebApplicationFactor
|
||||
/// </summary>
|
||||
private IDbContextFactory<AliasServerDbContext> _dbContextFactory = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The ServerSettingsService instance that is created for the test.
|
||||
/// </summary>
|
||||
private IServiceScope? _scope;
|
||||
|
||||
/// <summary>
|
||||
/// The cached DbContext instance that can be used during the test.
|
||||
/// </summary>
|
||||
@@ -74,12 +80,24 @@ public class WebApplicationApiFactoryFixture<TEntryPoint> : WebApplicationFactor
|
||||
return _dbContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ServerSettingsService instance for mutating server settings in tests.
|
||||
/// </summary>
|
||||
/// <returns>ServerSettingsService instance.</returns>
|
||||
public ServerSettingsService GetServerSettings()
|
||||
{
|
||||
_scope?.Dispose();
|
||||
_scope = Services.CreateScope();
|
||||
return _scope.ServiceProvider.GetRequiredService<ServerSettingsService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the DbConnection instance.
|
||||
/// </summary>
|
||||
/// <returns>ValueTask.</returns>
|
||||
public override ValueTask DisposeAsync()
|
||||
{
|
||||
_scope?.Dispose();
|
||||
_dbConnection.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
return base.DisposeAsync();
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user