
# Proteggere la tua API con il middleware

Ogni SDK server include un middleware essenziale che protegge la tua API con i token di accesso KeenAuth: `net/http` in Go, Express e Fastify in Node.js, e Django, Flask e FastAPI in Python. Si appoggia sul [verificatore di token](/docs/sdk/token-verification) e non aggiunge una logica propria sui token.

## Cosa fa il middleware

- **Legge il token** dall'header `Authorization` con lo schema `Bearer` o `DPoP`. Un token nella query dell'URL (`?access_token=`) viene sempre rifiutato, perché gli URL finiscono nei log e negli header `Referer`. Viene rifiutata anche una richiesta con più di un header `Authorization` o con più di una credenziale.
- **Controlla DPoP.** Con lo schema `DPoP`, l'header della prova `DPoP`, il metodo della richiesta e l'URL assoluto della richiesta (l'URL base pubblico configurato seguito dal percorso della richiesta) vengono passati al verificatore. Un token vincolato a DPoP inviato con lo schema `Bearer` viene rifiutato, così come lo schema `DPoP` con un token non vincolato a DPoP.
- **Controlla i requisiti della route:** gli scope richiesti (tutti) e, facoltativamente, il step-up: i valori `acr` accettabili e un'età massima dell'accesso (RFC 9470).
- **Chiede alla cache di revoca**, se ne configuri una, se il token è stato revocato.
- **Allega le claim verificate** alla richiesta: `protect.ClaimsFrom(ctx)` in Go, `req.auth` in Express e Fastify, `request.auth` in Django, `g.auth` in Flask e il valore restituito dalla dipendenza in FastAPI.
- **Risponde agli errori** con la sfida standard `WWW-Authenticate` e un piccolo corpo `application/problem+json`. La risposta non contiene mai il token né alcun valore di claim, e il middleware non li registra mai nei log.

## Go (net/http)

Il pacchetto `protect` usa solo la libreria standard e funziona con qualsiasi router che accetti un `http.Handler`.

```go
import (
	"github.com/keenauth/keenauth-go/protect"
	"github.com/keenauth/keenauth-go/verify"
)

v, err := verify.New(verify.Config{
	Issuer:   "https://acme.eu.keenauth.app",
	Audience: "https://api.acme.example",
})
if err != nil {
	return err
}
m, err := protect.New(v, protect.Options{
	PublicBaseURL:       "https://api.acme.example",
	ResourceMetadataURL: "https://api.acme.example/.well-known/oauth-protected-resource",
})
if err != nil {
	return err
}

mux := http.NewServeMux()
mux.Handle("GET /orders", m.Require(protect.Requirement{Scopes: []string{"orders:read"}})(http.HandlerFunc(
	func(w http.ResponseWriter, r *http.Request) {
		claims, _ := protect.ClaimsFrom(r.Context())
		fmt.Fprintln(w, claims.Subject)
	})))

age := 10 * time.Minute
mux.Handle("POST /payouts", m.Require(protect.Requirement{
	Scopes:        []string{"payouts:write"},
	AcceptableACR: []string{"urn:idp:acr:mfa"},
	MaxAuthAge:    &age,
})(payoutsHandler))
```

`m.Handler(next)` protegge un handler con un token valido e senza ulteriori requisiti. `Require` va in panic all'avvio se uno scope non è un token di scope valido, come fa `http.ServeMux` con un pattern non valido.

## Node.js (Express e Fastify)

Il middleware si trova nel sottopercorso `@keenauth/node/middleware`. Express e Fastify non sono dipendenze dell'SDK.

```ts
import { createVerifier } from "@keenauth/node";
import { createProtector, expressAuth, fastifyAuth } from "@keenauth/node/middleware";

const verifier = createVerifier({ issuer: "https://acme.eu.keenauth.app", audience: "https://api.acme.example" });
const protector = createProtector(verifier, { publicBaseUrl: "https://api.acme.example" });

// Express
app.get("/orders", expressAuth(protector, { scopes: ["orders:read"] }), (req, res) => {
  res.json({ sub: req.auth.subject });
});

// Fastify
fastify.decorateRequest("auth", null);
fastify.get("/orders", { preHandler: fastifyAuth(protector, { scopes: ["orders:read"] }) }, async (request) => {
  return { sub: request.auth.subject };
});
```

Lo step-up usa `{ acceptableAcr: ["urn:idp:acr:mfa"], maxAuthAgeSeconds: 600 }`. In TypeScript, tipizza la richiesta come `AuthenticatedRequest` (esportato dal sottopercorso) per leggere `req.auth`.

## Python (Django, Flask, FastAPI)

Installa l'extra per il tuo framework: `pip install "keenauth-python[django]"`, `"keenauth-python[flask]"` oppure `"keenauth-python[fastapi]"`. Il framework viene importato solo dal proprio modulo adattatore.

```python
from keenauth.middleware import Protector
from keenauth.verify import Verifier, VerifierConfig

verifier = Verifier(VerifierConfig(issuer="https://acme.eu.keenauth.app", audience="https://api.acme.example"))
protector = Protector(verifier, public_base_url="https://api.acme.example")
```

Django:

```python
from django.http import JsonResponse
from keenauth.middleware.django import protect

@protect(protector, scopes=["orders:read"])
def orders(request):
    return JsonResponse({"sub": request.auth.subject})
```

Flask:

```python
from flask import g
from keenauth.middleware.flask import protect

@app.get("/orders")
@protect(protector, scopes=["orders:read"])
def orders():
    return {"sub": g.auth.subject}
```

FastAPI:

```python
from fastapi import Depends, FastAPI
from keenauth.middleware.fastapi import RequireAuth, install
from keenauth.verify import AccessClaims

app = FastAPI()
install(app)

@app.get("/orders")
def orders(claims: AccessClaims = Depends(RequireAuth(protector, scopes=["orders:read"]))):
    return {"sub": claims.subject}
```

`install(app)` registra il handler che scrive il corpo del problema e un header `WWW-Authenticate` per ogni sfida. Lo step-up usa `acceptable_acr=["urn:idp:acr:mfa"], max_auth_age=timedelta(minutes=10)`.

## L'URL base pubblico (obbligatorio)

Una prova DPoP indica l'URL che il client ha chiamato (`htu`). Il middleware lo confronta con l'URL base pubblico che configuri (`PublicBaseURL`, `publicBaseUrl`, `public_base_url`) seguito dal percorso della richiesta. Impostalo sull'indirizzo che usano i client, per esempio `https://api.acme.example` oppure `https://acme.example/api`. È obbligatorio: creare il middleware senza di esso fallisce all'avvio con un errore di configurazione, anche se i tuoi client inviano solo token Bearer.

Il middleware non costruisce mai questo URL dall'header `Host` della richiesta né dagli header `X-Forwarded-*`, e in Python non usa l'URL di richiesta del framework, anche se il framework o il server sono configurati per fidarsi degli header inoltrati. I valori sono scelti dal client: se la tua API è servita con due nomi host, una prova creata per uno dei due potrebbe altrimenti essere riutilizzata sull'altro. Dietro un load balancer o un reverse proxy, l'URL base pubblico è anche ciò che indica al middleware lo schema, l'host e il prefisso di percorso che vedono i client. L'URL base pubblico deve usare `https`, eccetto per `localhost`, `127.0.0.1` e `[::1]` (RFC 6750 §5.3); schema e host vengono confrontati senza distinzione tra maiuscole e minuscole, e una porta predefinita (`:443`, `:80`) viene ignorata, sia nell'URL base sia nell'`htu` della prova.

## Risposte

| Situazione | Stato | `WWW-Authenticate` | `code` |
|---|---|---|---|
| Nessun token, oppure uno schema non supportato | `401` | `Bearer` e `DPoP algs="…"`, senza errore | `missing_token` |
| Token nella query, `Authorization` ripetuto o malformato | `400` | `Bearer error="invalid_request"` | `invalid_request` |
| Token non valido, scaduto o revocato | `401` | `error="invalid_token"` | `invalid_token` |
| Prova DPoP mancante, ripetuta o non valida | `401` | `DPoP error="invalid_dpop_proof"` | `invalid_dpop_proof` |
| Manca uno scope richiesto | `403` | `error="insufficient_scope", scope="…"` | `insufficient_scope` |
| È necessario lo step-up | `401` | `error="insufficient_user_authentication", acr_values="…", max_age="…"` | `insufficient_user_authentication` |
| Chiavi di firma o stato di revoca non disponibili | `503` | nessuno | `jwks_unavailable`, `revocation_unavailable` |

La sfida usa lo schema usato dal client. Quando imposti l'URL dei metadati della risorsa, ogni sfida `401` include `resource_metadata` (RFC 9728). Il corpo contiene `type`, `title`, `status`, `code` e un `detail` fisso; non indica mai il motivo per cui un token non era valido. Per i tuoi log, passa una callback di errore (`OnError`, `onError`, `on_error`): riceve l'errore del verificatore, che non contiene alcun token né valore di claim. Non registrare tu stesso gli header `Authorization` o `DPoP`.

## Cache di revoca (`token.revoked`)

Un token di accesso JWT resta valido fino alla scadenza. Per rifiutare prima i token revocati, gestisci il webhook `token.revoked` e registra il suo selettore in una cache di revoca che il middleware consulta. Gli SDK includono una cache in memoria: `protect.NewMemoryRevocationCache` (Go), `createMemoryRevocationCache` (Node.js) e `MemoryRevocationCache` (Python).

Un selettore indica un ID token (`jti`), una sessione (`sid`), un utente (`sub`), un'organizzazione (`org_id`), un'applicazione (`client_id`), oppure le coppie organizzazione + utente e applicazione + utente. Un selettore `jti` revoca quel token. Gli altri selettori revocano ogni token emesso prima del `not_before` dell'evento; i token emessi successivamente vengono accettati. Per impostazione predefinita, i marcatori vengono conservati per 24 ore e 1 minuto, cioè la durata massima possibile di un token di accesso più lo scarto dell'orologio.

```go
cache := protect.NewMemoryRevocationCache(0, nil)
m, _ := protect.New(v, protect.Options{PublicBaseURL: "https://api.acme.example", Revocation: cache})

// Nel tuo handler del webhook token.revoked verificato:
err := cache.Revoke(protect.Selector{SessionID: "ses_…"}, notBefore)
```

La cache in memoria appartiene a un solo processo. Se la tua API gira su più istanze, implementa l'interfaccia del controllo di revoca su un archivio condiviso. Se la cache non riesce a rispondere, il middleware rifiuta la richiesta con `503`.

## Test

Usa il [kit di test offline](/docs/sdk/testing) per generare token per i test del tuo middleware. Il kit non ha un helper per le prove DPoP; firma una prova nel tuo test con la libreria crittografica del linguaggio.
