From 32fe2156f1b7a7a097d788bb2798ad46adab91cd Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Mon, 17 Nov 2025 23:36:08 +0100 Subject: [PATCH] Refactor web app MobileLoginUtility flow, add helper model (#1347) --- .../Auth/Models/MobileLoginResult.cs | 34 +++++++++++++++++++ .../Auth/Pages/MobileLogin.razor | 34 ++++++++++++++++--- .../Auth/Services/MobileLoginUtility.cs | 31 ++++++++++++----- 3 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 apps/server/AliasVault.Client/Auth/Models/MobileLoginResult.cs diff --git a/apps/server/AliasVault.Client/Auth/Models/MobileLoginResult.cs b/apps/server/AliasVault.Client/Auth/Models/MobileLoginResult.cs new file mode 100644 index 000000000..bd11e42ea --- /dev/null +++ b/apps/server/AliasVault.Client/Auth/Models/MobileLoginResult.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) aliasvault. All rights reserved. +// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.Client.Auth.Models; + +/// +/// Result of a successful mobile login containing decrypted authentication data. +/// +public sealed class MobileLoginResult +{ + /// + /// Gets or sets the username. + /// + public required string Username { get; set; } + + /// + /// Gets or sets the JWT access token. + /// + public required string Token { get; set; } + + /// + /// Gets or sets the refresh token. + /// + public required string RefreshToken { get; set; } + + /// + /// Gets or sets the vault decryption key (base64 encoded). + /// + public required string DecryptionKey { get; set; } +} diff --git a/apps/server/AliasVault.Client/Auth/Pages/MobileLogin.razor b/apps/server/AliasVault.Client/Auth/Pages/MobileLogin.razor index ea41182ee..781010b30 100644 --- a/apps/server/AliasVault.Client/Auth/Pages/MobileLogin.razor +++ b/apps/server/AliasVault.Client/Auth/Pages/MobileLogin.razor @@ -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 @@ } /// - /// Handle successful authentication. + /// Handle successful authentication from mobile login. /// - private async Task HandleSuccessfulAuthAsync(string username, string token, string refreshToken, string decryptionKey, string salt, string encryptionType, string encryptionSettings) + /// The decrypted mobile login result containing authentication data. + 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(); + 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); diff --git a/apps/server/AliasVault.Client/Auth/Services/MobileLoginUtility.cs b/apps/server/AliasVault.Client/Auth/Services/MobileLoginUtility.cs index 22d7d6617..ba6a77a97 100644 --- a/apps/server/AliasVault.Client/Auth/Services/MobileLoginUtility.cs +++ b/apps/server/AliasVault.Client/Auth/Services/MobileLoginUtility.cs @@ -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 /// /// Starts polling the server for mobile login response. /// - /// Callback for successful authentication. + /// Callback for successful authentication with decrypted login result. /// Callback for errors. /// Task. - public Task StartPollingAsync(Func onSuccess, Action onError) + public Task StartPollingAsync(Func onSuccess, Action onError) { if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_privateKey)) { @@ -134,7 +135,7 @@ public sealed class MobileLoginUtility : IDisposable Cleanup(); } - private async Task PollServerAsync(Func onSuccess, Action onError) + private async Task PollServerAsync(Func onSuccess, Action onError) { if (string.IsNullOrEmpty(_requestId) || _cancellationTokenSource?.IsCancellationRequested == true) { @@ -161,20 +162,34 @@ public sealed class MobileLoginUtility : IDisposable var result = await response.Content.ReadFromJsonAsync(); - 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)