feat(sso): App Links autoVerify callback + paired-host trust check
All checks were successful
PR Checks / android-build (pull_request) Successful in 20m53s
All checks were successful
PR Checks / android-build (pull_request) Successful in 20m53s
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
This commit is contained in:
@@ -179,4 +179,45 @@ class SsoAuthManagerTest {
|
||||
assertTrue(!mgr.matchesCallback("runicgateway", "auth", "/other"))
|
||||
assertTrue(!mgr.matchesCallback("runicgateway", "evil", "/callback"))
|
||||
}
|
||||
|
||||
// ── App Links (docs/android/APP_LINKS.md) ────────────────────────────────
|
||||
|
||||
@Test fun `matchesAppLinkCallback accepts only https, the app-link path, and the paired host`() {
|
||||
val mgr = managerWith(FakeSsoApi { Response.success(tokenPair()) }, SessionManager(FakeTokenStore()))
|
||||
// Paired to shard.example.com (managerWith default base).
|
||||
assertTrue(mgr.matchesAppLinkCallback("https", "shard.example.com", "/mobile/callback"))
|
||||
// Host-trust: a foreign host is refused even over https + right path.
|
||||
assertTrue(!mgr.matchesAppLinkCallback("https", "evil.example.com", "/mobile/callback"))
|
||||
// Wrong scheme / wrong path.
|
||||
assertTrue(!mgr.matchesAppLinkCallback("http", "shard.example.com", "/mobile/callback"))
|
||||
assertTrue(!mgr.matchesAppLinkCallback("https", "shard.example.com", "/callback"))
|
||||
// Host match is case-insensitive.
|
||||
assertTrue(mgr.matchesAppLinkCallback("https", "SHARD.EXAMPLE.COM", "/mobile/callback"))
|
||||
}
|
||||
|
||||
@Test fun `matchesAppLinkCallback is false before a shard is paired`() {
|
||||
val mgr = managerWith(FakeSsoApi { Response.success(tokenPair()) }, SessionManager(FakeTokenStore()), base = null)
|
||||
assertTrue(!mgr.matchesAppLinkCallback("https", "shard.example.com", "/mobile/callback"))
|
||||
}
|
||||
|
||||
@Test fun `buildStartUrl requests the custom scheme when no app-link host is baked`() {
|
||||
val mgr = managerWith(FakeSsoApi { Response.success(tokenPair()) }, SessionManager(FakeTokenStore()))
|
||||
// Generic build: appLinkHost defaults to BuildConfig.APP_LINK_HOST ("" in tests).
|
||||
val redirect = mgr.buildStartUrl("google")!!.toHttpUrl().queryParameter("redirect_uri")
|
||||
assertEquals(SsoAuthManager.REDIRECT_URI, redirect)
|
||||
}
|
||||
|
||||
@Test fun `buildStartUrl requests the https app-link callback when the baked host matches the paired host`() {
|
||||
val mgr = managerWith(FakeSsoApi { Response.success(tokenPair()) }, SessionManager(FakeTokenStore()))
|
||||
mgr.appLinkHost = "shard.example.com" // white-label build baked this shard's host
|
||||
val redirect = mgr.buildStartUrl("google")!!.toHttpUrl().queryParameter("redirect_uri")
|
||||
assertEquals("https://shard.example.com/mobile/callback", redirect)
|
||||
}
|
||||
|
||||
@Test fun `buildStartUrl falls back to the custom scheme when the baked host does not match the paired shard`() {
|
||||
val mgr = managerWith(FakeSsoApi { Response.success(tokenPair()) }, SessionManager(FakeTokenStore()))
|
||||
mgr.appLinkHost = "other-shard.example.com" // built for a different shard than the paired one
|
||||
val redirect = mgr.buildStartUrl("google")!!.toHttpUrl().queryParameter("redirect_uri")
|
||||
assertEquals(SsoAuthManager.REDIRECT_URI, redirect)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user