Files
aliasvault/apps/server/AliasVault.Client/Auth/Pages/Unlock.razor
2025-04-30 19:03:18 +02:00

307 lines
13 KiB
Plaintext

@page "/unlock"
@page "/unlock/{SkipWebAuthn:bool}"
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
@inject ILogger<Unlock> Logger
@layout Auth.Layout.MainLayout
@using System.Text.Json
@using AliasVault.Client.Auth.Components
@using AliasVault.Client.Utilities
@using AliasVault.Shared.Models.WebApi.Auth
@using AliasVault.Cryptography.Client
<FullScreenLoadingIndicator @ref="_loadingIndicator" />
<ServerValidationErrors @ref="_serverValidationErrors" />
@if (IsLoading) {
<BoldLoadingIndicator />
}
else if (IsWebAuthnLoading) {
<BoldLoadingIndicator />
<p class="mt-6 text-center font-normal text-gray-500 dark:text-gray-400">
Logging in with WebAuthn...
</p>
}
else
{
<div class="flex space-x-4">
<img class="w-8 h-8 rounded-full" src="/img/avatar.webp" alt="User image">
<h2 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white">@Username</h2>
</div>
@if (ShowWebAuthnButton)
{
<div class="mb-6">
<p class="text-base font-normal text-gray-500 dark:text-gray-400 mb-4">
Quickly unlock your vault using your fingerprint, face ID, or security key. Or login with your password as a fallback.
</p>
<div class="flex space-x-4">
<button type="button" @onclick="UnlockWithWebAuthn" class="flex-grow inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
<svg class="w-5 h-5 mr-2 -ml-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"></path></svg>
Unlock with WebAuthn
</button>
<button type="button" @onclick="ShowPasswordLogin" class="inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center text-gray-900 rounded-lg border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 dark:text-white dark:border-gray-700 dark:hover:bg-gray-700 dark:focus:ring-gray-800">
Unlock with password
</button>
</div>
</div>
}
else
{
<p class="text-base font-normal text-gray-500 dark:text-gray-400 mb-4">
Enter your master password to unlock your vault.
</p>
<EditForm Model="_unlockModel" OnValidSubmit="UnlockSubmit" class="mt-8 space-y-6">
<DataAnnotationsValidator/>
<div>
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
<InputTextField id="password" @bind-Value="_unlockModel.Password" type="password" placeholder="••••••••"/>
<ValidationMessage For="() => _unlockModel.Password"/>
</div>
<button type="submit" class="w-full inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
<svg class="w-5 h-5 mr-2 -ml-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a5 5 0 00-5 5v2a2 2 0 00-2 2v5a2 2 0 002 2h10a2 2 0 002-2v-5a2 2 0 00-2-2H7V7a3 3 0 015.905-.75 1 1 0 001.937-.5A5.002 5.002 0 0010 2z"></path></svg>
Unlock
</button>
</EditForm>
}
<div class="text-sm font-medium text-gray-500 dark:text-gray-400 mt-6">
Switch accounts? <a href="/user/logout" class="text-primary-700 hover:underline dark:text-primary-500">Log out</a>
</div>
}
@code {
/// <summary>
/// Skip automatic WebAuthn unlock during page load if set to true.
/// </summary>
[Parameter]
public bool SkipWebAuthn { get; set; }
private string? Username { get; set; }
private bool IsLoading { get; set; } = true;
private bool IsWebAuthnLoading { get; set; }
private bool ShowWebAuthnButton { get; set; }
private readonly UnlockModel _unlockModel = new();
private FullScreenLoadingIndicator _loadingIndicator = new();
private ServerValidationErrors _serverValidationErrors = new();
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Trigger status API call to check if the user is still authenticated.
// If user is not authenticated a redirect to the login page will be triggered automatically.
await StatusCheck();
// Always check if WebAuthn is enabled
ShowWebAuthnButton = await AuthService.IsWebAuthnEnabledAsync();
// Try to unlock with WebAuthn if enabled and not explicitly skipped
if (ShowWebAuthnButton && !SkipWebAuthn)
{
await UnlockWithWebAuthn();
}
IsLoading = false;
StateHasChanged();
}
}
/// <summary>
/// Execute the unlock form submission.
/// </summary>
private async Task UnlockSubmit()
{
_loadingIndicator.Show("Unlocking vault...");
_serverValidationErrors.Clear();
try
{
await StatusCheck();
// Send request to server with email to get user salt.
var result = await Http.PostAsJsonAsync("v1/Auth/login", new LoginInitiateRequest(Username!));
var responseContent = await result.Content.ReadAsStringAsync();
if (!result.IsSuccessStatusCode)
{
var errors = ApiResponseUtility.ParseErrorResponse(responseContent);
foreach (var error in errors)
{
_serverValidationErrors.AddError(error);
}
return;
}
var loginResponse = JsonSerializer.Deserialize<LoginInitiateResponse>(responseContent);
if (loginResponse == null)
{
_serverValidationErrors.AddError("An error occurred while processing the unlock request.");
return;
}
// 3. Client derives shared session key.
byte[] passwordHash = await Encryption.DeriveKeyFromPasswordAsync(_unlockModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings);
// Check if the password is correct locally by decrypting the test string.
var validPassword = await AuthService.ValidateEncryptionKeyAsync(passwordHash);
if (!validPassword)
{
_serverValidationErrors.AddError("The password is incorrect. Please try entering your password again, or log out and log in again.");
return;
}
// Store the encryption key in memory.
await AuthService.StoreEncryptionKeyAsync(passwordHash);
// Redirect to the page the user was trying to access before if set.
var localStorageReturnUrl = await LocalStorage.GetItemAsync<string>(ReturnUrlKey);
if (!string.IsNullOrEmpty(localStorageReturnUrl))
{
await LocalStorage.RemoveItemAsync(ReturnUrlKey);
NavigationManager.NavigateTo(localStorageReturnUrl);
}
else
{
NavigationManager.NavigateTo("/");
}
}
#if DEBUG
catch (Exception ex)
{
// If in debug mode show the actual exception.
_serverValidationErrors.AddError(ex.ToString());
}
#else
catch
{
// If in release mode show a generic error.
_serverValidationErrors.AddError("An error occurred while processing the login request. Try again (later).");
}
#endif
finally
{
_loadingIndicator.Hide();
}
}
/// <summary>
/// Make a request to the server to check if access token is still valid.
/// If not, then this call will automatically result in redirect to the login page.
/// </summary>
private async Task StatusCheck()
{
await AuthStateProvider.GetAuthenticationStateAsync();
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity?.IsAuthenticated == false) {
// Not authenticated (anymore), redirect to login page.
GlobalNotificationService.AddErrorMessage("Your session has timed out. Please log in again.");
NavigationManager.NavigateTo("/user/login");
return;
}
// Check if encryption key test string is available. If not
// user should log in again.
if (!await AuthService.HasEncryptionKeyTestStringAsync())
{
// Clear all tokens and redirect to login page.
await AuthService.RemoveTokensAsync();
GlobalNotificationService.ClearMessages();
GlobalNotificationService.AddErrorMessage("Your session has timed out. Please log in again.");
NavigationManager.NavigateTo("/user/login");
return;
}
// Check if username is set.
// If not, redirect to login page.
Username = authState.User.Identity?.Name;
if (Username is null)
{
// Clear all tokens and redirect to login page.
await AuthService.RemoveTokensAsync();
await AuthStateProvider.GetAuthenticationStateAsync();
GlobalNotificationService.ClearMessages();
GlobalNotificationService.AddErrorMessage("Your session has timed out. Please log in again.");
NavigationManager.NavigateTo("/user/login");
return;
}
// Make a request to the server to check if the user is still authenticated.
// If user has no valid authentication an automatic redirect to login page will take place.
try
{
await Http.GetAsync("v1/Auth/status");
}
catch (Exception ex)
{
_serverValidationErrors.AddError("Connection with the AliasVault servers failed. Please try again (later).");
Logger.LogError(ex, "An error occurred while checking the user status.");
StateHasChanged();
}
}
/// <summary>
/// Unlock the vault with WebAuthn if enabled.
/// </summary>
/// <returns>Task.</returns>
private async Task UnlockWithWebAuthn()
{
await AuthStateProvider.GetAuthenticationStateAsync();
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
// Check if webauthn is enabled. If so, try to unlock the vault with it.
if (await AuthService.IsWebAuthnEnabledAsync())
{
IsLoading = false;
IsWebAuthnLoading = true;
StateHasChanged();
try
{
var decryptionKey = await AuthService.GetDecryptedWebAuthnEncryptionKeyAsync(authState.User.Identity!.Name!);
if (decryptionKey.Length == 0)
{
Logger.LogWarning("No decryption key found for user {Username}. Falling back to password unlock.", authState.User.Identity!.Name!);
return;
}
if (await AuthService.ValidateEncryptionKeyAsync(decryptionKey))
{
await AuthService.StoreEncryptionKeyAsync(decryptionKey);
NavigationManager.NavigateTo("/");
}
else
{
Logger.LogWarning("The decrypted encryption key does not match the persisted encryption key. Falling back to password unlock.");
}
}
catch (NotSupportedException)
{
GlobalNotificationService.AddErrorMessage("Your current browser does not support the WebAuthn PRF extension. Please login with your password instead.", true);
}
catch (Exception ex)
{
Logger.LogError(ex, "An error occurred while trying to unlock the vault with WebAuthn.");
}
finally
{
IsWebAuthnLoading = false;
StateHasChanged();
}
}
}
/// <summary>
/// Show the password login form.
/// </summary>
private void ShowPasswordLogin()
{
ShowWebAuthnButton = false;
StateHasChanged();
}
}