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

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

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.