Proteggi un'API Django
Questa guida rapida protegge un'API Django con i token di accesso KeenAuth. Aggiungi il decorator @protect per le view dell'SDK Python, richiedi uno scope su una view e testa l'API offline con il kit di test dell'SDK. L'esempio usa Django 5.2 e nessun database.
L'API ha tre endpoint:
| Endpoint | Richiede |
|---|---|
GET /api/public |
nulla |
GET /api/private |
un token di accesso valido per la tua API |
GET /api/messages |
un token di accesso valido con lo scope messages:read |
Prima di iniziare
- Un ambiente KeenAuth. Il suo issuer ha questa forma:
https://your-environment.eu.keenauth.app. - Un'API registrata nell'ambiente (Crea un'API). Il suo
identifier, per esempiohttps://api.example.com, è l'audience dei token che la tua API accetta. - Lo scope
messages:readsu quell'API, concesso all'applicazione che la chiama. - Python 3.10 o successivo.
1. Installa l'SDK
pip install "keenauth-python[django]"
L'SDK non è ancora pubblicato. Finché non lo sarà, l'esempio lo installa dal sorgente dell'SDK. L'esempio fissa ogni pacchetto a una versione esatta in requirements.txt.
2. Configura
project/settings.py legge ogni valore dalle variabili d'ambiente:
| Variabile | Obbligatoria | Significato |
|---|---|---|
DJANGO_SECRET_KEY |
sì | La secret key di Django; generane una e non committarla mai |
DJANGO_ALLOWED_HOSTS |
no | Nomi host separati da virgola, predefinito localhost,127.0.0.1 |
KEENAUTH_ISSUER |
sì | L'issuer del tuo ambiente |
KEENAUTH_AUDIENCE |
sì | L'identificatore della tua API |
PUBLIC_BASE_URL |
sì | L'URL che i client chiamano (vedi Dietro un proxy) |
KEENAUTH_WEBHOOK_SECRET |
consigliata | Il segreto di firma idp_whsec_ dell'endpoint webhook che consegna token.revoked (due, separati da virgola, mentre lo ruoti). Senza di esso l'API non viene mai a sapere dei token revocati |
3. Proteggi le tue view
@protect(protector) richiede un token valido e inserisce le claim verificate in request.auth; scopes=[...] richiede anche degli scope. Un controllo non superato restituisce una risposta application/problem+json prima che la view venga eseguita.
@require_GET
def public(request: HttpRequest) -> JsonResponse:
# No token needed.
return JsonResponse({"message": "Hello from a public endpoint."})
@require_GET
@protect(protector)
def private(request: HttpRequest) -> JsonResponse:
# Any valid access token for this API; the verified claims are in request.auth.
return JsonResponse({"message": "Hello from a private endpoint.", "sub": request.auth.subject})
@require_GET
@protect(protector, scopes=["messages:read"])
def messages(request: HttpRequest) -> JsonResponse:
# A valid access token that also carries the messages:read scope.
return JsonResponse({"messages": ["Hello from a scoped endpoint."]})
api/auth.py costruisce il protector dalle impostazioni all'avvio (ApiConfig.ready in api/apps.py), quindi un'impostazione mancante ferma il server prima della prima richiesta. Il verificatore scarica le chiavi di firma dal tuo ambiente la prima volta che vede un token.
@cache
def get_protector() -> Protector:
if not settings.KEENAUTH_ISSUER or not settings.KEENAUTH_AUDIENCE or not settings.KEENAUTH_PUBLIC_BASE_URL:
raise ImproperlyConfigured("KEENAUTH_ISSUER, KEENAUTH_AUDIENCE and PUBLIC_BASE_URL are required")
# The verifier refuses an issuer under the reserved .invalid domain, so a test token can never pass here.
verifier = Verifier(VerifierConfig(issuer=settings.KEENAUTH_ISSUER, audience=settings.KEENAUTH_AUDIENCE))
return Protector(
verifier,
public_base_url=settings.KEENAUTH_PUBLIC_BASE_URL,
revocation=revocation,
# The error never contains the token or claim values; never log the Authorization or DPoP header.
on_error=lambda err: log.warning("rejected: %s", err),
)
@cache
def get_webhook() -> Optional[RevocationWebhook]:
"""The token.revoked webhook, or None when KEENAUTH_WEBHOOK_SECRET is not set. A malformed secret raises
WebhookSecretError (without echoing it), so ApiConfig.ready stops the server at startup."""
if not settings.KEENAUTH_WEBHOOK_SECRETS:
return None
return RevocationWebhook(settings.KEENAUTH_WEBHOOK_SECRETS, revocation)
class ProtectorProxy:
"""Hands each request to `get_protector()`, so the `@protect` decorators can be applied at import time."""
def authenticate(self, req: RequestInfo, requirement: Optional[Requirement] = None) -> AccessClaims:
return get_protector().authenticate(req, requirement)
protector = ProtectorProxy()
4. Avvia e chiama l'API
export DJANGO_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(50))')"
export KEENAUTH_ISSUER=https://your-environment.eu.keenauth.app
export KEENAUTH_AUDIENCE=https://api.example.com
export PUBLIC_BASE_URL=https://api.example.com
python manage.py runserver 8080
runserver serve solo per lo sviluppo; in produzione avvia project.wsgi sotto un server WSGI.
Per una prima chiamata, ottieni un token con il client credentials grant di un'applicazione machine-to-machine che ha lo scope messages:read sulla tua API:
ACCESS_TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials \
-d resource=https://api.example.com \
-d scope=messages:read \
https://your-environment.eu.keenauth.app/oauth2/token | jq -r .access_token)
curl -i http://localhost:8080/api/messages -H "Authorization: Bearer $ACCESS_TOKEN"
5. Testa senza rete
keenauth.testing genera token firmati con una chiave di test pubblica, e testing.verifier accetta solo questi token. I test sostituiscono get_protector con un protector che usa quel verificatore, quindi non richiedono rete, un account KeenAuth né un interruttore di test nelle impostazioni. Vedi Kit di test offline.
class ProtectedAPITest(SimpleTestCase):
def setUp(self) -> None:
self.cache = MemoryRevocationCache()
test_protector = Protector(testing.verifier(audience=AUDIENCE), public_base_url=BASE_URL, revocation=self.cache)
patcher = mock.patch.object(auth, "get_protector", return_value=test_protector)
patcher.start()
self.addCleanup(patcher.stop)
def test_scope(self) -> None:
self.assert_problem(self.get("/api/messages", testing.mint_access_token(AUDIENCE)), 403, "insufficient_scope",
'scope="messages:read"')
tok = testing.mint_access_token(AUDIENCE, claims={"scope": "profile messages:read"})
self.assertEqual(self.get("/api/messages", tok).status_code, 200)
set -a; . ./.env.example; set +a
DJANGO_SECRET_KEY=insecure-test-only python manage.py test
I test dell'esempio verificano che un token valido venga accettato e che un token mancante, scaduto o con audience errata, uno scope mancante, un token nella query dell'URL e un token revocato vengano rifiutati con gli stati indicati sotto. Inviano anche un token vincolato a DPoP con una prova per PUBLIC_BASE_URL, e verificano che il protector di produzione rifiuti l'issuer di test. I test inviano anche consegne token.revoked firmate: una valida revoca il token, una falsificata, alterata o obsoleta riceve 400 e non revoca nulla, e una ripetuta ha un solo effetto.
Risposte
| Richiesta | Stato | code nel body |
|---|---|---|
Nessun header Authorization |
401 |
missing_token |
| Token scaduto, token per un'altra API, firma non valida, token revocato | 401 |
invalid_token |
| Token senza lo scope richiesto | 403 |
insufficient_scope |
Token nella query dell'URL (?access_token=) |
400 |
invalid_request |
Il body è application/problem+json e l'header WWW-Authenticate contiene la challenge standard; Django unisce più challenge in un unico header. La tabella completa è in Proteggere la tua API con il middleware.
Dietro un proxy
Se i client usano DPoP, il middleware verifica che ogni prova sia stata creata per l'URL chiamato dal client. Imposta PUBLIC_BASE_URL sull'URL esterno che i client chiamano, per esempio https://api.example.com, anche dietro un proxy o un load balancer. È obbligatorio e deve usare https (http solo per localhost, 127.0.0.1 o [::1]); l'app si rifiuta di avviarsi senza di esso. Il middleware non legge mai gli header X-Forwarded-* da sé; Django li applica solo se imposti SECURE_PROXY_SSL_HEADER o USE_X_FORWARDED_HOST, e con PUBLIC_BASE_URL impostato l'URL verificato non dipende da essi.
Token revocati
Un token di accesso resta valido fino alla scadenza, quindi KeenAuth comunica alla tua API quando i token vengono revocati. L'esempio passa al protector una cache di revoca in memoria e serve un webhook token.revoked su POST /webhooks/keenauth (api/webhook.py) che la alimenta.
- Nella dashboard, crea un endpoint webhook con l'URL
https://<your host>/webhooks/keenauthche si iscrive atoken.revoked, ed esporta il suo segreto di firma comeKEENAUTH_WEBHOOK_SECRET. - Il gestore verifica la firma con l'SDK sul body grezzo e risponde
400quando non è valida o quando il timestamp differisce di più di cinque minuti. Associa il selettore dell'evento (access_token,session,user,client,org,org_user,client_user) a un marcatore di revoca e risponde204; il middleware poi rifiuta ogni token corrispondente emesso prima dinot_before. - KeenAuth consegna almeno una volta, quindi il gestore applica ogni
webhook-iduna sola volta. Se il marcatore non può essere salvato risponde503e KeenAuth consegna di nuovo l'evento. Ignora gli altri tipi di evento e i selettori che nessun token di accesso può corrispondere (refresh_family,api).
Il gestore registra l'id dell'evento, il tipo di selettore e il motivo, mai il body, una firma o il segreto. Ogni processo worker ha la propria cache; con più worker o istanze, usa invece un archivio condiviso. Vedi Cache di revoca.
Prossimi passi
- Proteggere la tua API con il middleware: Flask, requisiti di step-up, DPoP e ogni opzione.
- Verificare i token: il verificatore sotto il middleware.
- Kit di test offline.