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);
+ }
+}