
# Vérification des jetons dans votre API

Le vérificateur de jetons du SDK contrôle les jetons d'accès et les jetons ID émis par votre environnement KeenAuth. Utilisez-le dans l'API que vous protégez. Les trois SDK (Go, TypeScript et Python) l'incluent, et ils suivent les mêmes règles et les mêmes vecteurs de test.

## Go

```go
import "github.com/keenauth/keenauth-go/verify"

v, err := verify.New(verify.Config{
	Issuer:   "https://acme.eu.keenauth.app", // your environment's issuer, exactly
	Audience: "https://api.acme.example",     // the identifier of your API
})
if err != nil {
	return err
}

// In your HTTP handler:
claims, err := v.VerifyAccessToken(r.Context(), token, &verify.DPoPInput{
	Proof:  r.Header.Get("DPoP"),
	Method: r.Method,
	URL:    "https://api.acme.example" + r.URL.Path,
})
if err != nil {
	w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
	http.Error(w, "unauthorized", http.StatusUnauthorized)
	return
}
// claims.Subject, claims.ClientID, claims.Scope
```

Pour les jetons ID, définissez `ClientID` dans la configuration et appelez `v.VerifyIDToken(ctx, idToken, verify.IDTokenOptions{Nonce: nonce})`.

## TypeScript

Le vérificateur TypeScript utilise uniquement WebCrypto et `fetch` de la plateforme (Node.js 22 ou version ultérieure) et n'a aucune dépendance.

```ts
import { createVerifier, VerifyError } from "@keenauth/node";

const verifier = createVerifier({
  issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
  audience: "https://api.acme.example",   // the identifier of your API
});

// In your HTTP handler:
try {
  const claims = await verifier.verifyAccessToken(token, {
    proof: req.get("DPoP") ?? "",
    method: req.method,
    url: "https://api.acme.example" + req.path,
  });
  // claims.subject, claims.clientId, claims.scope
} catch (e) {
  if (!(e instanceof VerifyError)) throw e;
  res.set("WWW-Authenticate", 'Bearer error="invalid_token"').status(401).send("unauthorized");
}
```

Pour les jetons ID, définissez `clientId` dans la configuration et appelez `verifier.verifyIdToken(idToken, { nonce })`.

## Python

Le vérificateur Python nécessite le paquet `cryptography`, fourni avec l'extra `verify` : `pip install "keenauth-python[verify]"`. Les appels sont synchrones et thread-safe.

```python
from keenauth.verify import DPoPInput, Verifier, VerifierConfig, VerifyError

verifier = Verifier(VerifierConfig(
    issuer="https://acme.eu.keenauth.app",  # your environment's issuer, exactly
    audience="https://api.acme.example",    # the identifier of your API
))

# In your HTTP handler:
try:
    claims = verifier.verify_access_token(
        token,
        DPoPInput(
            proof=request.headers.get("DPoP", ""),
            method=request.method,
            url="https://api.acme.example" + request.path,
        ),
    )
    # claims.subject, claims.client_id, claims.scope
except VerifyError:
    return Response("unauthorized", status=401, headers={"WWW-Authenticate": 'Bearer error="invalid_token"'})
```

Pour les jetons ID, définissez `client_id` dans la configuration et appelez `verifier.verify_id_token(id_token, nonce=nonce)`.

## Étape supplémentaire (RFC 9470)

Pour protéger une opération sensible, exigez une connexion plus forte ou plus récente à chaque appel. Transmettez les références de classe de contexte d'authentification acceptables (`acr`) et l'âge maximal de la connexion (`auth_time`). `dpop` ci-dessous est l'entrée DPoP des exemples précédents (`nil`/`undefined`/`None` si vous n'utilisez pas DPoP).

### Go

```go
age := 15 * time.Minute
claims, err := v.VerifyAccessToken(r.Context(), token, dpop, verify.AccessTokenOptions{
	AcceptableACR: []string{"urn:idp:acr:mfa", "urn:idp:acr:phr"},
	MaxAuthAge:    &age,
})
if challenge, ok := verify.StepUpChallenge(err); ok {
	w.Header().Set("WWW-Authenticate", challenge)
	http.Error(w, "step-up required", http.StatusUnauthorized)
	return
}
```

### TypeScript

```ts
import { createVerifier, stepUpChallenge } from "@keenauth/node";

try {
  const claims = await verifier.verifyAccessToken(token, dpop, {
    acceptableAcr: ["urn:idp:acr:mfa", "urn:idp:acr:phr"],
    maxAuthAgeSeconds: 900,
  });
} catch (e) {
  const challenge = stepUpChallenge(e);
  if (challenge === undefined) throw e;
  res.set("WWW-Authenticate", challenge).status(401).send("step-up required");
}
```

### Python

```python
from datetime import timedelta
from keenauth.verify import VerifyError, step_up_challenge

try:
    claims = verifier.verify_access_token(
        token,
        dpop,
        acceptable_acr=["urn:idp:acr:mfa", "urn:idp:acr:phr"],
        max_auth_age=timedelta(minutes=15),
    )
except VerifyError as err:
    challenge = step_up_challenge(err)
    if challenge is None:
        raise
    return Response("step-up required", status=401, headers={"WWW-Authenticate": challenge})
```

Les contrôles sont effectués après tous les autres, de sorte qu'un jeton invalide renvoie toujours `invalid_token`. Un jeton valide qui ne satisfait pas l'exigence reçoit le code `insufficient_user_authentication`, et l'erreur contient les `acr_values` et `max_age` requis (uniquement ceux que vous avez définis). `StepUpChallenge` (Go), `stepUpChallenge` (TypeScript) et `step_up_challenge` (Python) le transforment en valeur d'en-tête `WWW-Authenticate` de la RFC 9470 §3, par exemple `Bearer error="insufficient_user_authentication", error_description="authentication too old", max_age="900"`. Un client qui comprend ce défi démarre une nouvelle demande d'autorisation avec ces `acr_values` et `max_age`. `acr` doit être exactement égal à l'une des valeurs (sensible à la casse) ; `auth_time` est requis lorsque vous définissez un âge maximal, et la tolérance de décalage d'horloge s'applique comme pour les jetons ID. Les valeurs `urn:idp:acr:1fa`, `urn:idp:acr:mfa` et `urn:idp:acr:phr` sont celles que votre environnement publie dans `acr_values_supported`.

## DPoP et le schéma Authorization (RFC 9449)

Un jeton lié à DPoP (`cnf.jkt`) est inutilisable sans la clé privée de son client ; une copie volée ne peut donc pas être rejouée. Deux règles le garantissent, et le vérificateur les applique toutes deux lorsque vous transmettez le schéma de l'en-tête `Authorization` de la requête (`Bearer` ou `DPoP`) :

- Un jeton lié à DPoP envoyé avec le schéma `Bearer` est rejeté avec `invalid_token`, même si une preuve valide est jointe (RFC 9449 §7.2).
- Un jeton envoyé avec le schéma `DPoP` doit être lié à DPoP et accompagné d'une preuve valide (RFC 9449 §7.1).

Pour n'accepter que des jetons à expéditeur contraint sur une API (recommandé pour les API sensibles), définissez également « require DPoP » : un jeton non lié à DPoP reçoit le code `dpop_required`. L'assistant de défi DPoP transforme toute erreur `401` en valeur `WWW-Authenticate` de la RFC 9449 §7.1, par exemple `DPoP error="invalid_token", error_description="a DPoP-bound access token is required", algs="ES256 EdDSA PS256 RS256"`.

### Go

```go
claims, err := v.VerifyAccessToken(r.Context(), token, dpop, verify.AccessTokenOptions{
	Scheme:      scheme, // "Bearer" or "DPoP", from the Authorization header
	RequireDPoP: true,
})
if challenge, ok := verify.DPoPChallenge(err); ok {
	w.Header().Set("WWW-Authenticate", challenge)
	http.Error(w, "unauthorized", http.StatusUnauthorized)
	return
}
```

### TypeScript

```ts
import { dpopChallenge } from "@keenauth/node";

try {
  const claims = await verifier.verifyAccessToken(token, dpop, { scheme, requireDPoP: true });
} catch (e) {
  const challenge = dpopChallenge(e);
  if (challenge === undefined) throw e;
  res.set("WWW-Authenticate", challenge).status(401).send("unauthorized");
}
```

### Python

```python
from keenauth.verify import VerifyError, dpop_challenge

try:
    claims = verifier.verify_access_token(token, dpop, scheme=scheme, require_dpop=True)
except VerifyError as err:
    challenge = dpop_challenge(err)
    if challenge is None:
        raise
    return Response("unauthorized", status=401, headers={"WWW-Authenticate": challenge})
```

Le middleware du serveur de ressources (Go `protect`, TypeScript `@keenauth/node/middleware`, Python `keenauth.middleware`) lit le schéma à votre place. Son paramètre « require DPoP » (par API ou par route) répond à une requête `Bearer` uniquement par `401` et le défi DPoP.

## Ce que le vérificateur contrôle

- **Émetteur et clés :** le `iss` du jeton doit être égal à l'émetteur configuré. Les clés ne sont récupérées que depuis le `jwks_uri` du document de découverte de cet émetteur, et le `kid` n'est recherché que dans ce jeu de clés. Un jeton signé avec la clé d'un autre environnement est rejeté.
- **Algorithme :** uniquement RS256, PS256, ES256 et EdDSA. L'`alg` de la clé doit être égal à l'`alg` du jeton ; `none` et les algorithmes HMAC sont toujours rejetés.
- **Jetons d'accès :** `typ` doit être `at+jwt` (RFC 9068), `aud` doit contenir l'audience configurée, et `sub` et `client_id` doivent être présents.
- **Jetons ID :** `aud` doit contenir votre identifiant client, `azp` doit lui être égal lorsqu'il est présent, et `nonce` et `max_age` sont vérifiés lorsque vous les transmettez.
- **Temps :** `exp` est requis ; `nbf` et `iat` ne doivent pas être dans le futur. Un décalage d'horloge d'au plus 60 secondes est toléré.
- **DPoP :** lorsque le jeton est lié à une clé (`cnf.jkt`), une preuve DPoP valide (RFC 9449) est requise. Les preuves réutilisées sont rejetées. Avec l'option de schéma, un jeton lié n'est jamais accepté comme jeton porteur ; avec « require DPoP », seuls les jetons liés sont acceptés.
- **Cache des clés :** les clés sont mises en cache pendant le `max-age` du serveur (300 secondes par défaut, 600 au maximum). Un `kid` inconnu déclenche au plus une récupération par minute.

## Erreurs

Chaque erreur possède un `Code`. Le message ne contient jamais le jeton ni ses revendications.

| Code | Signification | Réponse |
|---|---|---|
| `invalid_token` | Le jeton est invalide, expiré, ou non destiné à cette API. | `401` |
| `jwks_unavailable` | Les clés de l'émetteur n'ont pas pu être récupérées et aucune n'est en cache. | `503` |
| `dpop_invalid` | La preuve DPoP est absente ou invalide. | `401` avec `WWW-Authenticate` fourni par l'assistant de défi DPoP |
| `dpop_required` | Vous exigez DPoP et le jeton n'est pas lié à DPoP. | `401` avec `WWW-Authenticate` fourni par l'assistant de défi DPoP |
| `insufficient_user_authentication` | Le jeton est valide, mais son `acr` ou son `auth_time` ne satisfait pas votre exigence (RFC 9470). | `401` avec `WWW-Authenticate` fourni par l'assistant d'étape supplémentaire |
| `config_invalid` | La configuration du vérificateur est incorrecte. | Corrigez la configuration. |

## Si vous utilisez une autre bibliothèque JWT

Appliquez la même liste de contrôle : liez l'émetteur à son document de découverte, ne prenez les clés que depuis ce `jwks_uri`, exigez que l'`alg` de la clé soit égal à l'`alg` du jeton, et rejetez `none` ainsi que les algorithmes HMAC.

## Tests hors ligne

Pour les tests unitaires et la CI, utilisez le [kit de test hors ligne](/docs/sdk/testing). Il génère des jetons de test que seul un vérificateur en mode test explicite accepte ; un vérificateur de production les rejette toujours.
