Add unique app instance ID for Android (#1930)

This commit is contained in:
Leendert de Borst
2026-04-19 09:46:09 +02:00
committed by Leendert de Borst
parent 36ade373ff
commit e6e4dbb6d6
2 changed files with 45 additions and 9 deletions

View File

@@ -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"
}
}

View File

@@ -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.
/// </summary>
/// <param name="request">The HttpRequest instance for the request that the client used.</param>
/// <returns>Unique device identifier as string.</returns>
@@ -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}";
}
}