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

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.

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.

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

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

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

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) :

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

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

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

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

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