From 987ddb54f8716572afcc50046fc523111268d374 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 20 Jul 2026 18:37:32 -0500 Subject: [PATCH] feat(sso): App Links autoVerify callback + paired-host trust check Add the app side of Android App Links (M9 follow-up, docs/android/APP_LINKS.md), layered on the M9 Part 2 native SSO callback: - Build-time `appLinkHost` Gradle property -> BuildConfig.APP_LINK_HOST + manifestPlaceholders["appLinkHost"]. autoVerify needs a literal host, so the generic multi-tenant build leaves it empty (placeholder falls back to the reserved runic-gateway.invalid sentinel, making the filter inert); a white-label build bakes one host with -PappLinkHost=play.myshard.com. - Manifest: an autoVerify https `/mobile/callback` intent-filter beside the unchanged custom-scheme one (the permanent fallback). - SsoAuthManager: request the https App Link redirect_uri iff the baked host matches the paired shard host; matchesAppLinkCallback() enforces a paired-host trust check (host must equal the currently-paired base URL host) as defense-in-depth. Both matchers feed the same complete()/exchange path. - MainActivity routes custom-scheme and App Link callbacks identically. +5 JVM tests (SsoAuthManagerTest -> 14). Built green (JDK 21, -Pksp.incremental=false); white-label host substitution verified in the merged manifest. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr --- app/build.gradle.kts | 13 +++++ app/src/main/AndroidManifest.xml | 20 +++++++- .../java/com/runicgateway/app/MainActivity.kt | 14 ++++-- .../app/core/auth/sso/SsoAuthManager.kt | 47 ++++++++++++++++++- .../app/core/auth/sso/SsoAuthManagerTest.kt | 41 ++++++++++++++++ 5 files changed, 126 insertions(+), 9 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 202213f..873409a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,6 +48,19 @@ android { versionName = (project.findProperty("versionName") as String?)?.takeIf { it.isNotBlank() } ?: "0.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + // Android App Links host (docs/android/APP_LINKS.md). autoVerify needs a + // *literal* host at build time, so a single multi-tenant APK cannot verify + // open-ended shard domains: App Links are a build-time opt-in. Left empty for + // the generic build (custom scheme only); a white-label/first-party build + // bakes one host with `-PappLinkHost=play.myshard.com`. + // • BuildConfig.APP_LINK_HOST — SsoAuthManager reads it to pick the redirect. + // • manifestPlaceholder appLinkHost — substituted into the intent-filter host; + // empty falls back to the reserved `.invalid` sentinel so the autoVerify + // filter is inert (matches no real link, never verifies). + val appLinkHost = (project.findProperty("appLinkHost") as String?)?.trim().orEmpty() + buildConfigField("String", "APP_LINK_HOST", "\"$appLinkHost\"") + manifestPlaceholders["appLinkHost"] = appLinkHost.ifBlank { "runic-gateway.invalid" } } signingConfigs { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 11822b2..27b167e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,8 +39,8 @@ + MOBILE_AUTH_REDIRECT_URIS allowlist exactly. This is the permanent + fallback on every build (docs/android/APP_LINKS.md). --> @@ -50,6 +50,22 @@ android:host="auth" android:path="/callback" /> + + + + + + + +