diff --git a/apps/server/AliasVault.Api/Controllers/AuthController.cs b/apps/server/AliasVault.Api/Controllers/AuthController.cs index e0bd3a96f..b44949e1a 100644 --- a/apps/server/AliasVault.Api/Controllers/AuthController.cs +++ b/apps/server/AliasVault.Api/Controllers/AuthController.cs @@ -100,34 +100,13 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM // Check client version compatibility if header is provided var clientSupported = false; - if (!string.IsNullOrEmpty(clientHeader)) + var clientInfo = ClientHeaderInfo.Parse(clientHeader); + if (!string.IsNullOrEmpty(clientInfo.ClientVersion) + && AppInfo.MinimumClientVersions.TryGetValue(clientInfo.ClientName, out var minimumVersion)) { - // Client header format should be "{platform}-{version}" e.g. "chrome-1.4.0" - var parts = clientHeader.Split('-'); - if (parts.Length == 2) - { - var platform = parts[0].ToLowerInvariant(); - var clientVersion = parts[1]; - - if (AppInfo.MinimumClientVersions.TryGetValue(platform, out var minimumVersion)) - { - // Check if version meets minimum requirement AND is not in blocked list - var meetsMinimum = VersionHelper.IsVersionEqualOrNewer(clientVersion, minimumVersion); - var isBlocked = VersionHelper.IsVersionBlocked(platform, clientVersion, AppInfo.UnsupportedClientVersions); - - clientSupported = meetsMinimum && !isBlocked; - } - else - { - // Unknown platform - clientSupported = false; - } - } - else - { - // Invalid header format - clientSupported = false; - } + var meetsMinimum = VersionHelper.IsVersionEqualOrNewer(clientInfo.ClientVersion, minimumVersion); + var isBlocked = VersionHelper.IsVersionBlocked(clientInfo.ClientName, clientInfo.ClientVersion, AppInfo.UnsupportedClientVersions); + clientSupported = meetsMinimum && !isBlocked; } return Ok(new StatusResponse diff --git a/apps/server/AliasVault.Api/Helpers/AuthHelper.cs b/apps/server/AliasVault.Api/Helpers/AuthHelper.cs index bf0570705..a0bd98d2b 100644 --- a/apps/server/AliasVault.Api/Helpers/AuthHelper.cs +++ b/apps/server/AliasVault.Api/Helpers/AuthHelper.cs @@ -106,19 +106,14 @@ public static class AuthHelper var userAgent = request.Headers.UserAgent.ToString(); var acceptLanguage = request.Headers.AcceptLanguage.ToString(); - // Client header is formatted like "[client name]-[version]" or "[client name]-[version]-[app-instance-id]" - // Examples: "chrome-0.25.0", "android-0.29.0-550e8400e29b41d4a716446655440000" - var clientHeader = request.Headers["X-AliasVault-Client"].ToString(); - var clientParts = clientHeader?.Split('-') ?? []; - var clientName = clientParts.Length > 0 ? clientParts[0] : "unknown"; + var clientInfo = ClientHeaderInfo.Parse(request.Headers[ClientHeaderInfo.HeaderName].ToString()); - // For Android, extract app instance ID if present (UUID without dashes as 3rd part) - var appInstanceSuffix = string.Empty; - if (clientName == "android" && clientParts.Length >= 3) - { - appInstanceSuffix = $"|{clientParts[2]}"; - } + // Only Android appends an app instance id, used to keep device identifiers unique + // across multiple Android User Profiles on the same physical device. + var appInstanceSuffix = clientInfo.ClientName == "android" && clientInfo.AppInstanceId is not null + ? $"|{clientInfo.AppInstanceId}" + : string.Empty; - return $"{clientName}|{userAgent}|{acceptLanguage}{appInstanceSuffix}"; + return $"{clientInfo.ClientName}|{userAgent}|{acceptLanguage}{appInstanceSuffix}"; } } diff --git a/apps/server/AliasVault.Api/Helpers/ClientHeaderInfo.cs b/apps/server/AliasVault.Api/Helpers/ClientHeaderInfo.cs new file mode 100644 index 000000000..85ab9475f --- /dev/null +++ b/apps/server/AliasVault.Api/Helpers/ClientHeaderInfo.cs @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------- +// +// 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.Api.Helpers; + +/// +/// Parsed components of the X-AliasVault-Client request header. +/// +/// Header format is "{client}-{version}", optionally followed by additional dash-separated +/// segments. +/// +/// Note: currently only the Android app appends a third segment with a per-install +/// app instance id (UUID without dashes) to support multiple Android User Profiles. +/// +/// Examples: +/// - "chrome-0.29.0" +/// - "ios-0.29.0" +/// - "android-0.29.0-550e8400e29b41d4a716446655440000". +/// +/// Lowercased client/platform identifier (e.g. "chrome", "android"). "unknown" when the header is missing or empty. +/// Client version string (e.g. "0.29.0"), or null when not present. +/// Per-install app instance identifier, or null when not present. +public sealed record ClientHeaderInfo(string ClientName, string? ClientVersion, string? AppInstanceId) +{ + /// + /// Header name used by AliasVault clients to identify themselves. + /// + public const string HeaderName = "X-AliasVault-Client"; + + /// + /// Parse a raw X-AliasVault-Client header value into its components. + /// + /// Raw header value, may be null or empty. + /// Parsed ClientHeaderInfo. Missing trailing segments are returned as null. + public static ClientHeaderInfo Parse(string? headerValue) + { + if (string.IsNullOrEmpty(headerValue)) + { + return new ClientHeaderInfo("unknown", null, null); + } + + var parts = headerValue.Split('-'); + var clientName = parts[0].ToLowerInvariant(); + var clientVersion = parts.Length > 1 ? parts[1] : null; + var appInstanceId = parts.Length > 2 ? parts[2] : null; + + return new ClientHeaderInfo(clientName, clientVersion, appInstanceId); + } +}