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 e non aggiunge una logica propria sui token.
Cosa fa il middleware
- Legge il token dall'header
Authorizationcon lo schemaBeareroDPoP. Un token nella query dell'URL (?access_token=) viene sempre rifiutato, perché gli URL finiscono nei log e negli headerReferer. Viene rifiutata anche una richiesta con più di un headerAuthorizationo con più di una credenziale. - Controlla DPoP. Con lo schema
DPoP, l'header della provaDPoP, 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 schemaBearerviene rifiutato, così come lo schemaDPoPcon un token non vincolato a DPoP. - Controlla i requisiti della route: gli scope richiesti (tutti) e, facoltativamente, il step-up: i valori
acraccettabili 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.authin Express e Fastify,request.authin Django,g.authin Flask e il valore restituito dalla dipendenza in FastAPI. - Risponde agli errori con la sfida standard
WWW-Authenticatee un piccolo corpoapplication/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.
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.
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.
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:
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:
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:
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.
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 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.