Improve webauthn unlock flow (#312)

This commit is contained in:
Leendert de Borst
2024-10-22 22:32:04 +02:00
parent faa578b5b5
commit 7a374d9730
4 changed files with 92 additions and 72 deletions

View File

@@ -1,4 +1,5 @@
@page "/unlock"
@page "/unlock/{SkipWebAuthn:bool}"
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
@inject ILogger<Unlock> Logger
@layout Auth.Layout.MainLayout
@@ -25,36 +26,63 @@ else
<h2 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white">@Username</h2>
</div>
<p class="text-base font-normal text-gray-500 dark:text-gray-400">
Enter your master password in order to unlock your database.
</p>
<FullScreenLoadingIndicator @ref="_loadingIndicator"/>
<ServerValidationErrors @ref="_serverValidationErrors"/>
<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"/>
@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">
Login 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 database.
</p>
<button type="submit" class="inline-flex items-center justify-center w-full 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>
<FullScreenLoadingIndicator @ref="_loadingIndicator"/>
<ServerValidationErrors @ref="_serverValidationErrors"/>
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
Switch accounts? <a href="/user/logout" class="text-primary-700 hover:underline dark:text-primary-500">Log out</a>
</div>
</EditForm>
<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; } = true;
private bool IsWebAuthnLoading { get; set; }
private bool ShowWebAuthnButton { get; set; }
private readonly UnlockModel _unlockModel = new();
private FullScreenLoadingIndicator _loadingIndicator = new();
private ServerValidationErrors _serverValidationErrors = new();
@@ -71,11 +99,16 @@ else
StatusCheck()
);
// Try to unlock with WebAuthn if enabled.
await UnlockWithWebAuthn();
// 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;
IsWebAuthnLoading = false;
StateHasChanged();
}
}
@@ -243,6 +276,20 @@ else
{
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();
}
}

View File

@@ -22,7 +22,7 @@
// Initialize empty database which removes unencrypted data.
DbService.InitializeEmptyDatabase();
// Redirect to unlock page.
NavigationManager.NavigateTo("/unlock");
// Redirect to unlock page with SkipWebAuthn parameter set to true.
NavigationManager.NavigateTo("/unlock/true");
}
}

View File

@@ -1565,21 +1565,11 @@ video {
padding-right: 1.75rem;
}
.px-8 {
padding-left: 2rem;
padding-right: 2rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.py-12 {
padding-top: 3rem;
padding-bottom: 3rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
@@ -1600,8 +1590,8 @@ video {
padding-bottom: 1rem;
}
.pb-0 {
padding-bottom: 0px;
.pb-28 {
padding-bottom: 7rem;
}
.pb-4 {
@@ -1644,18 +1634,6 @@ video {
padding-top: 2rem;
}
.pb-10 {
padding-bottom: 2.5rem;
}
.pb-20 {
padding-bottom: 5rem;
}
.pb-28 {
padding-bottom: 7rem;
}
.text-left {
text-align: left;
}
@@ -2203,6 +2181,11 @@ video {
--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity));
}
.focus\:ring-gray-100:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity));
}
.focus\:ring-offset-2:focus {
--tw-ring-offset-width: 2px;
}
@@ -2596,12 +2579,22 @@ video {
width: auto;
}
.sm\:flex-row {
flex-direction: row;
}
.sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0px * var(--tw-space-y-reverse));
}
.sm\:rounded-lg {
border-radius: 0.5rem;
}
@@ -2650,10 +2643,6 @@ video {
}
@media (min-width: 768px) {
.md\:relative {
position: relative;
}
.md\:col-span-1 {
grid-column: span 1 / span 1;
}
@@ -2742,14 +2731,6 @@ video {
margin-top: 0px;
}
.lg\:mb-4 {
margin-bottom: 1rem;
}
.lg\:mb-2 {
margin-bottom: 0.5rem;
}
.lg\:block {
display: block;
}
@@ -2833,18 +2814,10 @@ video {
padding-bottom: 4rem;
}
.lg\:pb-0 {
padding-bottom: 0px;
}
.lg\:pb-4 {
padding-bottom: 1rem;
}
.lg\:pt-4 {
padding-top: 1rem;
}
.lg\:pt-6 {
padding-top: 1.5rem;
}

View File

@@ -37,7 +37,7 @@
<body class="bg-gray-50 dark:bg-gray-900">
<div id="loading-screen">
<div class="fixed inset-0 flex items-center justify-center px-6 pt-8 pb-8">
<div class="w-full max-w-xl p-6 space-y-4 sm:p-8 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="w-full max-w-md p-6 space-y-4 sm:p-8 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="text-center">
<div class="inner">
<svg class="mx-auto animate-spin h-12 w-12 text-primary-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">