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

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.