
# 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

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

```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");
}
```

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.

```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"'})
```

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

```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})
```

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 `Bearer` viene rifiutato con `invalid_token`, anche se è allegata una prova valida (RFC 9449 §7.2).
- Un token inviato con lo schema `DPoP` deve 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

```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})
```

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 `iss` del token deve coincidere con l'issuer configurato. Le chiavi vengono recuperate solo dal `jwks_uri` nel documento di discovery di quell'issuer, e il `kid` viene 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'`alg` della chiave deve coincidere con l'`alg` del token; `none` e gli algoritmi HMAC vengono sempre rifiutati.
- **Access token:** `typ` deve essere `at+jwt` (RFC 9068), `aud` deve contenere l'audience configurata, e `sub` e `client_id` devono essere presenti.
- **ID token:** `aud` deve contenere il tuo client ID, `azp` deve coincidere con esso se presente, e `nonce` e `max_age` vengono controllati quando li passi.
- **Tempo:** `exp` è obbligatorio; `nbf` e `iat` non 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-age` del server (300 secondi per impostazione predefinita, al massimo 600). Un `kid` sconosciuto 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](/docs/sdk/testing). Genera token di test che solo un verificatore in modalità test esplicita accetta; un verificatore di produzione li rifiuta sempre.
