mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-12 11:48:39 -04:00
Add recovery code support to client 2FA login flow (#70)
This commit is contained in:
@@ -9,8 +9,8 @@
|
||||
<ServerValidationErrors @ref="ServerValidationErrors" />
|
||||
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
You have requested to log in with a recovery code. This login will not be remembered until you provide
|
||||
an authenticator app code at log in or disable 2FA and log in again.
|
||||
You have requested to log in with a recovery code. A recovery code is a one-time code that can be used to log in to your account.
|
||||
Note that if you don't manually disable 2FA after login, you will be asked for an authenticator code again at the next login.
|
||||
</p>
|
||||
<div class="w-full max-w-md">
|
||||
<EditForm Model="Input" FormName="login-with-recovery-code" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-6">
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
<LayoutPageTitle>Configure authenticator app</LayoutPageTitle>
|
||||
|
||||
@if (recoveryCodes is not null)
|
||||
@if (RecoveryCodes is not null)
|
||||
{
|
||||
<ShowRecoveryCodes RecoveryCodes="recoveryCodes.ToArray()"/>
|
||||
<ShowRecoveryCodes RecoveryCodes="RecoveryCodes.ToArray()"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -34,8 +34,8 @@ else
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p class="text-gray-700 dark:text-gray-300">Scan the QR Code or enter this key <kbd class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500">@sharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
|
||||
<div id="authenticator-uri" data-url="@authenticatorUri" class="mt-4"></div>
|
||||
<p class="text-gray-700 dark:text-gray-300">Scan the QR Code or enter this key <kbd class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500">@SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
|
||||
<div id="authenticator-uri" data-url="@AuthenticatorUri" class="mt-4"></div>
|
||||
</li>
|
||||
<li>
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
@@ -67,9 +67,9 @@ else
|
||||
@code {
|
||||
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||
|
||||
private string? sharedKey;
|
||||
private string? authenticatorUri;
|
||||
private IEnumerable<string>? recoveryCodes;
|
||||
private string? SharedKey { get; set; }
|
||||
private string? AuthenticatorUri { get; set; }
|
||||
private IEnumerable<string>? RecoveryCodes { get; set; }
|
||||
|
||||
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
|
||||
|
||||
@@ -77,9 +77,7 @@ else
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
await LoadSharedKeyAndQrCodeUriAsync(UserService.User());
|
||||
|
||||
await JsInvokeService.RetryInvokeAsync("generateQrCode", TimeSpan.Zero, 5, "authenticator-uri");
|
||||
}
|
||||
|
||||
@@ -88,10 +86,10 @@ else
|
||||
// Strip spaces and hyphens
|
||||
var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
var is2faTokenValid = await UserManager.VerifyTwoFactorTokenAsync(
|
||||
var is2FaTokenValid = await UserManager.VerifyTwoFactorTokenAsync(
|
||||
UserService.User(), UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
|
||||
if (!is2faTokenValid)
|
||||
if (!is2FaTokenValid)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Error: Verification code is invalid.");
|
||||
return;
|
||||
@@ -105,7 +103,7 @@ else
|
||||
|
||||
if (await UserManager.CountRecoveryCodesAsync(UserService.User()) == 0)
|
||||
{
|
||||
recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(UserService.User(), 10);
|
||||
RecoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(UserService.User(), 10);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -124,10 +122,10 @@ else
|
||||
unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
sharedKey = FormatKey(unformattedKey!);
|
||||
SharedKey = FormatKey(unformattedKey!);
|
||||
|
||||
var username = await UserManager.GetUserNameAsync(user);
|
||||
authenticatorUri = GenerateQrCodeUri(username!, unformattedKey!);
|
||||
AuthenticatorUri = GenerateQrCodeUri(username!, unformattedKey!);
|
||||
}
|
||||
|
||||
private string FormatKey(string unformattedKey)
|
||||
|
||||
@@ -49,6 +49,9 @@ else
|
||||
<button @onclick="EnableTwoFactor" class="px-3 py-2 text-xs font-medium text-center text-white bg-red-700 rounded-lg hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">
|
||||
Enable 2FA
|
||||
</button>
|
||||
<button @onclick="ResetTwoFactor" class="px-3 py-2 text-xs font-medium text-center text-white bg-red-700 rounded-lg hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">
|
||||
Remove 2FA keys
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -188,6 +191,27 @@ else
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method enables two-factor authentication for the user based on existing keys. If no keys are present
|
||||
/// then 2FA will not work. The user will need to manually set up a new authenticator device.
|
||||
/// </summary>
|
||||
private async Task EnableTwoFactor()
|
||||
{
|
||||
User = await DbContext.AliasVaultUsers.FindAsync(Id);
|
||||
|
||||
if (User != null)
|
||||
{
|
||||
User.TwoFactorEnabled = true;
|
||||
await DbContext.SaveChangesAsync();
|
||||
await LoadEntryAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method disables two-factor authentication for the user. This will NOT remove the authenticator keys.
|
||||
/// This means the admin can re-enable 2FA for the user without the user having to set up a new authenticator
|
||||
/// keys.
|
||||
/// </summary>
|
||||
private async Task DisableTwoFactor()
|
||||
{
|
||||
User = await DbContext.AliasVaultUsers.FindAsync(Id);
|
||||
@@ -200,13 +224,22 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnableTwoFactor()
|
||||
/// <summary>
|
||||
/// This method resets the two-factor authentication for the user which will remove all authenticator keys. The
|
||||
/// next time the user enables two-factor authentication new keys will be generated. When keys are removed it
|
||||
/// also means 2FA cannot be re-enabled until the user manually sets up a new authenticator device.
|
||||
/// </summary>
|
||||
private async Task ResetTwoFactor()
|
||||
{
|
||||
User = await DbContext.AliasVaultUsers.FindAsync(Id);
|
||||
|
||||
if (User != null)
|
||||
{
|
||||
User.TwoFactorEnabled = true;
|
||||
// Remove all authenticator keys and recovery codes.
|
||||
await DbContext.UserTokens
|
||||
.Where(x => x.UserId == User.Id && (x.Name == "AuthenticatorKey" || x.Name == "RecoveryCodes"))
|
||||
.ForEachAsync(x => DbContext.UserTokens.Remove(x));
|
||||
|
||||
await DbContext.SaveChangesAsync();
|
||||
await LoadEntryAsync();
|
||||
}
|
||||
|
||||
@@ -1085,6 +1085,11 @@ video {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.border-amber-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(253 230 138 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
||||
@@ -1110,24 +1115,19 @@ video {
|
||||
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-yellow-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(234 179 8 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-blue-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(147 197 253 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-sky-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(186 230 253 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-amber-200 {
|
||||
.border-yellow-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(253 230 138 / var(--tw-border-opacity));
|
||||
border-color: rgb(234 179 8 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 251 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-500 {
|
||||
@@ -1235,6 +1235,16 @@ video {
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-red-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(240 249 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
@@ -1250,26 +1260,6 @@ video {
|
||||
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-red-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 246 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(240 249 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 251 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -1453,6 +1443,11 @@ video {
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
.text-amber-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(180 83 9 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(37 99 235 / var(--tw-text-opacity));
|
||||
@@ -1518,6 +1513,11 @@ video {
|
||||
color: rgb(153 27 27 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(3 105 161 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
@@ -1533,21 +1533,6 @@ video {
|
||||
color: rgb(133 77 14 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 64 175 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(3 105 161 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-amber-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(180 83 9 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@@ -1755,6 +1740,11 @@ video {
|
||||
border-color: rgb(75 85 99 / var(--tw-divide-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-amber-800:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(146 64 14 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(107 114 128 / var(--tw-border-opacity));
|
||||
@@ -1770,21 +1760,11 @@ video {
|
||||
border-color: rgb(55 65 81 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-blue-800:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(30 64 175 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-sky-800:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(7 89 133 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-amber-800:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(146 64 14 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-600:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
|
||||
@@ -1820,30 +1800,35 @@ video {
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-yellow-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-600:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-slate-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 41 59 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-yellow-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-opacity-80:is(.dark *) {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
|
||||
.dark\:text-amber-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(252 211 77 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-blue-400:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(96 165 250 / var(--tw-text-opacity));
|
||||
@@ -1904,6 +1889,11 @@ video {
|
||||
color: rgb(248 113 113 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-sky-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(125 211 252 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-white:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
@@ -1919,16 +1909,6 @@ video {
|
||||
color: rgb(254 240 138 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-sky-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(125 211 252 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-amber-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(252 211 77 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
|
||||
--tw-placeholder-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
|
||||
|
||||
@@ -50,6 +50,11 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
/// </summary>
|
||||
private static readonly string[] Invalid2FaCode = ["Invalid authenticator code."];
|
||||
|
||||
/// <summary>
|
||||
/// Error message for invalid 2-factor authentication recovery code.
|
||||
/// </summary>
|
||||
private static readonly string[] InvalidRecoveryCode = ["Invalid recovery code."];
|
||||
|
||||
/// <summary>
|
||||
/// Login endpoint used to process login attempt using credentials.
|
||||
/// </summary>
|
||||
@@ -168,6 +173,58 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate login including two factor authentication recovery code check.
|
||||
/// </summary>
|
||||
/// <param name="model">ValidateLoginRequestRecoveryCode model.</param>
|
||||
/// <returns>Task.</returns>
|
||||
[HttpPost("validate-recovery-code")]
|
||||
public async Task<IActionResult> ValidateRecoveryCode([FromBody] ValidateLoginRequestRecoveryCode model)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(model.Username);
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest(ServerValidationErrorResponse.Create(InvalidUsernameOrPasswordError, 400));
|
||||
}
|
||||
|
||||
if (!cache.TryGetValue(model.Username, out var serverSecretEphemeral) || serverSecretEphemeral is not string)
|
||||
{
|
||||
return BadRequest(ServerValidationErrorResponse.Create(InvalidUsernameOrPasswordError, 400));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var serverSession = Cryptography.Srp.DeriveSessionServer(
|
||||
serverSecretEphemeral.ToString() ?? string.Empty,
|
||||
model.ClientPublicEphemeral,
|
||||
user.Salt,
|
||||
model.Username,
|
||||
user.Verifier,
|
||||
model.ClientSessionProof);
|
||||
|
||||
// If above does not throw an exception., then the client's proof is valid. Next check 2-factor code.
|
||||
|
||||
// Sanitize recovery code.
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty).ToUpper();
|
||||
|
||||
// Attempt to redeem the recovery code
|
||||
var result = await userManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return BadRequest(ServerValidationErrorResponse.Create(InvalidRecoveryCode, 400));
|
||||
}
|
||||
|
||||
// Generate and return the JWT token.
|
||||
var tokenModel = await GenerateNewTokensForUser(user);
|
||||
return Ok(new ValidateLoginResponse(false, serverSession.Proof, tokenModel));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest(ServerValidationErrorResponse.Create(InvalidUsernameOrPasswordError, 400));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh endpoint used to refresh an expired access token using a valid refresh token.
|
||||
/// </summary>
|
||||
@@ -258,7 +315,7 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
}
|
||||
|
||||
var encodedKey = urlEncoder.Encode(authenticatorKey!);
|
||||
var qrCodeUrl = $"otpauth://totp/{urlEncoder.Encode("AliasVault WASM")}:{urlEncoder.Encode(user.UserName!)}?secret={encodedKey}&issuer={urlEncoder.Encode("AliasVault WASM")}";
|
||||
var qrCodeUrl = $"otpauth://totp/{urlEncoder.Encode("AliasVault")}:{urlEncoder.Encode(user.UserName!)}?secret={encodedKey}&issuer={urlEncoder.Encode("AliasVault")}";
|
||||
|
||||
return Ok(new { Secret = authenticatorKey, QrCodeUrl = qrCodeUrl });
|
||||
}
|
||||
@@ -278,12 +335,12 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
|
||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
// Disable 2FA and remove any existing authenticator key(s).
|
||||
// Disable 2FA and remove any existing authenticator key(s) and recovery codes.
|
||||
await userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
context.UserTokens.RemoveRange(
|
||||
context.UserTokens.Where(
|
||||
x => x.UserId == user.Id &&
|
||||
x.Name == "AuthenticatorKey").ToList());
|
||||
(x.Name == "AuthenticatorKey" || x.Name == "RecoveryCodes")).ToList());
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
return Ok();
|
||||
@@ -308,12 +365,14 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
if (isValid)
|
||||
{
|
||||
await userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
return Ok();
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest("Invalid code.");
|
||||
|
||||
// Generate new recovery codes.
|
||||
var recoveryCodes = await userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
|
||||
return Ok(new { RecoveryCodes = recoveryCodes });
|
||||
}
|
||||
|
||||
return BadRequest("Invalid code.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
namespace AliasVault.Api.Controllers;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Models;
|
||||
using AliasVault.Shared.Models.WebApi.Favicon;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
@layout Auth.Layout.MainLayout
|
||||
@attribute [AllowAnonymous]
|
||||
@using System.Text.Json
|
||||
@using AliasVault.Shared.Models
|
||||
@using AliasVault.Client.Auth.Components
|
||||
@using AliasVault.Shared.Models.WebApi.Auth
|
||||
@using AliasVault.Client.Auth.Components
|
||||
@using Cryptography
|
||||
@using SecureRemotePassword
|
||||
|
||||
@@ -40,7 +39,36 @@
|
||||
</div>
|
||||
<p class="mt-6 text-sm text-gray-700 dark:text-gray-300">
|
||||
Don't have access to your authenticator device? You can
|
||||
<a href="user/loginWithRecoveryCode" class="text-primary-600 hover:underline dark:text-primary-500">log in with a recovery code</a>.
|
||||
<button @onclick="LoginWithRecoveryCode" class="text-primary-600 hover:underline dark:text-primary-500">log in with a recovery code</button>.
|
||||
</p>
|
||||
}
|
||||
else if (ShowLoginWithRecoveryCodeStep)
|
||||
{
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Recovery code verification
|
||||
</h2>
|
||||
|
||||
<ServerValidationErrors @ref="ServerValidationErrors" />
|
||||
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
You have requested to log in with a recovery code. A recovery code is a one-time code that can be used to log in to your account.
|
||||
Note that if you don't manually disable 2FA after login, you will be asked for an authenticator code again at the next login.
|
||||
</p>
|
||||
<div class="w-full">
|
||||
<EditForm Model="LoginModelRecoveryCode" FormName="login-with-recovery-code" OnValidSubmit="HandleRecoveryCode" method="post" class="space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
|
||||
<div>
|
||||
<label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Recovery Code</label>
|
||||
<InputText @bind-Value="LoginModelRecoveryCode.RecoveryCode" 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"/>
|
||||
<ValidationMessage For="() => LoginModelRecoveryCode.RecoveryCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
|
||||
</div>
|
||||
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Log in</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
<p class="mt-6 text-sm text-gray-700 dark:text-gray-300">
|
||||
Regained access to your authenticator device? You can
|
||||
<button @onclick="LoginWithAuthenticator" class="text-primary-600 hover:underline dark:text-primary-500">log in with authenticator code</button> instead.
|
||||
</p>
|
||||
}
|
||||
else
|
||||
@@ -85,9 +113,11 @@ else
|
||||
@code {
|
||||
private readonly LoginModel LoginModel = new();
|
||||
private readonly LoginModel2Fa LoginModel2Fa = new();
|
||||
private readonly LoginModelRecoveryCode LoginModelRecoveryCode = new();
|
||||
private FullScreenLoadingIndicator LoadingIndicator = new();
|
||||
private ServerValidationErrors ServerValidationErrors = new();
|
||||
private bool ShowTwoFactorAuthStep = false;
|
||||
private bool ShowTwoFactorAuthStep;
|
||||
private bool ShowLoginWithRecoveryCodeStep;
|
||||
|
||||
private SrpEphemeral ClientEphemeral = new();
|
||||
private SrpSession ClientSession = new();
|
||||
@@ -104,6 +134,20 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void LoginWithAuthenticator()
|
||||
{
|
||||
ShowLoginWithRecoveryCodeStep = false;
|
||||
ShowTwoFactorAuthStep = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void LoginWithRecoveryCode()
|
||||
{
|
||||
ShowLoginWithRecoveryCodeStep = true;
|
||||
ShowTwoFactorAuthStep = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task HandleLogin()
|
||||
{
|
||||
LoadingIndicator.Show();
|
||||
@@ -136,6 +180,61 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRecoveryCode()
|
||||
{
|
||||
LoadingIndicator.Show();
|
||||
ServerValidationErrors.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
// Sanitize username
|
||||
var username = LoginModel.Username.ToLowerInvariant().Trim();
|
||||
|
||||
// Validate 2-factor auth code auth and login
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/validate-recovery-code", new ValidateLoginRequestRecoveryCode(username, ClientEphemeral.Public, ClientSession.Proof, LoginModelRecoveryCode.RecoveryCode));
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
foreach (var error in ParseResponse(responseContent))
|
||||
{
|
||||
ServerValidationErrors.AddError(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
|
||||
if (validateLoginResponse == null)
|
||||
{
|
||||
ServerValidationErrors.AddError("An error occurred while processing the login request.");
|
||||
return;
|
||||
}
|
||||
|
||||
var errors = await ProcessLoginVerify(validateLoginResponse);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
ServerValidationErrors.AddError(error);
|
||||
}
|
||||
}
|
||||
#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();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Handle2Fa()
|
||||
{
|
||||
LoadingIndicator.Show();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthService AuthService
|
||||
@using System.Text.Json
|
||||
@using AliasVault.Shared.Models
|
||||
@using AliasVault.Shared.Models.WebApi.Auth
|
||||
@using AliasVault.Client.Auth.Components
|
||||
@using AliasVault.Client.Auth.Pages.Base
|
||||
@using Cryptography
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</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">
|
||||
<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">Email Settings</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<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">Recovery codes</h3>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
The recovery codes below are used to access your account in case you lose access to your authenticator device.
|
||||
Make a photo or write them down and store them in a secure location. Do not share them with anyone.
|
||||
</div>
|
||||
<div class="bg-primary-100 border-l-4 border-primary-500 text-primary-700 p-4 mb-4" role="alert">
|
||||
<p class="font-semibold">
|
||||
Store these recovery codes in a safe place.
|
||||
</p>
|
||||
<p>
|
||||
If you lose your device and don't have the recovery codes you will lose access to your account. These
|
||||
codes are only shown once!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1">
|
||||
@foreach (var recoveryCode in RecoveryCodes)
|
||||
{
|
||||
<div>
|
||||
<code class="block p-2">@recoveryCode</code>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The recovery codes to show.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string[] RecoveryCodes { get; set; } = [];
|
||||
}
|
||||
@@ -24,7 +24,7 @@ else
|
||||
<div class="mb-3 text-sm text-gray-600 dark:text-gray-400">Two factor authentication is currently enabled. Disable it in order to be able to access your vault with your password only.</div>
|
||||
<button @onclick="DisableTwoFactor"
|
||||
class="bg-red-500 text-white py-2 px-4 rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition duration-150 ease-in-out">
|
||||
Disable Two-Factor Authentication
|
||||
Confirm Disable Two-Factor Authentication
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
@page "/settings/security/enable-2fa"
|
||||
@using AliasVault.Client.Main.Pages.Settings.Security.Components
|
||||
@inherits MainBase
|
||||
@inject HttpClient Http
|
||||
|
||||
<LayoutPageTitle>Enable two-factor authentication</LayoutPageTitle>
|
||||
|
||||
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
|
||||
<div class="mb-4 col-span-full xl:mb-2">
|
||||
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Enable two-factor authentication</h1>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Enable two-factor authentication to increase the security of your vaults.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else if (RecoveryCodes is not null)
|
||||
{
|
||||
<ShowRecoveryCodes RecoveryCodes="RecoveryCodes.ToArray()"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
|
||||
<div class="mb-4 col-span-full xl:mb-2">
|
||||
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Enable two-factor authentication</h1>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Enable two-factor authentication to increase the security of your vaults.</p>
|
||||
</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">
|
||||
<div class="space-y-6">
|
||||
<div id="authenticator-uri" data-url="@QrCodeUrl" class="flex justify-center">
|
||||
@@ -49,6 +54,7 @@ else
|
||||
private string Secret { get; set; } = string.Empty;
|
||||
private VerificationModel VerifyModel = new();
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private List<string>? RecoveryCodes { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -76,7 +82,11 @@ else
|
||||
if (result != null)
|
||||
{
|
||||
QrCodeUrl = result.QrCodeUrl;
|
||||
Secret = result.Secret;
|
||||
|
||||
// Make secret more readable by adding spaces every 4 characters
|
||||
Secret = string.Join(" ", Enumerable.Range(0, result.Secret.Length / 4)
|
||||
.Select(i => result.Secret.Substring(i * 4, 4))).ToLower();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
JsInteropService.GenerateQrCode("authenticator-uri");
|
||||
@@ -93,15 +103,23 @@ else
|
||||
var response = await Http.PostAsJsonAsync("api/v1/Auth/verify-2fa", VerifyModel.Code);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
GlobalNotificationService.AddSuccessMessage("Two-factor authentication is now successfully enabled. On your " +
|
||||
"next login you will need to enter your 2FA code.");
|
||||
NavigationManager.NavigateTo("/settings/security");
|
||||
}
|
||||
else
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Failed to enable two-factor authentication.", true);
|
||||
StateHasChanged();
|
||||
var result = await response.Content.ReadFromJsonAsync<TotpVerifyResult>();
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
GlobalNotificationService.AddSuccessMessage("Two-factor authentication is now successfully enabled. On your " +
|
||||
"next login you will need to enter your 2FA code.", true);
|
||||
|
||||
// Show recovery codes.
|
||||
RecoveryCodes = result.RecoveryCodes;
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalNotificationService.AddErrorMessage("Failed to enable two-factor authentication.", true);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private class TotpSetupResult
|
||||
@@ -110,6 +128,11 @@ else
|
||||
public string QrCodeUrl { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private class TotpVerifyResult
|
||||
{
|
||||
public List<string> RecoveryCodes { get; set; } = new();
|
||||
}
|
||||
|
||||
private class VerificationModel
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace AliasVault.Client.Services.Auth;
|
||||
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using AliasVault.Shared.Models;
|
||||
using AliasVault.Shared.Models.WebApi.Auth;
|
||||
using Blazored.LocalStorage;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ using AliasClientDb;
|
||||
using AliasGenerators.Identity.Implementations;
|
||||
using AliasGenerators.Identity.Models;
|
||||
using AliasGenerators.Password.Implementations;
|
||||
using AliasVault.Shared.Models;
|
||||
using AliasVault.Shared.Models.WebApi.Favicon;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Identity = AliasGenerators.Identity.Models.Identity;
|
||||
|
||||
|
||||
@@ -1206,6 +1206,10 @@ video {
|
||||
border-left-width: 0px;
|
||||
}
|
||||
|
||||
.border-l-4 {
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
@@ -1245,6 +1249,11 @@ video {
|
||||
border-color: rgb(248 185 99 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-primary-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(244 149 65 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-primary-600 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(214 131 56 / var(--tw-border-opacity));
|
||||
@@ -1330,11 +1339,21 @@ video {
|
||||
background-color: rgb(253 222 133 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-200 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(251 203 116 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(248 185 99 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(244 149 65 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
|
||||
@@ -1370,11 +1389,6 @@ video {
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(244 149 65 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -1872,6 +1886,11 @@ video {
|
||||
background-color: rgb(246 167 82 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-primary-600:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-primary-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
|
||||
@@ -1897,11 +1916,6 @@ video {
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-primary-600:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:from-primary-600:hover {
|
||||
--tw-gradient-from: #d68338 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(214 131 56 / 0) var(--tw-gradient-to-position);
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="RegisterModel.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasVault.Shared.Models.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Register model.
|
||||
/// </summary>
|
||||
public class RegisterModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the username.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Username { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(8, ErrorMessage = "Password must be at least 8 characters long.")]
|
||||
public string Password { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password confirmation.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Compare("Password", ErrorMessage = "Passwords do not match.")]
|
||||
public string PasswordConfirm { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the terms and conditions are accepted or not.
|
||||
/// </summary>
|
||||
[MustBeTrue(ErrorMessage = "You must accept the terms and conditions.")]
|
||||
public bool AcceptTerms { get; set; } = false;
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models;
|
||||
namespace AliasVault.Shared.Models.WebApi.Auth;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models;
|
||||
namespace AliasVault.Shared.Models.WebApi.Auth;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Login model for two factor authentication step.
|
||||
/// Login model for two factor authentication step using an authenticator code.
|
||||
/// </summary>
|
||||
public class LoginModel2Fa
|
||||
{
|
||||
@@ -0,0 +1,22 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="LoginModelRecoveryCode.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi.Auth;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Login model for two factor authentication step using a recovery code.
|
||||
/// </summary>
|
||||
public class LoginModelRecoveryCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the recovery code.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string RecoveryCode { get; set; } = null!;
|
||||
}
|
||||
@@ -7,37 +7,37 @@
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi.Auth;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasVault.Shared.Models.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents the model for registering a new user
|
||||
/// using SRP (Secure Remote Password) protocol.
|
||||
/// Register model.
|
||||
/// </summary>
|
||||
public class RegisterModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegisterModel"/> class.
|
||||
/// Gets or sets the username.
|
||||
/// </summary>
|
||||
/// <param name="email">Email.</param>
|
||||
/// <param name="salt">Salt.</param>
|
||||
/// <param name="verifier">Verifier.</param>
|
||||
public RegisterModel(string email, string salt, string verifier)
|
||||
{
|
||||
Email = email;
|
||||
Salt = salt;
|
||||
Verifier = verifier;
|
||||
}
|
||||
[Required]
|
||||
public string Username { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email.
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
[Required]
|
||||
[MinLength(8, ErrorMessage = "Password must be at least 8 characters long.")]
|
||||
public string Password { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the salt.
|
||||
/// Gets or sets the password confirmation.
|
||||
/// </summary>
|
||||
public string Salt { get; set; }
|
||||
[Required]
|
||||
[Compare("Password", ErrorMessage = "Passwords do not match.")]
|
||||
public string PasswordConfirm { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the verifier.
|
||||
/// Gets or sets a value indicating whether the terms and conditions are accepted or not.
|
||||
/// </summary>
|
||||
public string Verifier { get; set; }
|
||||
[MustBeTrue(ErrorMessage = "You must accept the terms and conditions.")]
|
||||
public bool AcceptTerms { get; set; } = false;
|
||||
}
|
||||
|
||||
43
src/AliasVault.Shared/Models/WebApi/Auth/RegisterModelOld.cs
Normal file
43
src/AliasVault.Shared/Models/WebApi/Auth/RegisterModelOld.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="RegisterModelOld.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents the model for registering a new user
|
||||
/// using SRP (Secure Remote Password) protocol.
|
||||
/// </summary>
|
||||
public class RegisterModelOld
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegisterModelOld"/> class.
|
||||
/// </summary>
|
||||
/// <param name="email">Email.</param>
|
||||
/// <param name="salt">Salt.</param>
|
||||
/// <param name="verifier">Verifier.</param>
|
||||
public RegisterModelOld(string email, string salt, string verifier)
|
||||
{
|
||||
Email = email;
|
||||
Salt = salt;
|
||||
Verifier = verifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the salt.
|
||||
/// </summary>
|
||||
public string Salt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the verifier.
|
||||
/// </summary>
|
||||
public string Verifier { get; set; }
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models;
|
||||
namespace AliasVault.Shared.Models.WebApi.Auth;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models;
|
||||
namespace AliasVault.Shared.Models.WebApi.Auth;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ValidateLoginRequestRecoveryCode.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a request to validate a login with added 2-factor authentication code.
|
||||
/// </summary>
|
||||
public class ValidateLoginRequestRecoveryCode : ValidateLoginRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ValidateLoginRequestRecoveryCode"/> class.
|
||||
/// </summary>
|
||||
/// <param name="username">Username.</param>
|
||||
/// <param name="clientPublicEphemeral">Client public ephemeral.</param>
|
||||
/// <param name="clientSessionProof">Client session proof.</param>
|
||||
/// <param name="recoveryCode">2-factor recovery code.</param>
|
||||
public ValidateLoginRequestRecoveryCode(string username, string clientPublicEphemeral, string clientSessionProof, string recoveryCode)
|
||||
: base(username, clientPublicEphemeral, clientSessionProof)
|
||||
{
|
||||
RecoveryCode = recoveryCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the 2-factor authentication recovery code.
|
||||
/// </summary>
|
||||
public string RecoveryCode { get; }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models;
|
||||
namespace AliasVault.Shared.Models.WebApi.Favicon;
|
||||
|
||||
/// <summary>
|
||||
/// FaviconExtractModel model.
|
||||
@@ -19,7 +19,11 @@ public static class CredentialCsvService
|
||||
{
|
||||
private const string CsvVersionIdentifier = "1.0.0";
|
||||
|
||||
/// <param name="filePath">The file path of the CSV file.</param>
|
||||
/// <summary>
|
||||
/// Export list of credentials to CSV file.
|
||||
/// </summary>
|
||||
/// <param name="credentials">List of credentials to export.</param>
|
||||
/// <returns>CSV file as byte array.</returns>
|
||||
public static byte[] ExportCredentialsToCsv(List<Credential> credentials)
|
||||
{
|
||||
var records = new List<CredentialCsvRecord>();
|
||||
@@ -30,7 +34,7 @@ public static class CredentialCsvService
|
||||
{
|
||||
Version = CsvVersionIdentifier,
|
||||
Id = credential.Id,
|
||||
Username = credential.Username,
|
||||
Username = credential.Username ?? string.Empty,
|
||||
Notes = credential.Notes ?? string.Empty,
|
||||
CreatedAt = credential.CreatedAt,
|
||||
UpdatedAt = credential.UpdatedAt,
|
||||
|
||||
Reference in New Issue
Block a user