Refactor web app MobileLoginUtility flow, add helper model (#1347)

This commit is contained in:
Leendert de Borst
2025-11-17 23:36:08 +01:00
parent 6aa43bb1a2
commit 32fe2156f1
3 changed files with 86 additions and 13 deletions

View File

@@ -0,0 +1,34 @@
//-----------------------------------------------------------------------
// <copyright file="MobileLoginResult.cs" company="aliasvault">
// Copyright (c) aliasvault. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Client.Auth.Models;
/// <summary>
/// Result of a successful mobile login containing decrypted authentication data.
/// </summary>
public sealed class MobileLoginResult
{
/// <summary>
/// Gets or sets the username.
/// </summary>
public required string Username { get; set; }
/// <summary>
/// Gets or sets the JWT access token.
/// </summary>
public required string Token { get; set; }
/// <summary>
/// Gets or sets the refresh token.
/// </summary>
public required string RefreshToken { get; set; }
/// <summary>
/// Gets or sets the vault decryption key (base64 encoded).
/// </summary>
public required string DecryptionKey { get; set; }
}

View File

@@ -4,9 +4,11 @@
@attribute [AllowAnonymous]
@using System.Text.Json
@using AliasVault.Client.Auth.Components
@using AliasVault.Client.Auth.Models
@using AliasVault.Client.Auth.Services
@using AliasVault.Client.Utilities
@using AliasVault.Cryptography.Client
@using AliasVault.Shared.Models.WebApi.Auth
@using Microsoft.Extensions.Localization
@implements IDisposable
@@ -140,9 +142,10 @@
}
/// <summary>
/// Handle successful authentication.
/// Handle successful authentication from mobile login.
/// </summary>
private async Task HandleSuccessfulAuthAsync(string username, string token, string refreshToken, string decryptionKey, string salt, string encryptionType, string encryptionSettings)
/// <param name="result">The decrypted mobile login result containing authentication data.</param>
private async Task HandleSuccessfulAuthAsync(MobileLoginResult result)
{
try
{
@@ -150,12 +153,33 @@
_qrCodeUrl = null;
StateHasChanged();
// Call /login endpoint to retrieve salt and encryption settings
var loginInitiateRequest = new { username = result.Username };
var loginResponse = await Http.PostAsJsonAsync("v1/Auth/login", loginInitiateRequest);
if (!loginResponse.IsSuccessStatusCode)
{
_isLoading = false;
_errorMessage = SharedLocalizer["ErrorUnknown"];
StateHasChanged();
return;
}
var loginData = await loginResponse.Content.ReadFromJsonAsync<LoginInitiateResponse>();
if (loginData == null)
{
_isLoading = false;
_errorMessage = SharedLocalizer["ErrorUnknown"];
StateHasChanged();
return;
}
// Store the tokens in local storage
await AuthService.StoreAccessTokenAsync(token);
await AuthService.StoreRefreshTokenAsync(refreshToken);
await AuthService.StoreAccessTokenAsync(result.Token);
await AuthService.StoreRefreshTokenAsync(result.RefreshToken);
// Convert decryption key from base64 string to byte array
var decryptionKeyBytes = Convert.FromBase64String(decryptionKey);
var decryptionKeyBytes = Convert.FromBase64String(result.DecryptionKey);
// Store the encryption key in memory
await AuthService.StoreEncryptionKeyAsync(decryptionKeyBytes);

View File

@@ -11,6 +11,7 @@ using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AliasVault.Client.Auth.Models;
using AliasVault.Client.Services.JsInterop;
using AliasVault.Shared.Models.WebApi.Auth;
using Microsoft.Extensions.Logging;
@@ -75,10 +76,10 @@ public sealed class MobileLoginUtility : IDisposable
/// <summary>
/// Starts polling the server for mobile login response.
/// </summary>
/// <param name="onSuccess">Callback for successful authentication.</param>
/// <param name="onSuccess">Callback for successful authentication with decrypted login result.</param>
/// <param name="onError">Callback for errors.</param>
/// <returns>Task.</returns>
public Task StartPollingAsync(Func<string, string, string, string, string, string, string, Task> onSuccess, Action<string> onError)
public Task StartPollingAsync(Func<MobileLoginResult, Task> onSuccess, Action<string> onError)
{
if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_privateKey))
{
@@ -134,7 +135,7 @@ public sealed class MobileLoginUtility : IDisposable
Cleanup();
}
private async Task PollServerAsync(Func<string, string, string, string, string, string, string, Task> onSuccess, Action<string> onError)
private async Task PollServerAsync(Func<MobileLoginResult, Task> onSuccess, Action<string> onError)
{
if (string.IsNullOrEmpty(_requestId) || _cancellationTokenSource?.IsCancellationRequested == true)
{
@@ -161,20 +162,34 @@ public sealed class MobileLoginUtility : IDisposable
var result = await response.Content.ReadFromJsonAsync<MobileLoginPollResponse>();
if (result?.Fulfilled == true && !string.IsNullOrEmpty(result.EncryptedDecryptionKey) && !string.IsNullOrEmpty(result.Username) && result.Token != null && !string.IsNullOrEmpty(result.Salt) && !string.IsNullOrEmpty(result.EncryptionType) && !string.IsNullOrEmpty(result.EncryptionSettings))
if (result?.Fulfilled == true && !string.IsNullOrEmpty(result.EncryptedSymmetricKey))
{
// Stop polling
StopPolling();
// Decrypt the decryption key using private key
var decryptionKey = await _jsInteropService.DecryptWithPrivateKey(result.EncryptedDecryptionKey, _privateKey!);
// Decrypt the vault decryption key directly with RSA private key
var decryptionKey = await _jsInteropService.DecryptWithPrivateKey(result.EncryptedDecryptionKey!, _privateKey!);
// Decrypt the symmetric key with RSA private key
var symmetricKeyBase64 = await _jsInteropService.DecryptWithPrivateKey(result.EncryptedSymmetricKey, _privateKey!);
// Decrypt all remaining fields using the symmetric key
var token = await _jsInteropService.SymmetricDecrypt(result.EncryptedToken!, symmetricKeyBase64);
var refreshToken = await _jsInteropService.SymmetricDecrypt(result.EncryptedRefreshToken!, symmetricKeyBase64);
var username = await _jsInteropService.SymmetricDecrypt(result.EncryptedUsername!, symmetricKeyBase64);
// Clear sensitive data
_privateKey = null;
_requestId = null;
// Call success callback
await onSuccess(result.Username, result.Token.Token, result.Token.RefreshToken, decryptionKey, result.Salt, result.EncryptionType, result.EncryptionSettings);
// Call success callback with decrypted data
await onSuccess(new MobileLoginResult
{
Username = username,
Token = token,
RefreshToken = refreshToken,
DecryptionKey = decryptionKey,
});
}
}
catch (Exception ex)