Ce guide vaut pour FastAPI sur Python 3.11 ou plus récent, servi par uvicorn, et fonctionne de même dans toute application Starlette. L'appel utilise httpx avec un seul AsyncClient par processus, qui garde la connexion ouverte ; asyncio.timeout borne l'appel entier, connexion comprise, puisque le timeout propre à httpx s'applique à chaque étape séparément.
Le tag
Placez le tag dans le <head> du template qu'étendent vos pages d'inscription, de connexion et de checkout :
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
L'appel
# korzen.py import asyncio import os import httpx korzen = httpx.AsyncClient() # one per process: it keeps the connection open # The headers Bot Protection reads: those one browser build sends alike, and Accept-Language. FORWARDED = ["accept", "accept-encoding", "accept-language", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform", "sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site", "upgrade-insecure-requests"] async def decide(request, path): """The answer for this request, or None on any error: then go on as on allow (fail open).""" try: async with asyncio.timeout(0.2): # the whole call, the connection included r = await korzen.post( "https://korzen.dev/v1/decide", headers={"Authorization": f"Bearer {os.environ['KORZEN_API_KEY']}", "Korzen-Deadline-Ms": "200"}, json={ "ip": request.client.host if request.client else None, # see "The visitor's IP" "user_agent": request.headers.get("user-agent"), "headers": {h: request.headers[h] for h in FORWARDED if h in request.headers}, # The order the request's headers came in, as the ASGI server keeps it. "header_order": [name.decode("latin-1").lower() for name, _ in request.scope["headers"][:64]], "token": request.cookies.get("_kz"), "path": path, "method": "POST", }, ) return r.json() if r.status_code == 200 else None except Exception: return None
header_order est l'ordre dans lequel les headers de la requête sont arrivés : un script qui copie les valeurs d'un navigateur en copie rarement l'ordre. Contrairement à WSGI, ASGI le garde, dans request.scope["headers"]. Derrière nginx, un load balancer ou Cloudflare, c'est l'ordre dans lequel ce saut les transmet, pas celui du navigateur.
Dans la route qui crée le compte (Form a besoin de python-multipart) :
# main.py from typing import Annotated from fastapi import FastAPI, Form, Request from fastapi.responses import PlainTextResponse, RedirectResponse import korzen app = FastAPI() @app.post("/signup") async def signup(request: Request, email: Annotated[str, Form()]): d = await korzen.decide(request, "/signup") if d and d["action"] == "block": return PlainTextResponse("We could not create your account.", status_code=400) # "challenge": your CAPTCHA, or an email or phone check, before going on user = await create_account(email) # your own code await save_request_id(user, d and d["request_id"]) # see "Keep the request_id" return RedirectResponse("/welcome", status_code=303)
L'IP du visiteur
request.client.host est l'adresse de la connexion, sauf si uvicorn fait confiance au proxy devant. uvicorn lit X-Forwarded-For depuis les adresses que nomme --forwarded-allow-ips (127.0.0.1 par défaut) et prend comme client la dernière entrée qu'elles n'ont pas ajoutée : lancez-le avec l'adresse de votre load balancer, comme uvicorn main:app --forwarded-allow-ips 10.0.0.5, ou le réseau où il se trouve. Derrière Cloudflare, lisez plutôt request.headers.get("cf-connecting-ip"), si votre serveur ne reçoit du trafic que de Cloudflare.
Appliquer l'action
Ne lisez que action. Avec block, répondez avec la même erreur que l'action affiche quand elle échoue pour toute autre raison, pour qu'un bot n'apprenne rien. Avec challenge, utilisez la vérification que vous avez déjà (un CAPTCHA, un code par e-mail ou téléphone) ; sans aucune, continuez. Le compte commence en mode monitor, où action est toujours allow : rien ne change pour les visiteurs tant que vous n'activez pas le mode block.
Garder le request_id
Chaque réponse a un request_id. Gardez-le avec ce que l'action a créé, dans une colonne du compte ou de la commande, ou dans une ligne de log avec leur identifiant. Il désigne cette décision quand vous nous interrogez à son sujet, et vous permet de retrouver plus tard quelle réponse a laissé entrer un faux compte.
Tester sur votre machine
Lancez uvicorn main:app --reload et ouvrez-le sur localhost. Les pages sur localhost acceptent toujours le tag et n'apparaissent jamais comme domaine. En local, l'IP est 127.0.0.1 ou ::1, et c'est très bien. Dans Docker sur votre machine, l'app voit à la place l'adresse du bridge, comme 172.17.0.1 : la page de challenge vous ramène quand même sur localhost, puisque le tag y a vu votre page.
- Définissez
KORZEN_API_KEYdans l'environnement et ouvrez la page d'inscription. Dans les outils de développement du navigateur, le cookie_kzapparaît après quelques secondes. - Inscrivez-vous. La décision apparaît sur la page Décisions du tableau de bord, sous le chemin que vous avez envoyé.
- Vérifiez le fail-open : mettez une mauvaise clé et inscrivez-vous à nouveau. L'appel répond
401, et l'inscription passe.
Erreurs fréquentes
--forwarded-allow-ips '*'. Il fait confiance à chaque saut, donc le client devient la première entrée deX-Forwarded-For, que le visiteur écrit.- Un nouveau
httpx.AsyncClient()pour chaque appel. Chacun ouvre sa propre connexion, avec une poignée de main TLS dans votre timeout. timeout=sur httpx seul. Il borne chaque étape séparément (connexion, écriture, lecture), donc l'appel peut durer plusieurs fois plus longtemps ;asyncio.timeoutle borne en entier.- Appeler
/v1/decidedepuis le navigateur. L'API key atteindrait chaque visiteur ; l'appel a sa place sur votre serveur. - Pas de timeout, ou une erreur qui bloque l'inscription. Toute réponse autre que
200, ou l'absence de réponse à temps, signifie : laissez passer le visiteur.