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 et n'ajoute aucune logique propre aux jetons.

Ce que fait le middleware

Go (net/http)

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

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.

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.

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 :

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 :

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 :

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.

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 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.