make pwa token validation more stable

This commit is contained in:
Fabian Freund
2026-02-12 11:30:22 +01:00
parent 2911673e37
commit 447b3d49ef
3 changed files with 102 additions and 14 deletions
@@ -14,6 +14,7 @@ object PwaConstants {
const val EXTRA_PWA_PROFILE_UUID = "pwa_profile_uuid"
const val EXTRA_PWA_CONTEXT_ID = "pwa_context_id"
const val EXTRA_PWA_TOKEN = "pwa_token"
const val EXTRA_PWA_INSTALL_START_URL = "pwa_install_start_url"
// Profile and file paths
const val CURRENT_PROFILE_FILE = "weblibre_profiles/current_profile"
@@ -10,6 +10,8 @@ import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import eu.weblibre.flutter_mozilla_components.Components
@@ -147,7 +149,8 @@ class IntentReceiverActivity : Activity() {
}
private fun isTrustedPwaLaunch(intent: Intent, profileUuid: String, token: String?): Boolean {
val url = intent.dataString ?: return false
val intentUrl = intent.dataString ?: return false
val installStartUrl = intent.getStringExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL)
val action = intent.action
val hasTrustedAction = action == Intent.ACTION_VIEW || action == "mozilla.components.feature.pwa.VIEW_PWA"
if (!hasTrustedAction || token.isNullOrEmpty()) {
@@ -158,14 +161,53 @@ class IntentReceiverActivity : Activity() {
PwaConstants.PROFILE_MAPPING_PREFS,
Context.MODE_PRIVATE,
)
val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${url}::${profileUuid}"
val storedToken = prefs.getString(tokenKey, null)
if (storedToken == null || storedToken != token) {
Log.w(TAG, "PWA token mismatch for $url")
val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${intentUrl}::${profileUuid}"
if (prefs.getString(tokenKey, null) == token) {
return true
}
if (!installStartUrl.isNullOrEmpty() && installStartUrl != intentUrl) {
val installTokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${installStartUrl}::${profileUuid}"
if (prefs.getString(installTokenKey, null) == token) {
return true
}
}
if (isPinnedShortcutTokenMatch(intentUrl, profileUuid, token, installStartUrl)) {
return true
}
Log.w(TAG, "PWA token mismatch for $intentUrl")
return false
}
private fun isPinnedShortcutTokenMatch(
intentUrl: String,
profileUuid: String,
token: String,
installStartUrl: String?,
): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return false
}
return true
val shortcutManager = getSystemService(ShortcutManager::class.java) ?: return false
return shortcutManager.pinnedShortcuts.any { shortcut ->
val shortcutIntent = shortcut.intent ?: return@any false
if (shortcutIntent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) != profileUuid) {
return@any false
}
if (shortcutIntent.getStringExtra(PwaConstants.EXTRA_PWA_TOKEN) != token) {
return@any false
}
val shortcutUrl = shortcutIntent.dataString
val shortcutInstallUrl = shortcutIntent.getStringExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL)
shortcutUrl == intentUrl ||
(shortcutInstallUrl != null && shortcutInstallUrl == intentUrl) ||
(!installStartUrl.isNullOrEmpty() && shortcutInstallUrl == installStartUrl && (shortcutUrl == intentUrl || shortcutUrl == installStartUrl))
}
}
private fun resolveSessionIdFromStore(
@@ -145,7 +145,12 @@ class GeckoPwaApiImpl(
val (iconBitmap, isMaskable) = loadPwaIcon(manifest)
val shortcutId = generateShortcutId(manifest.startUrl)
val launchToken = generateAndStoreLaunchToken(manifest.startUrl, profileUuid)
val launchToken = resolveLaunchToken(
shortcutManager = shortcutManager,
shortcutId = shortcutId,
startUrl = manifest.startUrl,
profileUuid = profileUuid,
)
val appName = manifest.shortName ?: manifest.name ?: "Web App"
@@ -155,6 +160,7 @@ class GeckoPwaApiImpl(
putExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID, profileUuid)
putExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID, contextId)
putExtra(PwaConstants.EXTRA_PWA_TOKEN, launchToken)
putExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL, manifest.startUrl)
}
val shortcut = ShortcutInfo.Builder(context, shortcutId).apply {
@@ -261,16 +267,55 @@ class GeckoPwaApiImpl(
.apply()
}
private fun generateAndStoreLaunchToken(startUrl: String, profileUuid: String): String {
val token = UUID.randomUUID().toString()
val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${startUrl}::${profileUuid}"
val committed = appPrefs.edit()
.putString(tokenKey, token)
.commit()
private fun resolveLaunchToken(
shortcutManager: ShortcutManager,
shortcutId: String,
startUrl: String,
profileUuid: String,
): String {
val storedToken = getStoredLaunchToken(startUrl, profileUuid)
val existingShortcutToken = shortcutManager.pinnedShortcuts
.firstOrNull { shortcut -> shortcut.id == shortcutId }
?.intent
?.takeIf { shortcutIntent ->
shortcutIntent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) == profileUuid
}
?.getStringExtra(PwaConstants.EXTRA_PWA_TOKEN)
if (!existingShortcutToken.isNullOrEmpty()) {
val committed = storeLaunchToken(startUrl, profileUuid, existingShortcutToken)
if (!committed) {
logger.warn("Failed to persist pinned shortcut PWA token for $startUrl")
}
return existingShortcutToken
}
if (!storedToken.isNullOrEmpty()) {
val committed = storeLaunchToken(startUrl, profileUuid, storedToken)
if (!committed) {
logger.warn("Failed to refresh stored PWA launch token index for $startUrl")
}
return storedToken
}
val generatedToken = UUID.randomUUID().toString()
val committed = storeLaunchToken(startUrl, profileUuid, generatedToken)
if (!committed) {
logger.warn("Failed to persist PWA launch token for $startUrl")
}
return token
return generatedToken
}
private fun getStoredLaunchToken(startUrl: String, profileUuid: String): String? {
val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${startUrl}::${profileUuid}"
return appPrefs.getString(tokenKey, null)
}
private fun storeLaunchToken(startUrl: String, profileUuid: String, token: String): Boolean {
val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${startUrl}::${profileUuid}"
return appPrefs.edit()
.putString(tokenKey, token)
.commit()
}
private fun getProfileMapping(startUrl: String): String? {