diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/webapi/WebApiService.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/webapi/WebApiService.kt
index 0a6674394..f19fa2e4d 100644
--- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/webapi/WebApiService.kt
+++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/webapi/WebApiService.kt
@@ -34,12 +34,29 @@ class WebApiService(private val context: Context) {
private const val API_URL_KEY = "apiUrl"
private const val ACCESS_TOKEN_KEY = "accessToken"
private const val REFRESH_TOKEN_KEY = "refreshToken"
+ private const val APP_INSTANCE_ID_KEY = "appInstanceId"
private const val DEFAULT_API_URL = "https://app.aliasvault.net/api"
private const val SHARED_PREFS_NAME = "aliasvault"
}
private val sharedPreferences = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
+ /**
+ * Unique app instance ID for this app installation/Android user profile.
+ * This is generated once and persists to differentiate between multiple
+ * Android User Profiles on the same device.
+ */
+ private val appInstanceId: String by lazy {
+ var id = sharedPreferences.getString(APP_INSTANCE_ID_KEY, null)
+ if (id == null) {
+ // Generate a random UUID and remove dashes to avoid conflicts with header format
+ id = java.util.UUID.randomUUID().toString().replace("-", "")
+ sharedPreferences.edit().putString(APP_INSTANCE_ID_KEY, id).apply()
+ Log.d(TAG, "Generated new app instance ID: $id")
+ }
+ id
+ }
+
// MARK: - Configuration Management
/**
@@ -309,16 +326,18 @@ class WebApiService(private val context: Context) {
/**
* Get the client version header value.
+ * Format: "android-{version}-{appInstanceId}"
+ * The app instance ID uniquely identifies this app installation/Android user profile.
*/
private fun getClientVersionHeader(): String {
return try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val version = packageInfo.versionName ?: "0.0.0"
val baseVersion = version.split("-").firstOrNull() ?: "0.0.0"
- "android-$baseVersion"
+ "android-$baseVersion-$appInstanceId"
} catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG, "Error getting package version", e)
- "android-0.0.0"
+ "android-0.0.0-$appInstanceId"
}
}
diff --git a/apps/server/AliasVault.Api/Helpers/AuthHelper.cs b/apps/server/AliasVault.Api/Helpers/AuthHelper.cs
index 6123c6a48..bf0570705 100644
--- a/apps/server/AliasVault.Api/Helpers/AuthHelper.cs
+++ b/apps/server/AliasVault.Api/Helpers/AuthHelper.cs
@@ -86,9 +86,18 @@ public static class AuthHelper
/// conflicts when a user is logged in on multiple clients from the same browser/device.
/// For example, logging out from the browser extension won't affect the web app session.
///
- /// NOTE: current implementation means that only one refresh token can be valid for a
- /// specific user/device combo at a time. The identifier generation could be made more unique in the future
- /// to prevent any potential unwanted conflicts.
+ /// For Android, the identifier also includes an app instance ID at the end to support
+ /// multiple Android User Profiles on the same physical device. Each Android User Profile
+ /// generates a unique UUID (without dashes) on first launch that persists for the lifetime
+ /// of that installation.
+ ///
+ /// Device identifier format examples:
+ /// - Web/Browser: "chrome|Mozilla/5.0...|en-US"
+ /// - Android: "android|Dalvik/2.1.0...|en-US|550e8400e29b41d4a716446655440000"
+ /// - iOS: "ios|AliasVault/1.0...|en-US"
+ ///
+ /// NOTE: This implementation ensures only one refresh token can be valid for a
+ /// specific user/device combo at a time.
///
/// The HttpRequest instance for the request that the client used.
/// Unique device identifier as string.
@@ -97,11 +106,19 @@ public static class AuthHelper
var userAgent = request.Headers.UserAgent.ToString();
var acceptLanguage = request.Headers.AcceptLanguage.ToString();
- // Client header is usually formatted like "[client name]-[version]" e.g. "chrome-0.25.0", take only "chrome"
+ // 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 clientName = clientHeader?.Split('-')[0] ?? "unknown";
+ var clientParts = clientHeader?.Split('-') ?? [];
+ var clientName = clientParts.Length > 0 ? clientParts[0] : "unknown";
- var rawIdentifier = $"{clientName}|{userAgent}|{acceptLanguage}";
- return rawIdentifier;
+ // 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]}";
+ }
+
+ return $"{clientName}|{userAgent}|{acceptLanguage}{appInstanceSuffix}";
}
}