Aller au contenu

Passkeys (WebAuthn)

L’Auth Relay courtise les cérémonies WebAuthn / passkey pour chaque tenant (signé HMAC #1). Il valide l’origine du navigateur contre l’allowlist du tenant, puis transmet à auth_api le webauthn_rp_id et le rp_name du tenant — les credentials sont ainsi partitionnés par tenant.

Deux champs (posés au provisionnement ou via /update/tenant) activent les passkeys :

  • webauthn_rp_id — le Relying Party ID, l’eTLD+1 de l’origine web du tenant (ex. example.com). Requis ; ne peut pas franchir une frontière eTLD+1 (example.com couvre app.example.com mais pas example.net).
  • webauthn_origins — l’allowlist d’origines autorisées à exécuter une cérémonie. Liste vide = passkeys désactivées.

Si webauthn_rp_id est absent ou l’origine hors allowlist, les routes de cérémonie renvoient 403.

Quatre routes de cérémonie (paire options/verify) et trois routes de gestion des credentials.

MéthodeCheminRôle
POST/relay/webauthn/register/optionsDémarrer l’enrôlement (creation options).
POST/relay/webauthn/register/verifyFinaliser l’enrôlement (attestation).
POST/relay/webauthn/authenticate/optionsDémarrer la connexion (request options).
POST/relay/webauthn/authenticate/verifyFinaliser la connexion (assertion).
POST/relay/webauthn/fetch/passkeysLister les passkeys de l’utilisateur.
POST/relay/webauthn/rename/passkeyRenommer un passkey.
POST/relay/webauthn/revoke/passkeyRévoquer (soft-delete) un passkey.

browser_origin doit être dans webauthn_origins. relay_user_linked_id est l’identifiant côté relais de l’utilisateur (24 caractères).

Fenêtre de terminal
curl $BASE_URL/api/v1/relay/webauthn/register/options \
-X POST \
-H "X-Bloonio-Tenant-Id: tnt_..." \
-H "X-Bloonio-Timestamp: 1718966400000" \
-H "X-Bloonio-Signature: <hex_hmac_sha256>" \
-H "Content-Type: application/json" \
-d '{
"relay_user_linked_id": "652f1f77bcf86cd799439011",
"username": "jane@example.com",
"display_name": "Jane Doe",
"browser_origin": "https://example.com"
}'

Le data renvoyé contient les PublicKeyCredentialCreationOptions à passer à navigator.credentials.create(). Renvoyez ensuite le session_id et l’attestation à register/verify.

Fenêtre de terminal
curl $BASE_URL/api/v1/relay/webauthn/register/verify \
-X POST \
-H "X-Bloonio-Tenant-Id: tnt_..." \
-H "X-Bloonio-Timestamp: 1718966400000" \
-H "X-Bloonio-Signature: <hex_hmac_sha256>" \
-H "Content-Type: application/json" \
-d '{
"session_id": "sess_abcdef012345",
"attestation": { "id": "...", "rawId": "...", "type": "public-key", "response": {} },
"nickname": "Mon ordinateur",
"browser_origin": "https://example.com"
}'

Symétrique : authenticate/options puis authenticate/verify. À l’étape options, relay_user_linked_id est optionnel — omettez-le pour un flux sans nom d’utilisateur (credential découvrable).

Fenêtre de terminal
curl $BASE_URL/api/v1/relay/webauthn/authenticate/options \
-X POST \
-H "X-Bloonio-Tenant-Id: tnt_..." \
-H "X-Bloonio-Timestamp: 1718966400000" \
-H "X-Bloonio-Signature: <hex_hmac_sha256>" \
-H "Content-Type: application/json" \
-d '{ "browser_origin": "https://example.com" }'
Fenêtre de terminal
curl $BASE_URL/api/v1/relay/webauthn/fetch/passkeys \
-X POST \
-H "X-Bloonio-Tenant-Id: tnt_..." \
-H "X-Bloonio-Timestamp: 1718966400000" \
-H "X-Bloonio-Signature: <hex_hmac_sha256>" \
-H "Content-Type: application/json" \
-d '{ "relay_user_linked_id": "652f1f77bcf86cd799439011" }'

Les passkeys natifs (Android/iOS) exigent que l’app soit déclarée dans les fichiers d’association de la Relying Party bloonio.com. Le relais sert lui-même /.well-known/assetlinks.json (Android) et /.well-known/apple-app-site-association (iOS) ; Caddy route les chemins de l’apex bloonio.com vers le relais.

Ajouter une app à la RP bloonio.com est une étape formelle d’onboarding, en résumé :

  1. Empreinte Android — le SHA-256 de la clé de signature d’app (Play Console → App signing), pas la clé d’upload ni un keystore local.
  2. Identité iOS — Team ID et bundle id canonique, plus l’entitlement Associated Domains webcredentials:bloonio.com.
  3. Compléter l’entrée dans passkey_identities.yaml ; passer la section à enabled: true seulement quand chaque valeur est réelle.
  4. Régénérer (python wellknown/generate_wellknown.py) et committer le YAML et generated/ ensemble (la CI l’impose).
  5. Déployer le relais — les fichiers sont livrés avec le service.
  6. Appliquer les origines tenant sur l’hôte : ./bash/install/apply-passkey-origins.py prod --tenant-id tnt_…. Sans cette étape, les assertions Android natives sont rejetées même avec des .well-known valides.
  7. Valider : ./wellknown/validate_wellknown.sh (avec --google pour interroger l’API Digital Asset Links), à re-exécuter après tout changement Cloudflare.

Le drapeau passkeys_enabled par tenant pilote un déploiement progressif sans jamais verrouiller personne :

ValeurEffet
null (défaut)Activé quand configuré (webauthn_rp_id + webauthn_origins). Tenants vivants inchangés.
falseEnrôlement et connexion refusés (403) — mais la gestion reste disponible.
trueIdentique à null (activation explicite).

Séquence recommandée : onboarder le tenant, le tenir en pause (passkeys_enabled: false) pendant un test interne, puis activer (passkeys_enabled: true). Repli instantané avec {"passkeys_enabled": false}.

Aucun backend de métriques n’est câblé : le relais émet des lignes de log analysables au point de passage multi-tenant (app/modules/webauthn/router.py), à agréger via Loki / CloudWatch / grep :

passkey.metric event=<enroll|signin|revoke> outcome=<ok|fail> tenant=<tnt_…> [reason=<…>]

Les échecs côté client (cancelled, unsupported, no_credential) n’atteignent jamais le serveur — les SDK les exposent comme des erreurs typées ; branchez-les sur votre propre analytique si vous avez besoin des taux d’annulation.