
# Protéger votre API avec un middleware

Chaque SDK serveur fournit un middleware léger qui protège votre API à l'aide des jetons d'accès KeenAuth : `net/http` en Go, Express et Fastify en Node.js, et Django, Flask et FastAPI en Python. Il s'appuie sur le [vérificateur de jetons](/docs/sdk/token-verification) et n'ajoute aucune logique propre aux jetons.

## Ce que fait le middleware

- **Lit le jeton** dans l'en-tête `Authorization`, avec le schéma `Bearer` ou `DPoP`. Un jeton passé dans la query de l'URL (`?access_token=`) est toujours refusé, car les URL finissent dans les journaux et les en-têtes `Referer`. Une requête contenant plusieurs en-têtes `Authorization` ou plusieurs identifiants est également refusée.
- **Vérifie DPoP.** Avec le schéma `DPoP`, l'en-tête de preuve `DPoP`, la méthode de la requête et l'URL absolue de la requête (l'URL publique de base configurée suivie du chemin de la requête) sont transmis au vérificateur. Un jeton lié à DPoP envoyé avec le schéma `Bearer` est rejeté, tout comme le schéma `DPoP` employé avec un jeton qui n'est pas lié à DPoP.
- **Vérifie les exigences de la route :** les scopes requis (tous) et, éventuellement, une authentification renforcée : valeurs `acr` acceptables et âge maximal de la connexion (RFC 9470).
- **Interroge le cache de révocation**, si vous en configurez un, pour savoir si le jeton a été révoqué.
- **Attache les revendications vérifiées** à la requête : `protect.ClaimsFrom(ctx)` en Go, `req.auth` avec Express et Fastify, `request.auth` avec Django, `g.auth` avec Flask, et la valeur retournée par la dépendance avec FastAPI.
- **Répond aux échecs** avec le challenge standard `WWW-Authenticate` et un petit corps `application/problem+json`. La réponse ne contient jamais le jeton ni la valeur d'une revendication, et le middleware ne les journalise jamais.

## Go (net/http)

Le package `protect` n'utilise que la bibliothèque standard et fonctionne avec tout routeur acceptant un `http.Handler`.

```go
import (
	"github.com/keenauth/keenauth-go/protect"
	"github.com/keenauth/keenauth-go/verify"
)

v, err := verify.New(verify.Config{
	Issuer:   "https://acme.eu.keenauth.app",
	Audience: "https://api.acme.example",
})
if err != nil {
	return err
}
m, err := protect.New(v, protect.Options{
	PublicBaseURL:       "https://api.acme.example",
	ResourceMetadataURL: "https://api.acme.example/.well-known/oauth-protected-resource",
})
if err != nil {
	return err
}

mux := http.NewServeMux()
mux.Handle("GET /orders", m.Require(protect.Requirement{Scopes: []string{"orders:read"}})(http.HandlerFunc(
	func(w http.ResponseWriter, r *http.Request) {
		claims, _ := protect.ClaimsFrom(r.Context())
		fmt.Fprintln(w, claims.Subject)
	})))

age := 10 * time.Minute
mux.Handle("POST /payouts", m.Require(protect.Requirement{
	Scopes:        []string{"payouts:write"},
	AcceptableACR: []string{"urn:idp:acr:mfa"},
	MaxAuthAge:    &age,
})(payoutsHandler))
```

`m.Handler(next)` protège un handler avec un jeton valide et sans exigence supplémentaire. `Require` provoque un panic au démarrage si un scope n'est pas un jeton de scope valide, comme le fait `http.ServeMux` pour un motif invalide.

## Node.js (Express et Fastify)

Le middleware se trouve dans le sous-chemin `@keenauth/node/middleware`. Express et Fastify ne sont pas des dépendances du SDK.

```ts
import { createVerifier } from "@keenauth/node";
import { createProtector, expressAuth, fastifyAuth } from "@keenauth/node/middleware";

const verifier = createVerifier({ issuer: "https://acme.eu.keenauth.app", audience: "https://api.acme.example" });
const protector = createProtector(verifier, { publicBaseUrl: "https://api.acme.example" });

// Express
app.get("/orders", expressAuth(protector, { scopes: ["orders:read"] }), (req, res) => {
  res.json({ sub: req.auth.subject });
});

// Fastify
fastify.decorateRequest("auth", null);
fastify.get("/orders", { preHandler: fastifyAuth(protector, { scopes: ["orders:read"] }) }, async (request) => {
  return { sub: request.auth.subject };
});
```

L'authentification renforcée utilise `{ acceptableAcr: ["urn:idp:acr:mfa"], maxAuthAgeSeconds: 600 }`. En TypeScript, typez la requête avec `AuthenticatedRequest` (exporté par le sous-chemin) pour lire `req.auth`.

## Python (Django, Flask, FastAPI)

Installez l'extra correspondant à votre framework : `pip install "keenauth-python[django]"`, `"keenauth-python[flask]"` ou `"keenauth-python[fastapi]"`. Le framework n'est importé que par son propre module d'adaptation.

```python
from keenauth.middleware import Protector
from keenauth.verify import Verifier, VerifierConfig

verifier = Verifier(VerifierConfig(issuer="https://acme.eu.keenauth.app", audience="https://api.acme.example"))
protector = Protector(verifier, public_base_url="https://api.acme.example")
```

Django :

```python
from django.http import JsonResponse
from keenauth.middleware.django import protect

@protect(protector, scopes=["orders:read"])
def orders(request):
    return JsonResponse({"sub": request.auth.subject})
```

Flask :

```python
from flask import g
from keenauth.middleware.flask import protect

@app.get("/orders")
@protect(protector, scopes=["orders:read"])
def orders():
    return {"sub": g.auth.subject}
```

FastAPI :

```python
from fastapi import Depends, FastAPI
from keenauth.middleware.fastapi import RequireAuth, install
from keenauth.verify import AccessClaims

app = FastAPI()
install(app)

@app.get("/orders")
def orders(claims: AccessClaims = Depends(RequireAuth(protector, scopes=["orders:read"]))):
    return {"sub": claims.subject}
```

`install(app)` enregistre le handler qui écrit le corps de problème et un en-tête `WWW-Authenticate` par challenge. L'authentification renforcée utilise `acceptable_acr=["urn:idp:acr:mfa"], max_auth_age=timedelta(minutes=10)`.

## L'URL publique de base (obligatoire)

Une preuve DPoP indique l'URL appelée par le client (`htu`). Le middleware compare cette valeur à l'URL publique de base que vous configurez (`PublicBaseURL`, `publicBaseUrl`, `public_base_url`), suivie du chemin de la requête. Renseignez l'adresse utilisée par vos clients, par exemple `https://api.acme.example` ou `https://acme.example/api`. Ce paramètre est obligatoire : la création du middleware sans lui échoue au démarrage avec une erreur de configuration, même si vos clients n'envoient que des jetons Bearer.

Le middleware ne construit jamais cette URL à partir de l'en-tête `Host` de la requête ni des en-têtes `X-Forwarded-*`, et en Python il n'utilise pas l'URL de requête propre au framework, même si le framework ou le serveur est configuré pour faire confiance aux en-têtes transmis. Ces valeurs sont choisies par le client : si votre API est servie sous deux noms d'hôte, une preuve établie pour l'un d'eux pourrait sinon être rejouée sur l'autre. Derrière un répartiteur de charge ou un proxy inverse, l'URL publique de base indique aussi au middleware le schéma, l'hôte et le préfixe de chemin que voient les clients. L'URL publique de base doit utiliser `https`, sauf pour `localhost`, `127.0.0.1` et `[::1]` (RFC 6750 §5.3) ; le schéma et l'hôte sont comparés sans tenir compte de la casse, et un port par défaut (`:443`, `:80`) est ignoré, aussi bien dans l'URL de base que dans le `htu` de la preuve.

## Réponses

| Situation | Statut | `WWW-Authenticate` | `code` |
|---|---|---|---|
| Aucun jeton, ou un schéma non pris en charge | `401` | `Bearer` et `DPoP algs="…"`, sans erreur | `missing_token` |
| Jeton dans la query, `Authorization` répété ou mal formé | `400` | `Bearer error="invalid_request"` | `invalid_request` |
| Jeton invalide, expiré ou révoqué | `401` | `error="invalid_token"` | `invalid_token` |
| Preuve DPoP absente, répétée ou invalide | `401` | `DPoP error="invalid_dpop_proof"` | `invalid_dpop_proof` |
| Un scope requis est absent | `403` | `error="insufficient_scope", scope="…"` | `insufficient_scope` |
| Authentification renforcée nécessaire | `401` | `error="insufficient_user_authentication", acr_values="…", max_age="…"` | `insufficient_user_authentication` |
| Clés de signature ou état de révocation indisponibles | `503` | aucun | `jwks_unavailable`, `revocation_unavailable` |

Le challenge utilise le schéma employé par le client. Lorsque vous renseignez l'URL des métadonnées de ressource, chaque challenge `401` contient `resource_metadata` (RFC 9728). Le corps contient `type`, `title`, `status`, `code` et un `detail` fixe ; il n'indique jamais pourquoi un jeton était invalide. Pour vos journaux, transmettez un callback d'erreur (`OnError`, `onError`, `on_error`) : il reçoit l'erreur du vérificateur, qui ne contient aucune valeur de jeton ni de revendication. Ne journalisez pas vous-même les en-têtes `Authorization` ou `DPoP`.

## Cache de révocation (`token.revoked`)

Un jeton d'accès JWT reste valide jusqu'à son expiration. Pour rejeter plus tôt les jetons révoqués, traitez le webhook `token.revoked` et enregistrez son sélecteur dans un cache de révocation que le middleware consulte. Les SDK incluent un cache en mémoire : `protect.NewMemoryRevocationCache` (Go), `createMemoryRevocationCache` (Node.js) et `MemoryRevocationCache` (Python).

Un sélecteur désigne un identifiant de jeton (`jti`), une session (`sid`), un utilisateur (`sub`), une organisation (`org_id`), une application (`client_id`), ou les couples organisation + utilisateur et application + utilisateur. Un sélecteur `jti` révoque ce jeton. Les autres sélecteurs révoquent tous les jetons émis avant le `not_before` de l'événement ; les jetons émis plus tard sont acceptés. Les marqueurs sont conservés 24 heures et 1 minute par défaut, soit la durée de vie maximale possible d'un jeton d'accès plus la dérive d'horloge.

```go
cache := protect.NewMemoryRevocationCache(0, nil)
m, _ := protect.New(v, protect.Options{PublicBaseURL: "https://api.acme.example", Revocation: cache})

// Dans votre handler de webhook token.revoked vérifié :
err := cache.Revoke(protect.Selector{SessionID: "ses_…"}, notBefore)
```

Le cache en mémoire appartient à un seul processus. Si votre API tourne sur plusieurs instances, implémentez l'interface du vérificateur de révocation sur un stockage partagé. Si le cache ne peut pas répondre, le middleware refuse la requête avec un `503`.

## Tests

Utilisez le [kit de test hors ligne](/docs/sdk/testing) pour émettre des jetons dans vos tests de middleware. Le kit ne fournit pas d'assistant de preuve DPoP ; signez une preuve dans votre test avec la bibliothèque cryptographique du langage.
