Verifica dei token nella tua API
Il verificatore di token dell'SDK controlla gli access token e gli ID token emessi dal tuo ambiente KeenAuth. Usalo nell'API che proteggi. Tutti e tre gli SDK (Go, TypeScript e Python) lo includono e seguono le stesse regole e gli stessi test vector.
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
Per gli ID token, imposta ClientID nella configurazione e chiama v.VerifyIDToken(ctx, idToken, verify.IDTokenOptions{Nonce: nonce}).
TypeScript
Il verificatore TypeScript usa solo WebCrypto e fetch della piattaforma (Node.js 22 o successivo) e non ha dipendenze.
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");
}
Per gli ID token, imposta clientId nella configurazione e chiama verifier.verifyIdToken(idToken, { nonce }).
Python
Il verificatore Python richiede il pacchetto cryptography, incluso con l'extra verify: pip install "keenauth-python[verify]". Le chiamate sono sincrone e 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"'})
Per gli ID token, imposta client_id nella configurazione e chiama verifier.verify_id_token(id_token, nonce=nonce).
Step-up (RFC 9470)
Per proteggere un'operazione sensibile, richiedi per ogni chiamata un accesso più forte o più recente. Passa i valori di authentication context class reference (acr) accettabili e l'età massima dell'accesso (auth_time). dpop qui sotto è l'input DPoP degli esempi precedenti (nil/undefined/None se non usi 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})
I controlli vengono eseguiti dopo tutti gli altri, quindi un token non valido restituisce sempre invalid_token. Un token valido che non soddisfa il requisito riceve il codice insufficient_user_authentication, e l'errore contiene acr_values e max_age richiesti (solo quelli che hai impostato). StepUpChallenge (Go), stepUpChallenge (TypeScript) e step_up_challenge (Python) lo trasformano nel valore dell'header WWW-Authenticate previsto dalla sezione 3 di RFC 9470, ad esempio Bearer error="insufficient_user_authentication", error_description="authentication too old", max_age="900". Un client che riconosce la challenge avvia una nuova richiesta di autorizzazione con quei acr_values e quel max_age. acr deve corrispondere esattamente a uno dei valori (con distinzione tra maiuscole e minuscole); auth_time è obbligatorio quando imposti un'età massima, e si applica la tolleranza di disallineamento dell'orologio come per gli ID token. I valori urn:idp:acr:1fa, urn:idp:acr:mfa e urn:idp:acr:phr sono quelli che il tuo ambiente pubblica in acr_values_supported.
DPoP e lo schema Authorization (RFC 9449)
Un token vincolato con DPoP (cnf.jkt) non serve a nulla senza la chiave privata del suo client, quindi una copia rubata non può essere riprodotta. Due regole garantiscono questo, e il verificatore le applica entrambe quando passi lo schema dell'header Authorization della richiesta (Bearer o DPoP):
- Un token vincolato con DPoP inviato con lo schema
Bearerviene rifiutato coninvalid_token, anche se è allegata una prova valida (RFC 9449 §7.2). - Un token inviato con lo schema
DPoPdeve essere vincolato con DPoP e accompagnato da una prova valida (RFC 9449 §7.1).
Per accettare solo token vincolati al mittente su un'API (consigliato per le API sensibili), imposta anche "require DPoP": un token non vincolato con DPoP riceve il codice dpop_required. L'helper della challenge DPoP trasforma qualsiasi errore 401 nel valore WWW-Authenticate previsto dalla sezione 7.1 di RFC 9449, ad esempio 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})
Il middleware del resource server (Go protect, TypeScript @keenauth/node/middleware, Python keenauth.middleware) legge lo schema al posto tuo. La sua impostazione "require DPoP" (per API o per route) risponde a una richiesta Bearer solo con 401 e la challenge DPoP.
Cosa controlla il verificatore
- Issuer e chiavi: il
issdel token deve coincidere con l'issuer configurato. Le chiavi vengono recuperate solo daljwks_urinel documento di discovery di quell'issuer, e ilkidviene cercato solo in quel set di chiavi. Un token firmato dalla chiave di un altro ambiente viene rifiutato. - Algoritmo: solo RS256, PS256, ES256 ed EdDSA. L'
algdella chiave deve coincidere con l'algdel token;nonee gli algoritmi HMAC vengono sempre rifiutati. - Access token:
typdeve essereat+jwt(RFC 9068),auddeve contenere l'audience configurata, esubeclient_iddevono essere presenti. - ID token:
auddeve contenere il tuo client ID,azpdeve coincidere con esso se presente, enonceemax_agevengono controllati quando li passi. - Tempo:
expè obbligatorio;nbfeiatnon devono essere nel futuro. È consentito un disallineamento dell'orologio fino a 60 secondi. - DPoP: quando il token è vincolato a una chiave (
cnf.jkt), è richiesta una prova DPoP valida (RFC 9449). Le prove riutilizzate vengono rifiutate. Con l'opzione dello schema, un token vincolato non viene mai accettato come bearer token; con "require DPoP" vengono accettati solo i token vincolati. - Cache delle chiavi: le chiavi vengono messe in cache per il
max-agedel server (300 secondi per impostazione predefinita, al massimo 600). Unkidsconosciuto attiva al massimo un nuovo recupero al minuto.
Errori
Ogni errore ha un Code. Il messaggio non contiene mai il token né i suoi claim.
| Codice | Significato | Rispondi con |
|---|---|---|
invalid_token |
Il token non è valido, è scaduto o non è destinato a questa API. | 401 |
jwks_unavailable |
Le chiavi dell'issuer non possono essere recuperate e non ce ne sono in cache. | 503 |
dpop_invalid |
La prova DPoP manca o non è valida. | 401 con WWW-Authenticate dall'helper della challenge DPoP |
dpop_required |
Richiedi DPoP e il token non è vincolato con DPoP. | 401 con WWW-Authenticate dall'helper della challenge DPoP |
insufficient_user_authentication |
Il token è valido ma il suo acr o auth_time non soddisfa il requisito (RFC 9470). |
401 con WWW-Authenticate dall'helper dello step-up |
config_invalid |
La configurazione del verificatore non è corretta. | Correggi la configurazione. |
Se usi un'altra libreria JWT
Applica la stessa checklist: vincola l'issuer al suo documento di discovery, prendi le chiavi solo da quel jwks_uri, richiedi che l'alg della chiave coincida con l'alg del token, e rifiuta none e gli algoritmi HMAC.
Test offline
Per i unit test e la CI, usa il kit di test offline. Genera token di test che solo un verificatore in modalità test esplicita accetta; un verificatore di produzione li rifiuta sempre.