mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-22 16:43:24 -04:00
Improve webauthn unlock flow (#312)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user