UC-009: Login with Mobile Wallet
Description
The user logs into the web portal using the Almena Network wallet installed on a mobile device. The portal requests an authentication challenge from the backend, generates a QR code encoding a deep link (almena://auth?challenge=...), and displays it on screen. The user scans the QR with the mobile wallet, approves the challenge, and the wallet signs and sends the response to the backend. The QR auto-rotates every 30 seconds, generating a fresh challenge each time. The portal detects completion via polling.
Actors
- End User: Person logging into the web portal using their mobile device
- Frontend (Portal): Next.js web application displaying the QR code and polling for result
- Backend API: FastAPI service managing challenges, signature verification, user management, and JWT issuance
- Wallet (Mobile): Tauri mobile application scanning the QR code and signing the challenge
- Blockchain (optional): Consulted for DID Document resolution during signature verification
Preconditions
- The user has an identity created in the mobile wallet (UC-001)
- The mobile wallet is installed and the camera is accessible
- The backend API is accessible from the portal
- The portal is loaded in the browser at the login page (
/[locale]/login)
Main Flow
- The user navigates to the login page and clicks the Almena Network login button
- The portal calls
POST /api/v1/auth/challengewith the portal's origin URL - The backend generates a challenge (same as UC-008):
challenge_id,nonce,expires_at(5 minutes),callback_url,requested_proof - The backend returns the
ChallengeResponseto the portal - The portal attempts desktop wallet delivery (HTTP + deep link). If both fail (expected when there is no desktop wallet), the portal proceeds to display the QR code
- The portal constructs the deep link URL:
almena://auth?challenge=<base64url>&callback=<callback_url>&origin=<origin> - The portal renders a QR code from the deep link URL using the
qrcode.reactlibrary (QRCodeSVG component) - The portal starts polling
GET /api/v1/auth/status/{challenge_id}every 2 seconds - The portal starts a QR rotation timer: every 30 seconds, it requests a new challenge from the backend, generates a new QR code, and starts polling the new challenge (stops polling the old one)
- The user opens the mobile wallet and scans the QR code with the device camera
- The mobile wallet decodes the QR, extracts the
almena://authdeep link, and parses the challenge - The wallet displays the AuthConsent screen showing the origin, requested proof, and countdown timer
- The user taps Approve
- The wallet signs the challenge with Ed25519 and POSTs the
AuthResponseto thecallback_url - The backend verifies the signature, consumes the challenge, finds or creates the user, and generates a JWT (same verification flow as UC-008, steps 15-19)
- The portal's next polling request detects
status: "completed"withaccess_token - The portal stores
almena_token,almena_user_id,almena_didin localStorage - The portal redirects to
/dashboard
Alternative Flows
AF-1: User rejects the challenge
- At step 13, the user taps Reject
- The wallet calls
POST /api/v1/auth/reject/{challenge_id} - The portal's polling detects
status: "rejected" - The portal shows: "The authentication request was declined in the wallet."
AF-2: QR rotates before scanning
- At step 9, the 30-second rotation timer fires before the user scans
- The portal requests a new challenge from the backend
- A new QR code is rendered replacing the old one
- Polling switches to the new challenge_id
- The old challenge remains
PENDINGuntil it expires (5 minutes) and is cleaned up
AF-3: Challenge expires
- The user does not scan or respond within 5 minutes
- The portal's polling timeout fires
- The portal shows: "The authentication request has expired. Please try again."
AF-4: Camera permission denied
- At step 10, the mobile device denies camera access
- The user cannot scan the QR and must grant camera permission or use an alternative method
AF-5: Desktop wallet also available
- At step 5, the desktop wallet delivery succeeds
- Both the desktop wallet consent and the QR code are available simultaneously
- The first approval (from either device) completes the challenge
- The portal polling detects completion regardless of which wallet responded
AF-6: User scans expired QR
- The user scans a QR code that was already rotated
- The old challenge may still be
PENDING(within 5 minutes) - The wallet submits the response for the old challenge_id
- The backend verifies it normally if still valid
- However, the portal may have already switched to polling a newer challenge_id and won't detect it
- The user should scan the currently displayed QR
Postconditions
- The user is authenticated in the portal with a valid JWT
almena_token,almena_user_id, andalmena_didare stored in localStorage- A user record exists in the backend database for this DID
- The challenge is consumed and cannot be reused
Modules Involved
| Module | Role |
|---|---|
| frontend | Login page UI, challenge request, QR code generation (qrcode.react), 30-second QR rotation, polling, session storage, redirect |
| backend | Challenge creation and storage (in-memory), signature verification (Ed25519/PyNaCl), user management, JWT issuance, polling endpoint |
| wallet (mobile) | QR scanning, deep link parsing, AuthConsent UI, Ed25519 signing, POST response to callback |
| blockchain | DID Document resolution (optional) |
Technical Notes
- QR rotation: Every 30 seconds a completely new challenge is created. This means a new
POST /api/v1/auth/challengecall, new QR image, and polling switches to the new challenge_id. Old challenges expire naturally - QR content: The QR encodes the full
almena://auth?challenge=...deep link URL, not just the challenge data. This allows the mobile OS to route it directly to the wallet app - QR library:
qrcode.react(QRCodeSVG component) renders the QR as SVG in the browser - Dual path: Both desktop delivery (HTTP + deep link) and QR display happen simultaneously. The QR appears in the "waiting" state while the portal also tries the desktop wallet. Whichever wallet responds first wins
- Polling vs rotation: Polling interval is 2 seconds for status checks. QR rotation interval is 30 seconds for fresh challenges. These are independent timers
- No WebSocket: Status is checked via polling only. No push mechanism from backend to frontend