Cross-Device Authentication (QR Login)
Enable users to sign in on a desktop device by scanning a QR code with their mobile phone (where their passkey is stored).
TryMellon provides a seamless, zero-friction flow for cross-device authentication without needing WebSockets or complex state synchronization on your end.
1. Desktop: Initialize and Show QR
On the desktop interface (e.g., your login page), call init() to create a cross-device session. This returns a session_id and a qr_url.
approval_context (auth-link): To show a custom message on the mobile screen (e.g. “Access to orders”), send it when creating the session. The API accepts
POST /v1/auth/cross-device/initwith body{ "approval_context": "Your message" }(max 200 chars; use the sameAuthorization: Bearer <publishableKey>andOriginas for the SDK). The SDKinit()does not yet accept options; use the API directly for approval_context until the SDK exposes it.
import { TryMellon } from '@trymellon/js'
// 1. Create TryMellon Client
const clientResult = TryMellon.create({ appId: 'YOUR_APP_ID', publishableKey: 'cli_xxxx' });
if (!clientResult.ok) throw clientResult.error;
const client = clientResult.value;
// 2. Initialize cross-device session
const initResult = await client.auth.crossDevice.init()
if (!initResult.ok) {
console.error("Failed to init QR flow:", initResult.error.message);
return;
}
const { session_id, qr_url } = initResult.value;
// 3. Render the QR code
// You can use libraries like 'qrcode.react', 'svelte-qrcode', or 'qrcode' to render 'qr_url'
renderQrCode(qr_url);
// 4. Start polling for approval (Desktop waits here)
const controller = new AbortController()
const pollResult = await client.auth.crossDevice.waitForSession(
session_id,
controller.signal
)
if (!pollResult.ok) {
if (pollResult.error.code === 'TIMEOUT') {
console.error('QR code expired. Please refresh the page.')
}
return;
}
// 5. Success! Send the sessionToken to your backend
console.log('Session token:', pollResult.value.sessionToken)
[!WARNING] About
qr_urland the mobile app: The API returnsqr_urlin the form{baseUrl}/mobile-auth?session_id={session_id}. Your mobile web app must be deployed and its URL/origin allowed in the TryMellon dashboard for that application.
2. Mobile: Auth-link screen and approve
When the user scans the QR code, they are sent to your mobile page (e.g. /mobile-auth?session_id=...). The auth-link flow uses the same API and adds binding transaccional:
- GET context: The mobile page calls
client.auth.crossDevice.getContext(sessionId)to obtain session type, WebAuthn options, and optionallyapproval_contextandapplication_namefrom the backend. - Above the fold: The screen shows title “Confirmar identidad”, the client/application name, and when present the
approval_context(what action is being authorized), then the consequence phrase and the CTA (“Usar passkey”). This follows the contract indocs/epic-auth-ia-link/contrato-approval-context-y-copy.md. - Approve: When the user taps the CTA, call
approve(sessionId)as below. On success, show “Listo. Ya puedes cerrar esta ventana.” On 410/404 or no passkey, show the contract messages and recovery/register links where applicable.
3. Mobile: Approve login (code)
On your mobile page (e.g., /mobile-auth), extract the ID and trigger the approval.
// 1. Extract session_id from URL
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get('session_id');
if (!sessionId) {
console.error("No session ID found in URL.");
return;
}
// 2. Create the TryMellon client (same as desktop)
const clientResult = TryMellon.create({ appId: 'YOUR_APP_ID', publishableKey: 'cli_xxxx' });
if (!clientResult.ok) throw clientResult.error;
const client = clientResult.value;
// 3. Trigger WebAuthn approval on mobile
const approveResult = await client.auth.crossDevice.approve(sessionId)
if (approveResult.ok) {
// Passkey validated. Show contract success message (e.g. "Listo. Ya puedes cerrar esta ventana.").
alert('Process complete! You are now logged in on your desktop.')
} else {
alert('Failed to approve login: ' + approveResult.error.message)
}
Compatibilidad con RFC 8628 (Device Authorization Grant)
El flujo cross-device de TryMellon es conceptualmente similar al Device Authorization Grant (RFC 8628). Para integradores que ya usan device codes, este es el mapeo:
| RFC 8628 | TryMellon cross-device |
|---|---|
device_code | session_id (UUID de la sesión) |
verification_uri | Base URL del landing (ej. https://app.trymellon.com) |
verification_uri_complete | qr_url (URL completa con ?session_id=... para la página mobile-auth) |
Poll POST /token con device_code | Poll GET /v1/auth/cross-device/status/:sessionId |
access_token en respuesta al poll | session_token en el body cuando status === 'completed' |
expires_in (device_code) | Session in Redis with TTL (e.g. 5 min); expires_at in init |
TryMellon no expone user_code ni interval; el cliente debe hacer polling con un intervalo razonable (ej. 2–3 s) y respetar un timeout global de 2–3 minutos.
QR Default Domain (Bridge)
Don’t have a /mobile-auth page yet? Use the QR default domain — TryMellon hosts the mobile approval screen for you.
With the bridge domain, init() returns a qr_url pointing to TryMellon’s hosted page. Your desktop app only needs the SDK; no mobile page deployment required.
When to use it
- Getting started fast: ship cross-device auth without building a mobile-auth page.
- POCs and demos: show the QR flow to stakeholders in minutes.
- Production with custom domain later: switch by changing
primary_qr_base_url— no user migration.
Migrating to your own domain
- Deploy your own
/mobile-authpage (see sections above for the mobile approval code). - In your app settings, set
primary_qr_base_urltohttps://yourdomain.com. - Add
https://yourdomain.comto Allowed origins.
Existing passkeys keep working — no re-registration needed.
[!TIP] For a complete integration guide with code snippets, dashboard config, and framework examples, see QR Default Domain — Integration Guide.
Under the Hood
init(): Creates a temporary challenge in the TryMellon Redis cache (valid for 5 minutes).waitForSession(): Uses resilient HTTP Long-Polling. It doesn’t overload the network and will gracefully timeout if the user abandons the process.approve(): Leverages the user’s mobile platform authenticator (FaceID / TouchID) to sign the challenge, converting it into a fullsessionTokenthat the desktop immediately receives.