Skip to content

Commit

Permalink
Fix merge conflict (#190)
Browse files Browse the repository at this point in the history
  • Loading branch information
lanedirt committed Dec 28, 2024
2 parents d3518ec + d39ecf6 commit 1151089
Show file tree
Hide file tree
Showing 15 changed files with 123 additions and 28 deletions.
16 changes: 16 additions & 0 deletions src/AliasVault.Admin/Main/Pages/Settings/Server.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
1 change: 1 addition & 0 deletions src/AliasVault.Api/AliasVault.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
16 changes: 7 additions & 9 deletions src/AliasVault.Api/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace AliasVault.Api.Controllers;
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;
Expand All @@ -40,10 +41,11 @@ namespace AliasVault.Api.Controllers;
/// <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(IAliasServerDbContextFactory dbContextFactory, UserManager<AliasVaultUser> userManager, SignInManager<AliasVaultUser> signInManager, IConfiguration configuration, IMemoryCache cache, ITimeProvider timeProvider, AuthLoggingService authLoggingService, Config config) : ControllerBase
public class AuthController(IAliasServerDbContextFactory 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.
Expand Down Expand Up @@ -688,18 +690,14 @@ private string GenerateJwtToken(AliasVaultUser user)
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);
Expand Down
2 changes: 2 additions & 0 deletions src/AliasVault.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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;
Expand All @@ -38,6 +39,7 @@
builder.Services.AddSingleton<ITimeProvider, SystemTimeProvider>();
builder.Services.AddScoped<TimeValidationJwtBearerEvents>();
builder.Services.AddScoped<AuthLoggingService>();
builder.Services.AddScoped<ServerSettingsService>();
builder.Services.AddHttpContextAccessor();

builder.Services.AddLogging(logging =>
Expand Down
26 changes: 25 additions & 1 deletion src/AliasVault.Client/Auth/Pages/Login.razor
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
<DataAnnotationsValidator/>
<div>
<label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authenticator code</label>
<InputNumber @bind-Value="_loginModel2Fa.TwoFactorCode" id="two-factor-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm: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-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
<InputNumber @bind-Value="_loginModel2Fa.TwoFactorCode"
id="two-factor-code"
@oninput="OnTwoFactorCodeInput"
class="bg-gray-50 border border-gray-300 text-gray-900 sm: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-blue-500 dark:focus:border-blue-500"
autocomplete="off"/>
<ValidationMessage For="() => _loginModel2Fa.TwoFactorCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
</div>
<div class="flex items-start">
Expand Down Expand Up @@ -417,4 +421,24 @@ else

return [];
}

/// <summary>
/// Auto submit the 2FA code when 6 digits are entered.
/// </summary>
/// <param name="e"></param>
private async Task OnTwoFactorCodeInput(ChangeEventArgs e)
{
if (e.Value?.ToString()?.Length >= 6)
{
// Update the blazor model with the current value.
_loginModel2Fa.TwoFactorCode = int.Parse(e.Value.ToString()!);

// Submit the form.
await Handle2Fa();
}
else
{
_serverValidationErrors.Clear();
}
}
}
12 changes: 12 additions & 0 deletions src/Shared/AliasVault.Shared.Server/Models/ServerSettingsModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ public async Task<ServerSettingsModel> GetAllSettingsAsync()
}
}

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;
}

Expand All @@ -161,5 +171,7 @@ public async Task SaveSettingsAsync(ServerSettingsModel model)
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());
}
}
4 changes: 2 additions & 2 deletions src/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.3.0" />
<PackageReference Include="NUnit" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.4.0">
<PackageReference Include="NUnit.Analyzers" Version="4.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
6 changes: 6 additions & 0 deletions src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace AliasVault.E2ETests.Common;

using AliasServerDb;
using AliasVault.Shared.Providers.Time;
using AliasVault.Shared.Server.Services;
using Microsoft.Playwright;

/// <summary>
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace AliasVault.E2ETests.Infrastructure.Abstracts;

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;
Expand All @@ -33,6 +34,11 @@ public abstract class WebApplicationFactoryFixture<TEntryPoint> : WebApplication
/// </summary>
private AliasServerDbContext? _dbContext;

/// <summary>
/// The ServerSettingsService instance that is created for the test.
/// </summary>
private IServiceScope? _scope;

/// <summary>
/// The name of the temporary test database.
/// </summary>
Expand All @@ -58,6 +64,17 @@ public AliasServerDbContext GetDbContext()
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 and drops the temporary database.
/// </summary>
Expand All @@ -70,6 +87,8 @@ public override async ValueTask DisposeAsync()
_dbContext = null;
}

_scope?.Dispose();

if (!string.IsNullOrEmpty(_tempDbName))
{
// Create a connection to 'postgres' database to drop the test database
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,17 @@ public async Task TwoFactorAuthCodeLockoutTest()
for (var i = 0; i < 11; i++)
{
await Page.Locator("input[id='two-factor-code']").FillAsync("000000");
var submitButton = Page.Locator("button[type='submit']");
await submitButton.ClickAsync();

if (i == 10)
{
break;
}

// Form should auto-submit after entering the code.
await WaitForUrlAsync("user/login**", "Invalid authenticator code.");

// Clear the input field for the next attempt.
await Page.Locator("input[id='two-factor-code']").FillAsync(string.Empty);
}

// Wait for account lockout message.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,11 @@ await successMessage.WaitForAsync(new LocatorWaitForOptions
var prompt = await Page.TextContentAsync("label:has-text('Authenticator code')");
Assert.That(prompt, Does.Contain("Authenticator code"), "No 2FA code prompt displayed.");

// Fill in the 2FA code and submit the form.
// Fill in the 2FA code.
var totpField = await WaitForAndGetElement("input[id='two-factor-code']");
await totpField.FillAsync(totpCode);

var submitButton = Page.Locator("button[type='submit']");
await submitButton.ClickAsync();
// Form should auto-submit after filling in the 6-digit code.

// Check if we get redirected to the index page.
await WaitForUrlAsync("welcome**", WelcomeMessage);
Expand Down
18 changes: 11 additions & 7 deletions src/Tests/AliasVault.E2ETests/Tests/Client/Shard4/AuthTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ public async Task RegistrationAuthLog()
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>
Expand All @@ -61,10 +62,12 @@ public async Task LogoutAndLoginTest()
var authLogEntry = await ApiDbContext.AuthLogs.FirstOrDefaultAsync(x => x.Username == TestUserUsername && x.EventType == AuthEventType.Login);
Assert.That(authLogEntry, Is.Not.Null, "Auth log entry not found in database after login.");

// Check if the refresh token is stored in the database and its expiration date is set 4 hours in the future.
// Check if the refresh token is stored in the database and its expiration date is set to the short lifetime
// because the "Remember me" checkbox was not checked.
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.AddHours(4)), "Refresh token expiration date is not 4 hours in the future.");
Assert.That(refreshToken.ExpireDate, Is.EqualTo(refreshToken.CreatedAt.AddHours(settings.RefreshTokenLifetimeShort)), "Refresh token expiration date does not match the configured short lifetime while rememberMe was not checked.");
}

/// <summary>
Expand Down Expand Up @@ -127,11 +130,12 @@ public async Task LogoutAndLoginRememberMeTest()
var authLogEntry = await ApiDbContext.AuthLogs.FirstOrDefaultAsync(x => x.Username == TestUserUsername && x.EventType == AuthEventType.Login);
Assert.That(authLogEntry, Is.Not.Null, "Auth log entry not found in database after login.");

// 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
// because the "Remember me" checkbox was checked.
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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
<PackageReference Include="coverlet.collector" Version="6.0.2"/>
<PackageReference Include="MailKit" Version="4.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="NUnit" Version="4.3.0"/>
<PackageReference Include="NUnit.Analyzers" Version="4.4.0"/>
<PackageReference Include="NUnit" Version="4.3.1"/>
<PackageReference Include="NUnit.Analyzers" Version="4.5.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
Expand Down
4 changes: 2 additions & 2 deletions src/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.3.0" />
<PackageReference Include="NUnit" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.4.0">
<PackageReference Include="NUnit.Analyzers" Version="4.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down

0 comments on commit 1151089

Please sign in to comment.