UC-007: Autenticarse mediante Código QR
Descripción
Una aplicación externa escanea el código QR de identidad del usuario (mostrado en UC-006) e inicia un challenge de autenticación. La wallet recibe el challenge mediante un deep link (almena://auth?challenge=...), muestra una pantalla de consentimiento donde el usuario puede aprobar o rechazar, y si se aprueba, firma el challenge con la clave privada Ed25519 y envía la respuesta firmada a la URL de callback de la aplicación solicitante.
Actores
- Usuario Final: Persona que aprueba o rechaza la solicitud de autenticación en la wallet
- Aplicación Externa: App web, servicio u otra wallet que escanea el código QR e inicia el challenge
- Backend API: Backend de la plataforma que crea el challenge de autenticación y valida la respuesta firmada
- Wallet (Frontend): Aplicación Svelte que gestiona el deep link, la UI de consentimiento y el flujo de respuesta
- Wallet (Rust Backend): Comandos Tauri que gestionan el almacenamiento del challenge, la firma Ed25519 y la construcción de la respuesta
Precondiciones
- El usuario tiene una identidad creada en la wallet (UC-001)
- La wallet está en ejecución (primer plano o segundo plano)
- El código QR de identidad ha sido escaneado por una aplicación externa
- El
tauri-plugin-deep-linkestá configurado para manejar URLsalmena:// - La clave privada Ed25519 está almacenada en el keychain del sistema
Flujo Principal
- La aplicación externa escanea el código QR de identidad del usuario y extrae el DID del payload JSON
- La aplicación externa envía el DID al backend API para iniciar una solicitud de autenticación
- El backend crea un challenge de autenticación que contiene:
challenge_id: identificador únicononce: valor aleatorio para prevenir replaytimestamp: cuándo se creó el challengeexpires_at: cuándo expira el challengeorigin: identificador de la aplicación solicitantecallback_url: a dónde enviar la respuesta firmadarequested_proof: qué se está solicitando
- El backend codifica el challenge como JSON en base64url y construye un deep link:
almena://auth?challenge=<challenge_codificado_base64url> - El deep link se entrega a la wallet (mediante el manejador de URLs del SO)
- El
tauri-plugin-deep-linkde la wallet captura la URLalmena://y disparaon_open_url - La wallet llama a
handle_deep_link()que parsea la URL auth::parse_auth_deep_link()extrae el parámetrochallenge, decodifica el JSON base64url y deserializa la estructuraAuthRequest- El challenge se almacena en el mutex
PENDING_AUTH_REQUEST(estado global en Rust) - El frontend de la wallet detecta la solicitud pendiente y muestra el componente AuthConsent mostrando:
- El origen solicitante
- La acción/prueba solicitada
- Un temporizador de cuenta atrás mostrando el tiempo restante hasta la expiración (formato:
MM:SS) - Botones Aprobar y Rechazar
- El usuario hace clic en Aprobar
- La wallet invoca el comando Rust
approve_auth_request(did):- Recupera la clave privada Ed25519 del keychain del sistema
- Construye el payload a firmar:
{
"challenge_id": "...",
"nonce": "...",
"timestamp": "...",
"expires_at": "...",
"origin": "..."
} - Firma los bytes del payload con Ed25519:
signing_key.sign(payload_bytes) - Construye el
AuthResponse:{
"challenge_id": "...",
"did": "did:almena:...",
"signature": "<firma_ed25519_base64url>",
"signed_payload": "<payload_base64url>",
"verification_method": "did#key-1",
"timestamp": "..."
}
- La wallet envía por POST el
AuthResponsea lacallback_urldel challenge original - El backend verifica la firma contra la clave pública del DID, valida el nonce y los timestamps, y completa la autenticación
- La pantalla de consentimiento se cierra y el usuario vuelve a la wallet
Flujos Alternativos
FA-1: El usuario rechaza la solicitud
- En el paso 11, el usuario hace clic en Rechazar
- La wallet llama a
reject_auth_request()que limpia elPENDING_AUTH_REQUEST - No se envía respuesta a la callback URL
- La pantalla de consentimiento se cierra
FA-2: Challenge expirado
- En el paso 10, la cuenta atrás llega a cero antes de que el usuario actúe
- La pantalla de consentimiento transiciona a un estado "expirado"
- El botón de aprobar se deshabilita
- El usuario solo puede descartar la pantalla
FA-3: Deep link recibido con la wallet bloqueada
- En el paso 6, si la sesión de la wallet está bloqueada, el usuario debe primero desbloquear (mediante contraseña o biometría, ver UC-005)
- Tras desbloquear, se muestra la solicitud de autenticación pendiente
FA-4: Callback URL inaccesible
- En el paso 13, si el POST a la callback URL falla, se muestra un error al usuario
- La respuesta firmada fue generada pero no pudo ser entregada
FA-5: Clave privada no encontrada
- En el paso 12, si la clave privada Ed25519 no se encuentra en el keychain, la operación falla con un error
Postcondiciones
- El challenge de autenticación ha sido firmado y enviado a la aplicación solicitante
- El
PENDING_AUTH_REQUESTse ha limpiado - No hay cambios de estado persistente en la wallet — el flujo de autenticación es sin estado
- La aplicación externa puede verificar la identidad del usuario usando la respuesta firmada
Módulos Involucrados
| Módulo | Rol |
|---|---|
| wallet (frontend) | Componente UI AuthConsent, detección de deep link, temporizador de cuenta atrás, POST de respuesta al callback |
| wallet (Rust backend) | Parseo de deep link (auth::parse_auth_deep_link), almacenamiento del challenge (mutex PENDING_AUTH_REQUEST), firma Ed25519 (sign_challenge), construcción de respuesta |
| backend | Crea challenges de autenticación, valida respuestas firmadas, completa la autenticación |
Notas Técnicas
- Protocolo de deep link:
almena://auth?challenge=<base64url>— manejado portauri-plugin-deep-link - Almacenamiento del challenge: Una única solicitud pendiente almacenada en un
Mutex<Option<AuthRequest>>en Rust. Un nuevo challenge sobrescribe cualquier pendiente existente - Firma Ed25519: El payload del challenge se firma como bytes crudos usando la clave privada Ed25519 del keychain del sistema. La firma y el payload se codifican en base64url en la respuesta
- Método de verificación: La respuesta referencia
did#key-1como método de verificación, coincidiendo con la clave en el DID Document (si está anclado, ver UC-003) - Sin escaneo QR en la wallet: La wallet NO escanea códigos QR. Solo los muestra. El escaneo lo realizan aplicaciones externas
- Flujo sin estado: Cada autenticación es independiente. No se persisten sesiones ni tokens en la wallet después de enviar la respuesta