mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-01-28 07:55:35 -05:00
362 lines
15 KiB
C#
362 lines
15 KiB
C#
//-----------------------------------------------------------------------
|
|
// <copyright file="AuthService.cs" company="lanedirt">
|
|
// Copyright (c) lanedirt. All rights reserved.
|
|
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
|
// </copyright>
|
|
//-----------------------------------------------------------------------
|
|
|
|
namespace AliasVault.Client.Services.Auth;
|
|
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using AliasVault.Shared.Models.WebApi.Auth;
|
|
using Blazored.LocalStorage;
|
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
|
|
|
/// <summary>
|
|
/// This service is responsible for handling authentication-related operations such as refreshing tokens,
|
|
/// storing tokens, and revoking tokens.
|
|
/// </summary>
|
|
/// <param name="httpClient">The HTTP client.</param>
|
|
/// <param name="localStorage">The local storage service.</param>
|
|
/// <param name="environment">IWebAssemblyHostEnvironment instance.</param>
|
|
/// <param name="config">Config instance.</param>
|
|
/// <param name="jsInteropService">JSInteropService instance.</param>
|
|
public sealed class AuthService(HttpClient httpClient, ILocalStorageService localStorage, IWebAssemblyHostEnvironment environment, Config config, JsInteropService jsInteropService)
|
|
{
|
|
private const string AccessTokenKey = "token";
|
|
private const string RefreshTokenKey = "refreshToken";
|
|
|
|
/// <summary>
|
|
/// Test string that is stored in local storage in encrypted state. This is used to validate the encryption key
|
|
/// locally during future vault unlocks.
|
|
/// </summary>
|
|
private const string EncryptionTestString = "aliasvault-test-string";
|
|
|
|
/// <summary>
|
|
/// The username of the currently logged-in user to prevent any conflicts during future vault saves.
|
|
/// </summary>
|
|
private string _username = string.Empty;
|
|
|
|
/// <summary>
|
|
/// The encryption key used to encrypt and decrypt the vault data.
|
|
/// </summary>
|
|
private byte[] _encryptionKey = new byte[32];
|
|
|
|
/// <summary>
|
|
/// Refreshes the access token asynchronously.
|
|
/// </summary>
|
|
/// <returns>The new access token.</returns>
|
|
public async Task<string?> RefreshTokenAsync()
|
|
{
|
|
var accessToken = await GetAccessTokenAsync();
|
|
var refreshToken = await GetRefreshTokenAsync();
|
|
var tokenInput = new TokenModel { Token = accessToken, RefreshToken = refreshToken };
|
|
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/Auth/refresh")
|
|
{
|
|
Content = JsonContent.Create(tokenInput),
|
|
};
|
|
|
|
// Add the X-Ignore-Failure header to the request so any failure does not trigger another refresh token request.
|
|
request.Headers.Add("X-Ignore-Failure", "true");
|
|
var response = await httpClient.SendAsync(request);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var responseContent = await response.Content.ReadAsStringAsync();
|
|
var tokenResponse = JsonSerializer.Deserialize<TokenModel>(responseContent);
|
|
|
|
if (tokenResponse != null)
|
|
{
|
|
// Store the token as a plain string in local storage.
|
|
await StoreAccessTokenAsync(tokenResponse.Token);
|
|
await StoreRefreshTokenAsync(tokenResponse.RefreshToken);
|
|
|
|
return tokenResponse.Token;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the username of the currently logged-in user.
|
|
/// </summary>
|
|
/// <returns>The currently logged-in user's username.</returns>
|
|
public string GetUsername()
|
|
{
|
|
return _username;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the username of the vault owner in local memory. This value will be sent to the server during
|
|
/// vault updates to ensure that the API is updating the correct vault of the correct user preventing any conflicts
|
|
/// or vault corruption.
|
|
/// </summary>
|
|
/// <param name="username">The username of the currently logged-in user and owner of the vault being loaded.</param>
|
|
public void StoreUsername(string? username)
|
|
{
|
|
_username = username ?? string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the stored access token asynchronously.
|
|
/// </summary>
|
|
/// <returns>The stored access token.</returns>
|
|
public async Task<string> GetAccessTokenAsync()
|
|
{
|
|
return await localStorage.GetItemAsStringAsync(AccessTokenKey) ?? string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the new access token asynchronously.
|
|
/// </summary>
|
|
/// <param name="newToken">The new access token.</param>
|
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
|
public async Task StoreAccessTokenAsync(string newToken)
|
|
{
|
|
await localStorage.SetItemAsStringAsync(AccessTokenKey, newToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get encryption key.
|
|
/// </summary>
|
|
/// <returns>SrpArgonEncryption key as byte[].</returns>
|
|
public byte[] GetEncryptionKey()
|
|
{
|
|
return _encryptionKey;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get encryption key as base64 string.
|
|
/// </summary>
|
|
/// <returns>SrpArgonEncryption key as base64 string.</returns>
|
|
public string GetEncryptionKeyAsBase64Async()
|
|
{
|
|
if (environment.IsDevelopment() && config.UseDebugEncryptionKey)
|
|
{
|
|
// When project runs in development mode a static encryption key will be used.
|
|
// This allows to skip the unlock screen for faster development.
|
|
// Use launch profile "http-release" to get the actual user flow.
|
|
return "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=";
|
|
}
|
|
|
|
return Convert.ToBase64String(GetEncryptionKey());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether the encryption key is set.
|
|
/// </summary>
|
|
/// <returns>Return true if encryption key is set, otherwise false.</returns>
|
|
public bool IsEncryptionKeySet()
|
|
{
|
|
// Check that encryption key is set. If not, redirect to unlock screen.
|
|
var encryptionKey = GetEncryptionKeyAsBase64Async();
|
|
if (encryptionKey == string.Empty || encryptionKey == "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
|
|
{
|
|
// SrpArgonEncryption key is empty or base64 encoded empty string.
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the encryption key asynchronously in-memory.
|
|
/// </summary>
|
|
/// <param name="newKey">SrpArgonEncryption key.</param>
|
|
/// <returns>Task.</returns>
|
|
public async Task StoreEncryptionKeyAsync(byte[] newKey)
|
|
{
|
|
_encryptionKey = newKey;
|
|
|
|
// When storing a new encryption key, encrypt a test string and save it to local storage.
|
|
// This test string can then be used to locally validate the password during future unlocks.
|
|
var encryptedTestString = await jsInteropService.SymmetricEncrypt(EncryptionTestString, GetEncryptionKeyAsBase64Async());
|
|
|
|
// Store the encrypted test string in local storage.
|
|
await localStorage.SetItemAsStringAsync("encryptionTestString", encryptedTestString);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if WebAuthn is enabled.
|
|
/// </summary>
|
|
/// <returns>True if WebAuthn is enabled, otherwise false.</returns>
|
|
public async Task<bool> IsWebAuthnEnabledAsync()
|
|
{
|
|
await localStorage.GetItemAsStringAsync("webAuthnEnabled");
|
|
return await localStorage.GetItemAsStringAsync("webAuthnEnabled") == "true";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the encryption key that is stored in local storage and decrypt it with the WebAuthn derived key.
|
|
/// </summary>
|
|
/// <param name="username">The username to associate with the credential.</param>
|
|
/// <returns>Decrypted encryption key.</returns>
|
|
public async Task<byte[]> GetDecryptedWebAuthnEncryptionKeyAsync(string username)
|
|
{
|
|
var encryptedEncryptionKey = await localStorage.GetItemAsStringAsync("webAuthnEncryptedEncryptionKey");
|
|
var webauthnCredentialId = await localStorage.GetItemAsStringAsync("webAuthnCredentialId");
|
|
var webauthnSalt = await localStorage.GetItemAsStringAsync("webAuthnSalt");
|
|
if (string.IsNullOrEmpty(encryptedEncryptionKey) || string.IsNullOrEmpty(webauthnCredentialId) || string.IsNullOrEmpty(webauthnSalt))
|
|
{
|
|
throw new InvalidOperationException("WebAuthn encrypted encryption key is not set or WebAuthn credential ID is not set.");
|
|
}
|
|
|
|
var webauthnCredentialDerivedKey = await jsInteropService.GetWebAuthnCredentialDerivedKey(webauthnCredentialId, webauthnSalt);
|
|
|
|
// Decrypt the encrypted encryption key with the WebAuthn derived key.
|
|
var decryptedString = await jsInteropService.SymmetricDecrypt(encryptedEncryptionKey, webauthnCredentialDerivedKey);
|
|
|
|
return Convert.FromBase64String(decryptedString);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set WebAuthn enabled. This will be used to determine if WebAuthn should be used for attempting to unlock the vault.
|
|
/// If set to false, the user will be prompted to enter the master password instead.
|
|
/// </summary>
|
|
/// <param name="enabled">True if WebAuthn is enabled, otherwise false.</param>
|
|
/// <param name="webauthCredentialId">WebAuthn credential ID.</param>
|
|
/// <param name="webauthSalt">WebAuthn salt.</param>
|
|
/// <param name="webauthCredentialDerivedKey">WebAuthn credential derived key.</param>
|
|
/// <returns>Task.</returns>
|
|
public async Task SetWebAuthnEnabledAsync(bool enabled, string? webauthCredentialId = null, string? webauthSalt = null, string? webauthCredentialDerivedKey = null)
|
|
{
|
|
await localStorage.SetItemAsStringAsync("webAuthnEnabled", enabled.ToString().ToLower());
|
|
|
|
// Encrypt the current encryption key with the webauthn derived key and store it in local storage.
|
|
if (enabled && !string.IsNullOrEmpty(webauthCredentialId) && !string.IsNullOrEmpty(webauthSalt) && !string.IsNullOrEmpty(webauthCredentialDerivedKey))
|
|
{
|
|
var encryptionKeyBase64 = Convert.ToBase64String(GetEncryptionKey());
|
|
var encryptedEncryptionKey = await jsInteropService.SymmetricEncrypt(encryptionKeyBase64, webauthCredentialDerivedKey);
|
|
await localStorage.SetItemAsStringAsync("webAuthnCredentialId", webauthCredentialId);
|
|
await localStorage.SetItemAsStringAsync("webAuthnSalt", webauthSalt);
|
|
await localStorage.SetItemAsStringAsync("webAuthnEncryptedEncryptionKey", encryptedEncryptionKey);
|
|
}
|
|
else
|
|
{
|
|
// Clear the WebAuthn credential ID, salt and derived key if WebAuthn is disabled.
|
|
await localStorage.RemoveItemAsync("webAuthnCredentialId");
|
|
await localStorage.RemoveItemAsync("webAuthnSalt");
|
|
await localStorage.RemoveItemAsync("webAuthnCredentialDerivedKey");
|
|
await localStorage.RemoveItemAsync("webAuthnEncryptedEncryptionKey");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the encryption test string is stored in local storage which is used to validate
|
|
/// the encryption key locally during future vault unlocks. If it's not stored the unlock
|
|
/// attempts will fail and user should log in again instead.
|
|
/// </summary>
|
|
/// <returns>Task.</returns>
|
|
public async Task<bool> HasEncryptionKeyTestStringAsync()
|
|
{
|
|
return await localStorage.GetItemAsStringAsync("encryptionTestString") != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate the encryption locally by attempting to decrypt test string stored in local storage.
|
|
/// </summary>
|
|
/// <param name="encryptionKey">The encryption key to validate.</param>
|
|
/// <returns>True if encryption key is valid, false if not.</returns>
|
|
public async Task<bool> ValidateEncryptionKeyAsync(byte[] encryptionKey)
|
|
{
|
|
// Get the encrypted test string from local storage.
|
|
var encryptedTestString = await localStorage.GetItemAsStringAsync("encryptionTestString");
|
|
if (encryptedTestString == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var base64EncryptionKey = Convert.ToBase64String(encryptionKey);
|
|
|
|
// Decrypt the test string using the provided encryption key.
|
|
try
|
|
{
|
|
var decryptedTestString = await jsInteropService.SymmetricDecrypt(encryptedTestString, base64EncryptionKey);
|
|
|
|
// If the decrypted test string is not equal to the test string, the encryption key is invalid.
|
|
return decryptedTestString == EncryptionTestString;
|
|
}
|
|
catch
|
|
{
|
|
// Ignore errors, if decryption fails the encryption key is invalid.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the new refresh token asynchronously.
|
|
/// </summary>
|
|
/// <param name="newToken">The new refresh token.</param>
|
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
|
public async Task StoreRefreshTokenAsync(string newToken)
|
|
{
|
|
await localStorage.SetItemAsStringAsync(RefreshTokenKey, newToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the stored access and refresh tokens asynchronously, called when logging out.
|
|
/// </summary>
|
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
|
public async Task RemoveTokensAsync()
|
|
{
|
|
// Revoke the tokens from the server by calling the webapi.
|
|
try
|
|
{
|
|
await RevokeTokenAsync();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// If an exception occurs we ignore it and continue with removing the tokens from local storage.
|
|
}
|
|
|
|
// Remove the tokens from local storage.
|
|
_username = string.Empty;
|
|
await localStorage.RemoveItemAsync(AccessTokenKey);
|
|
await localStorage.RemoveItemAsync(RefreshTokenKey);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the encryption key from memory, called during logout.
|
|
/// </summary>
|
|
public void RemoveEncryptionKey()
|
|
{
|
|
_encryptionKey = new byte[32];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Revokes the access and refresh tokens on the server asynchronously.
|
|
/// </summary>
|
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
|
private async Task RevokeTokenAsync()
|
|
{
|
|
// Remove webauthn enabled flag.
|
|
await SetWebAuthnEnabledAsync(false);
|
|
|
|
var tokenInput = new TokenModel
|
|
{
|
|
Token = await GetAccessTokenAsync(),
|
|
RefreshToken = await GetRefreshTokenAsync(),
|
|
};
|
|
|
|
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/Auth/revoke")
|
|
{
|
|
Content = JsonContent.Create(tokenInput),
|
|
};
|
|
|
|
// Add the X-Ignore-Failure header to the request so any failure does not trigger another refresh token request.
|
|
request.Headers.Add("X-Ignore-Failure", "true");
|
|
await httpClient.SendAsync(request);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the stored refresh token asynchronously.
|
|
/// </summary>
|
|
/// <returns>The stored refresh token.</returns>
|
|
private async Task<string> GetRefreshTokenAsync()
|
|
{
|
|
return await localStorage.GetItemAsStringAsync(RefreshTokenKey) ?? string.Empty;
|
|
}
|
|
}
|