Use a reverse-domain redirect scheme for the native app

- Android/iOS now return to cat.ikerdemo.musify:/callback instead of
  musify://callback: a private-use scheme based on a domain we own
  (RFC 8252 section 7.1), so another app is unlikely to claim it.
- The Musify Native app no longer needs devMode: loopback and custom
  scheme redirects are both valid for a native app.
- zitadel-init also updates an existing app's config, so the change
  reaches instances created before it (the client id is kept).
- iOS takes the scheme from everything before the first ':', since the
  new URI has a single slash.
This commit is contained in:
2026-09-24 15:06:25 +02:00
parent c33b0e8bfd
commit 7b8f3f1222
9 changed files with 43 additions and 28 deletions
+3 -3
View File
@@ -54,7 +54,7 @@ al patrón que ya usa Scalar para probar la API — ver `deploy/compose.yml` (`z
Redirect URIs registrados:
- Desktop: `http://127.0.0.1:17453/callback` (RFC 8252, loopback con puerto fijo).
- Android / iOS: `musify://callback` (custom scheme).
- Android / iOS: `cat.ikerdemo.musify:/callback` (custom scheme con dominio invertido, RFC 8252 §7.1).
`mobile/gradle.properties` controla a qué backend apunta Android/Desktop:
@@ -125,13 +125,13 @@ Kotlin/Native (`iosMain/`) sigue las APIs estándar de interop (`AVFoundation`,
1. Genera un proyecto Xcode con el asistente "Kotlin Multiplatform" de Android Studio, o crea uno vacío
e importa `composeApp` como framework (`./gradlew :composeApp:embedAndSignAppleFrameworkForXcode`).
2. Registra el scheme `musify` en `Info.plist`:
2. Registra el scheme `cat.ikerdemo.musify` en `Info.plist`:
```xml
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>musify</string></array>
<array><string>cat.ikerdemo.musify</string></array>
</dict>
</array>
```
@@ -18,7 +18,9 @@
</intent-filter>
</activity>
<!-- Catches the `musify://callback` redirect Zitadel sends after login/register.
<!-- Catches the `cat.ikerdemo.musify:/callback` redirect Zitadel sends after
login/register. The URI has no host, so the filter matches on the scheme
alone.
See BrowserAuthLauncher.android.kt and AppConfig.nativeRedirectUri. -->
<activity
android:name=".auth.AuthCallbackActivity"
@@ -29,7 +31,7 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="musify" android:host="callback" />
<data android:scheme="cat.ikerdemo.musify" />
</intent-filter>
</activity>
</application>
@@ -9,5 +9,5 @@ actual object AppConfig {
// Matches the AuthCallbackActivity intent-filter in AndroidManifest.xml and
// the redirect URI registered for the "Musify Native" app in Zitadel.
actual val nativeRedirectUri: String = "musify://callback"
actual val nativeRedirectUri: String = "cat.ikerdemo.musify:/callback"
}
@@ -5,7 +5,7 @@ import android.content.Intent
import android.os.Bundle
/**
* Declared in AndroidManifest.xml with an intent-filter for `musify://callback`.
* Declared in AndroidManifest.xml with an intent-filter for `cat.ikerdemo.musify:/callback`.
* Chrome Custom Tabs redirects here once Zitadel finishes the login/register
* flow; this activity just forwards the URL to [AndroidAuthBridge] and closes.
*/
@@ -9,7 +9,8 @@ package com.musify.app
* `nativeRedirectUri` is the OAuth redirect registered for the "Musify
* Native" Zitadel app (see deploy/compose.yml, zitadel-init): a fixed
* loopback port on Desktop (RFC 8252 native-app loopback flow), and the
* `musify://callback` custom scheme on Android/iOS.
* `cat.ikerdemo.musify:/callback` reverse-domain custom scheme on Android/iOS
* (RFC 8252 §7.1: based on a domain we own, so no other app plausibly claims it).
*/
expect object AppConfig {
val apiBaseUrl: String
@@ -12,7 +12,7 @@ package com.musify.app.auth
* - Desktop: a one-shot loopback HTTP server on the fixed port registered in
* Zitadel (RFC 8252 §7.3), opened via the OS default browser.
* - Android: Chrome Custom Tabs; the redirect is caught by
* [AuthCallbackActivity] via the `musify://callback` intent filter.
* [AuthCallbackActivity] via the `cat.ikerdemo.musify:/callback` intent filter.
* - iOS: `ASWebAuthenticationSession`, which owns the whole round trip.
*/
expect class BrowserAuthLauncher() {
@@ -12,5 +12,5 @@ actual object AppConfig {
// Matches CFBundleURLTypes in Info.plist and the redirect URI registered
// for the "Musify Native" app in Zitadel.
actual val nativeRedirectUri: String = "musify://callback"
actual val nativeRedirectUri: String = "cat.ikerdemo.musify:/callback"
}
@@ -23,14 +23,15 @@ private class PresentationContextProvider : NSObject(), ASWebAuthenticationPrese
/**
* `ASWebAuthenticationSession` owns the whole round trip: it opens a system
* browser sheet at [authorizeUrl] (Zitadel's hosted login, with its
* "Regístrate" link) and intercepts the `musify://` redirect itself, no app
* "Regístrate" link) and intercepts the `cat.ikerdemo.musify:` redirect itself, no app
* delegate / `onOpenURL` wiring required.
*/
@OptIn(ExperimentalForeignApi::class)
actual class BrowserAuthLauncher actual constructor() {
actual suspend fun launch(authorizeUrl: String, redirectUri: String): String =
suspendCancellableCoroutine { continuation ->
val scheme = redirectUri.substringBefore("://")
// `scheme:/path` (RFC 8252 private-use scheme): everything before the first ':'.
val scheme = redirectUri.substringBefore(":")
val contextProvider = PresentationContextProvider()
val session = ASWebAuthenticationSession(