Turnstile For Front¶
Ce document décrit la cible front à implémenter en même temps que la refonte backend antibot.
Objectif:
- supprimer toute dépendance front à
reCAPTCHA - supprimer toute logique front basée sur
recaptchaTokenouturnstileToken - standardiser tous les formulaires publics protégés sur un seul contrat:
challengeToken - lier les challenges Turnstile au bon flow, et quand nécessaire, au bon token métier
Statut: cible à implémenter telle quelle
Règle générale¶
Le front ne doit plus raisonner en "provider captcha".
Le front doit raisonner en:
- "je dois obtenir un challenge pour ce flow"
- "ce flow a éventuellement un binding métier"
- "j'envoie ensuite
challengeTokenà l'API"
Le front ne doit plus envoyer:
recaptchaTokenturnstileToken
Le front doit envoyer partout:
challengeToken
Configuration front requise¶
- ajouter
TURNSTILE_SITE_KEY - exposer un helper unique d'acquisition de challenge
Exemple de shape cible:
type ChallengeFlow =
| "signup_account_v1"
| "contact_us_v1"
| "waitlist_register_v1"
| "waitlist_invite_signup_v1"
| "user_invite_signup_v1"
| "structure_adherent_invite_signup_v1";
type AcquireChallengeInput = {
flow: ChallengeFlow;
binding?: string;
};
type AcquireChallengeResult = {
challengeToken: string;
};
async function acquireChallenge(
input: AcquireChallengeInput,
): Promise<AcquireChallengeResult> {
// wrapper Turnstile
}
Flows Turnstile à utiliser¶
- création de compte:
signup_account_v1 - contact-us:
contact_us_v1 - inscription waitlist:
waitlist_register_v1 - signup depuis invitation waitlist:
waitlist_invite_signup_v1 - signup depuis invitation utilisateur:
user_invite_signup_v1 - signup depuis invitation adhérent structure:
structure_adherent_invite_signup_v1
Chaque écran protégé doit demander un challenge avec le flow exact
correspondant à son endpoint.
Endpoints et payloads cibles¶
Les routes backend cibles à appeler sont:
POST /v1/public/accountsPOST /v1/public/contact-usPOST /v1/public/waitlist-entriesPOST /v1/public/waitlist-invitations/signupPOST /v1/public/user-invitations/signupPOST /v1/public/structure-adherent-invitations/signup
Les routes publiques sans challenge dans ce périmètre restent:
POST /v1/public/waitlist-confirmations/confirmPOST /v1/public/structure-adherent-invitations/resolvePOST /v1/public/structure-adherent-invitations/decline
Payloads cibles:
Création de compte¶
{
"pseudo": "club-alpha",
"email": "contact@club-alpha.test",
"accountType": "pro",
"challengeToken": "token"
}
Contact-us¶
{
"name": "Jane",
"email": "jane@example.com",
"message": "Message de plus de 30 caractères pour le support.",
"challengeToken": "token"
}
Inscription waitlist¶
{
"email": "alice@example.com",
"accountType": "individual",
"cityId": 12,
"sportIds": [1, 2],
"challengeToken": "token"
}
accountType est obligatoire pour toute inscription waitlist. Valeurs
autorisées: individual ou pro. Le backend refuse le payload si le champ est
absent ou invalide; le front ne doit donc pas appliquer de fallback silencieux.
Signup depuis invitation waitlist¶
{
"token": "invite-token",
"pseudo": "alice-alpha",
"challengeToken": "token"
}
Signup depuis invitation utilisateur¶
{
"token": "invite-token",
"pseudo": "alice-alpha",
"challengeToken": "token"
}
Signup depuis invitation adhérent structure¶
{
"token": "invite-token",
"pseudo": "alice-alpha",
"challengeToken": "token"
}
Binding: quand il est obligatoire¶
Le binding sert à lier le challenge Turnstile à un token métier précis.
Pas de binding pour:
- création de compte
- contact-us
- inscription waitlist
Binding obligatoire pour:
- signup invitation waitlist
- signup invitation utilisateur
- signup invitation adhérent structure
Valeur du binding:
base64url(sha256(token))
Important:
- ne jamais envoyer le token métier brut dans
cData - ne pas lowercaser le token
- éviter toute normalisation autre qu'un éventuel
trim()strictement identique au backend
Exemple TypeScript:
async function toBase64UrlSha256(value: string): Promise<string> {
const data = new TextEncoder().encode(value);
const digest = await crypto.subtle.digest("SHA-256", data);
const bytes = Array.from(new Uint8Array(digest));
const base64 = btoa(String.fromCharCode(...bytes));
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
Exemple d'usage:
const binding = await toBase64UrlSha256(token);
const { challengeToken } = await acquireChallenge({
flow: "user_invite_signup_v1",
binding,
});
Règles d'implémentation des écrans¶
Pour chaque submit protégé:
- calculer le
bindingsi nécessaire - appeler
acquireChallenge({ flow, binding }) - envoyer le
challengeTokenreçu dans le payload JSON
Pattern recommandé:
async function onSubmit() {
const binding = token ? await toBase64UrlSha256(token) : undefined;
const { challengeToken } = await acquireChallenge({
flow: "structure_adherent_invite_signup_v1",
binding,
});
await api.structureInvitationSignup({
token,
pseudo,
challengeToken,
});
}
Le front ne doit pas:
- stocker durablement un
challengeToken - réutiliser un même challenge pour plusieurs tentatives
- réutiliser un challenge d'un écran sur un autre écran
- mutualiser un challenge entre plusieurs formulaires
Un challengeToken est jetable et doit être réacquis à chaque tentative utile.
Gestion d'erreur cible¶
Le front doit gérer les réponses antibot suivantes:
403aveccode = "invalid_challenge"503aveccode = "challenge_unavailable"
UX recommande:
invalid_challenge: afficher une erreur courte, invalider le challenge local, proposer une nouvelle tentativechallenge_unavailable: afficher une erreur temporaire, ne pas insister silencieusement, permettre de réessayer plus tard
Le front doit privilégier le champ code pour la logique applicative, et le
champ error pour l'affichage si nécessaire.
Definition of done côté front¶
- plus aucun écran public ne dépend de
reCAPTCHA - plus aucun payload front n'utilise
recaptchaToken - plus aucun payload front n'utilise
turnstileToken - tous les formulaires publics protégés envoient
challengeToken - tous les flows cibles utilisent le bon
action - tous les flows tokenisés utilisent le bon
binding - les erreurs
invalid_challengeetchallenge_unavailablesont gérées explicitement