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) :
- Un jeton lié à DPoP envoyé avec le schéma
Bearerest rejeté avecinvalid_token, même si une preuve valide est jointe (RFC 9449 §7.2). - Un jeton envoyé avec le schéma
DPoPdoit ê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
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
- Émetteur et clés : le
issdu jeton doit être égal à l'émetteur configuré. Les clés ne sont récupérées que depuis lejwks_uridu document de découverte de cet émetteur, et lekidn'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'
algde la clé doit être égal à l'algdu jeton ;noneet les algorithmes HMAC sont toujours rejetés. - Jetons d'accès :
typdoit êtreat+jwt(RFC 9068),auddoit contenir l'audience configurée, etsubetclient_iddoivent être présents. - Jetons ID :
auddoit contenir votre identifiant client,azpdoit lui être égal lorsqu'il est présent, etnonceetmax_agesont vérifiés lorsque vous les transmettez. - Temps :
expest requis ;nbfetiatne 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-agedu serveur (300 secondes par défaut, 600 au maximum). Unkidinconnu 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. 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.