ADR-011 — Estrategia de deep links¶
| Campo | Valor |
|---|---|
| Estado | Aceptado |
| Fecha | 18 de febrero de 2026 |
| Decisores | Equipo Custodiam (Marcos Val Sanz, Rodrigo Mulero García) |
Contexto¶
Custodiam necesita que ciertos enlaces, recibidos desde fuera de la aplicación, abran la app móvil instalada (o la PWA cuando la app nativa no lo está) en lugar de un navegador web genérico. Tres familias de enlaces concentran esta necesidad:
- Callback del flujo OAuth/OIDC con Keycloak. Tras autenticarse en
auth.custodiam.es, el servidor de identidad redirige al cliente con unauthorization codeque la app debe canjear por tokens. La redirección ocurre dentro del contenedor de autenticación que la propia app inicia (Chrome Custom Tab en Android oASWebAuthenticationSessionen iOS). - Enlaces dentro de emails transaccionales generados por Keycloak. Recuperación de contraseña y verificación de email contienen un token de un solo uso que la app debe procesar para completar la operación.
- Enlaces dentro de notificaciones push y enlaces compartidos entre usuarios. Deep links a entidades específicas del modelo (
/servicio/{id},/emergencia/{id}) cuyo destino es la pantalla concreta de esa entidad.
Android (desde 6.0) e iOS (desde 9.0) ofrecen dos mecanismos para resolver el problema, con perfiles muy distintos:
| Mecanismo | Declaración en la app | Verificación de propiedad del dominio | Manifiesto del dominio |
|---|---|---|---|
| Custom URL scheme | CFBundleURLSchemes en Info.plist (iOS) o intent-filter con android:scheme (Android) |
Ninguna — cualquier app instalada puede declarar cualquier scheme | No requiere |
| App Links / Universal Links HTTPS | applinks: entitlement (iOS) o intent-filter con android:autoVerify="true" (Android) |
Criptográfica — el sistema operativo descarga un manifiesto en el dominio y comprueba que su contenido autoriza al binario firmado de la app | apple-app-site-association (iOS) y assetlinks.json (Android) servidos en /.well-known/ con Content-Type: application/json |
La decisión a tomar: ¿qué mecanismo se usa en cada familia?
Decisión¶
| Familia de enlaces | Plataforma | Mecanismo | URL canónica |
|---|---|---|---|
| Callback OAuth (Keycloak) | Android e iOS | Custom URL scheme | es.custodiam://callback |
| Callback OAuth (Keycloak) | Flutter Web | Ruta interna de la PWA (no aplica AASA / assetlinks) |
https://app.custodiam.es/callback, interceptada por go_router |
| Email "recuperar contraseña" y "verificar email" (Keycloak) | Android, iOS y Web | App Link / Universal Link HTTPS verificado | https://app.custodiam.es/reset-password, /verify-email |
| Deep links de notificaciones push y enlaces compartidos | Android, iOS y Web | App Link / Universal Link HTTPS verificado | https://app.custodiam.es/servicio/*, /emergencia/* |
Justificación¶
-
Los clientes de email strippean los custom schemes; los contenedores OAuth no. Es el argumento operativo principal. Gmail, Outlook, Apple Mail y otros clientes de correo de cuota relevante eliminan o desactivan los enlaces con esquemas no estándar (
whatever://) por razones de seguridad anti-phishing — un enlacebank://login?token=...podría secuestrar una aplicación maliciosa instalada por el usuario sin que este lo perciba. La consecuencia práctica es que, si Keycloak enviase emails cones.custodiam://reset-password?token=..., los usuarios verían un enlace inerte o ausente y el flujo se rompería. El callback de OAuth, en cambio, nunca atraviesa un cliente de email: vive dentro de un Chrome Custom Tab oASWebAuthenticationSessionque el propio sistema operativo proporciona como contenedor del flujo de autenticación, contenedores diseñados específicamente para capturar custom schemes y devolver el control a la app que los inició. -
PKCE neutraliza la vulnerabilidad clásica del custom scheme en OAuth. La objeción histórica al uso de custom schemes en OAuth es el riesgo de scheme hijacking: una app maliciosa instalada antes que la nuestra podría declarar el mismo scheme y, en sistemas operativos que muestren un selector de aplicaciones, recibir el
authorization codelegítimo. Si después intercambiase ese code por tokens, conseguiría suplantar la sesión. Este vector está neutralizado en Custodiam porque el cliente OAuth tiene PKCE S256 obligatorio (ADR-010). El intercambiocode → tokensexige presentar elcode_verifieroriginal, un secreto generado dinámicamente y mantenido en memoria por el cliente que originó el flujo. Una app maliciosa que capturara el code no tendría manera de adivinarlo, y Keycloak rechazaría la petición coninvalid_grant. RFC 8252 §8.1 reconoce explícitamente esta combinación (custom scheme con dominio propio del fabricante + PKCE obligatorio) como aceptable para native apps. -
App Links y Universal Links sobre HTTPS introducen fricción operacional grave en desarrollo móvil local. Las verificaciones criptográficas exigen tres elementos no triviales:
- Certificado HTTPS público firmado por una autoridad certificadora reconocida por el sistema operativo. Certificados autofirmados,
mkcertlocal o autoridades certificadoras propias no son válidos para Universal Links en iOS. - Archivo de manifiesto (
assetlinks.jsonoapple-app-site-association) servido sobre ese HTTPS público en la ruta canónica/.well-known/, con MIME type correcto. - Cache de verificación propio del sistema operativo. Cada cambio en el manifiesto requiere desinstalar y reinstalar la app en el dispositivo o emulador para forzar la re-validación.
Esta fricción es asumible para enlaces outbound (emails y notificaciones, que en local se simulan con dominio público de pruebas o se aceptan como flujo no validado localmente). Pero sería un coste recurrente fijo para todo desarrollo iOS/Android de OAuth, que es uno de los flujos más ejercitados durante el ciclo de desarrollo del frontend. El custom scheme funciona sin red pública, sin DNS, sin certificados y sin reinstalaciones — declararlo en el manifiesto nativo de cada plataforma es suficiente.
- Certificado HTTPS público firmado por una autoridad certificadora reconocida por el sistema operativo. Certificados autofirmados,
-
La asimetría plataforma móvil ↔ plataforma web es natural, no es divergencia. En Flutter Web, el
redirect_uridel cliente OAuth ya es HTTPS (https://app.custodiam.es/callbacken producción) porque el callback es una ruta interna del propio PWA: el navegador la sirve mediante navegación habitual ygo_routerla intercepta. Aquí no interviene el sistema operativo móvil; AASA yassetlinks.jsonson irrelevantes para una SPA que se navega a sí misma. La elección del custom scheme en móvil no genera divergencia conceptual con la rama web: cada plataforma usa el mecanismo nativo más simple para su entorno, y el cliente OAuth de Keycloak acepta los tresredirect_urideclarados (móvil custom scheme + web localhost de desarrollo + web HTTPS de producción). -
Los enlaces outbound a la app no incluyen nunca el callback OAuth. El callback se produce siempre como continuación inmediata del flujo de autenticación iniciado desde dentro de la app. Nunca llega al usuario por canal asíncrono: ningún email, push, enlace compartido ni mensaje en otra app va a contener un
/callback?code=.... En consecuencia, ningún flujo realista justifica que/callbackesté declarado en los manifiestosapple-app-site-associationoassetlinks.json. Si se incluyera, sería un path que el sistema operativo verificaría sin que ningún emisor lo use jamás — declaración muerta que invita a confusión sobre qué partes del flujo de auth están migradas y cuáles no.
Alternativas evaluadas y descartadas¶
A. Custom scheme también para emails y notificaciones¶
Era el estado original del proyecto en la fase de fundación.
- Pros: simplicidad operativa, sin manifiestos
/.well-known/ni certificados. - Contras: argumento 1 (strippeo en clientes de correo) lo invalida para email. Notificaciones push y enlaces compartidos también pueden pasar por canales externos (Telegram, WhatsApp) que aplican el mismo strippeo.
- Descartado por: no escala al canal email ni a enlaces compartidos.
B. App Link HTTPS también para el callback OAuth¶
- Pros: paridad de mecanismo en todos los enlaces.
- Contras: argumentos 3 (fricción en desarrollo local) y 5 (canal de entrega del callback es OAuth-contained, no outbound). El intercambio en seguridad no compensa: PKCE ya elimina el vector que App Link aportaría como mejora.
- Descartado por: coste operativo desproporcionado para una protección que PKCE ya garantiza.
C. División parcial: custom scheme para algunos emails y HTTPS para otros¶
Por ejemplo, custom scheme para reset-password (porque "el usuario ya sabe lo que ha pedido") y HTTPS para verify-email.
- Pros: optimizaría el flujo de reset.
- Contras: inconsistencia operacional — la división correcta es por canal de entrega (Chrome Custom Tab contained vs cualquier canal outbound), no por el tipo concreto de email.
- Descartado por: invertiría la lógica del argumento 1.
Implicaciones operativas¶
- OAuth móvil funciona en cualquier entorno de desarrollo sin red pública ni certificados — solo manifiestos nativos (
Info.plistyAndroidManifest.xml). Coste cero por iteración. - Emails y notificaciones requieren
app.custodiam.esaccesible públicamente con HTTPS válido. En desarrollo local se simulan apuntando a un dominio público de pruebas, o se acepta que ese flujo concreto no se valide localmente. En producción lo cubre la PWA servida con los manifiestos en/.well-known/. - El cliente OAuth en Keycloak declara tres
redirect_uriseparados y disjuntos:es.custodiam://callbackpara móvil,http://localhost:3000/callbackpara web de desarrollo yhttps://app.custodiam.es/callbackpara web de producción. Cualquier nuevo dominio que sirva la PWA requiere registro adicional comoredirect_uriválido. - Los manifiestos
apple-app-site-associationyassetlinks.jsondeclaran exclusivamente los paths de canal outbound:/reset-password,/verify-email,/servicio/*,/emergencia/*. No declaran/callback, coherente con esta decisión. - Las plantillas de email en Keycloak usan
https://app.custodiam.es/...como URL base de los enlaces. Elredirect_uridel custom scheme se mantiene en la configuración del cliente Keycloak indefinidamente: cumple solo el flujo OAuth, no los flujos de email.
Referencias¶
- RFC 8252 — OAuth 2.0 for Native Apps — establece PKCE obligatorio (§7.2) y reconoce el uso de custom schemes con dominio propio del fabricante (§7.1) como aceptable para aplicaciones nativas.
- Apple Developer — Supporting Associated Domains — requisitos de servido del
apple-app-site-association, cache de verificación e integración con Xcode. - Google Developers — Verify Android App Links — especificación del
assetlinks.json, verificación conpm verify-app-links, casos de fallback. - ADR-010 OAuth + PKCE + Keycloak — establece PKCE S256 obligatorio en el cliente, base del argumento 2.