Aller au contenu

Connexion dans le navigateur

Ouvrir votre page de connexion dans le navigateur du téléphone (passkeys, trousseau).

Dans une vue web intégrée à une application, iOS et Android n’autorisent les passkeys (WebAuthn, Face ID, empreinte digitale) et le remplissage automatique des mots de passe que pour les domaines explicitement associés à l’application. Votre domaine ne l’est pas. Résultat, dans l’écran du lien :

  • le bouton de connexion biométrique de votre site n’apparaît pas ou échoue ;
  • le trousseau propose les identifiants de l’application de la commune, pas ceux de votre site.

La fonction « Connexion dans le navigateur » contourne cette limite sans rien déclarer dans l’application : votre page de connexion s’ouvre dans le navigateur du système, par-dessus l’application, où tout fonctionne comme d’habitude. Une fois l’utilisateur connecté, votre page redirige vers une URL de rappel, la feuille se ferme et votre page dans l’application reprend la main.

Plateforme Navigateur utilisé
iPhone Feuille Safari (ASWebAuthenticationSession), qui partage les cookies, passkeys et mots de passe de Safari.
Android Onglet du navigateur par défaut (Auth Tab, ou Custom Tab si le navigateur ne connaît pas l’Auth Tab), qui partage les cookies et identifiants de ce navigateur.

Sur iPhone, le système affiche d’abord une alerte qui demande à l’utilisateur d’autoriser l’application à utiliser votre site pour se connecter. S’il refuse, open() est rejetée avec CANCELLED.

La case « Connexion dans le navigateur » doit être cochée sur la carte lien. mairies.auth n’existe que dans ce cas.

Propriété ou méthode Description
mairies.auth.callbackUrl URL de rappel propre à l’application, par exemple net.mairies.macommune.webauth://callback. Lecture seule.
mairies.auth.open(url) Ouvre url dans le navigateur du système. Se résout avec { url } : l’URL de rappel complète, dès que votre page de connexion redirige vers callbackUrl.

L’adresse passée à open() doit être en https et sur la même origine que votre page (même hôte, même port). Sinon, l’appel est rejeté avec NOT_ALLOWED. Si le paramètre manque, l’erreur est INVALID_PARAMS.

La promesse reste en attente pendant toute la connexion, qui peut durer plusieurs minutes. N’y mettez pas de délai d’expiration court.

L’URL de rappel a la forme <identifiant de l’application>.webauth://callback. Elle diffère d’une application à l’autre : deux applications mairies.net installées sur le même téléphone ne reçoivent pas le rappel l’une de l’autre. Lisez-la toujours dans mairies.auth.callbackUrl, ne l’écrivez pas en dur.

Déroulé recommandé :

Écran du lien (votre page) Navigateur du système (votre page de connexion) Votre serveur
────────────────────────── ─────────────────────────────────────────────── ─────────────
state = aléatoire, mémorisé
mairies.auth.open(https://vous/connexion?state=…&return=<callbackUrl>)
l’utilisateur se connecte (passkey, trousseau…)
─── demande un code à usage unique ────────────▶ émet le code (≤ 60 s)
location.replace(<callbackUrl>?code=…&state=…)
◀── promesse résolue : { url } ───────
vérifie state, envoie le code ────────────────────────────────────────────────────────▶ échange code → session
session ouverte dans l’écran du lien

Côté page affichée dans l’application :

async function loginWithSystemBrowser() {
const m = window.mairies;
if (!m?.auth) return false; // hors application ou case non cochée : formulaire habituel
const state = crypto.randomUUID();
sessionStorage.setItem('auth-state', state);
const target = new URL('/connexion-navigateur', location.origin);
target.searchParams.set('state', state);
target.searchParams.set('return', m.auth.callbackUrl);
try {
const { url } = await m.auth.open(target.toString());
const back = new URL(url);
if (back.searchParams.get('state') !== sessionStorage.getItem('auth-state')) {
throw new Error('state invalide');
}
await exchangeCodeForSession(back.searchParams.get('code')); // votre backend
return true;
} catch (error) {
if (error.code === 'CANCELLED') return false; // feuille fermée par l’utilisateur
throw error;
} finally {
sessionStorage.removeItem('auth-state');
}
}

Côté page de connexion, ouverte dans le navigateur, une fois l’utilisateur authentifié :

const params = new URLSearchParams(location.search);
const returnUrl = params.get('return');
// N’acceptez que la forme <…>.webauth://callback
if (!/^[a-z][a-z0-9+.-]*\.webauth:\/\/callback$/.test(returnUrl ?? '')) {
throw new Error('Destination de retour refusée');
}
const { code } = await fetch('/api/one-time-code', { method: 'POST' }).then((r) => r.json());
const callback = new URL(returnUrl);
callback.searchParams.set('code', code);
callback.searchParams.set('state', params.get('state'));
location.replace(callback.toString());

Sécuriser l’échange (code à usage unique et state)

Section intitulée « Sécuriser l’échange (code à usage unique et state) »

Une URL de rappel à schéma personnalisé n’est pas réservée : sur Android notamment, une autre application installée peut tenter de se faire passer pour le rappel. Ces règles rendent une telle tentative inoffensive :

  • Ne mettez jamais de jeton de session dans l’URL de rappel. Transmettez un code à usage unique, valable quelques dizaines de secondes au plus, que votre page échange ensuite contre une session auprès de votre serveur.
  • Générez un state aléatoire avant open(), gardez-le dans la page, et vérifiez-le au retour. Un faux rappel ne le connaît pas.
  • Sur votre page de connexion, n’acceptez comme destination de retour que la forme <…>.webauth://callback. Jamais une URL http(s) ou javascript: fournie par un paramètre : ce serait une redirection ouverte.

De son côté, l’application :

  • refuse d’ouvrir une adresse d’une autre origine que celle du lien ;
  • n’ouvre qu’une connexion à la fois ;
  • ne renvoie l’URL de rappel qu’à la page qui a appelé open(), et seulement si elle porte le schéma de l’application.
Situation Résultat de open()
L’utilisateur ferme la feuille ou l’onglet sans se connecter Rejet avec CANCELLED
L’utilisateur quitte l’écran du lien pendant la connexion Rejet avec CANCELLED (si la page peut encore le recevoir)
Une connexion est déjà en cours Rejet avec BUSY, rien n’est ouvert
Le navigateur du système ne peut pas s’ouvrir Rejet avec UNAVAILABLE
Toute autre erreur du navigateur Rejet avec INTERNAL

Traitez CANCELLED comme un choix de l’utilisateur, pas comme une erreur : revenez simplement à l’écran de connexion. Pour éviter BUSY, désactivez votre bouton de connexion tant que la promesse est en attente.

L’utilisateur est déjà connecté dans son navigateur ? La session du navigateur est partagée (cookies conservés). Votre page de connexion peut donc reconnaître l’utilisateur et rediriger aussitôt vers l’URL de rappel ; la feuille s’ouvre et se referme presque immédiatement.