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

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.

  1. Nella dashboard, crea un endpoint webhook con l'URL https://<your host>/webhooks/keenauth che si iscrive a token.revoked, ed esporta il suo segreto di firma come KEENAUTH_WEBHOOK_SECRET.
  2. Il gestore verifica la firma con l'SDK sul body grezzo e risponde 400 quando 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 risponde 204; il middleware poi rifiuta ogni token corrispondente emesso prima di not_before.
  3. KeenAuth consegna almeno una volta, quindi il gestore applica ogni webhook-id una sola volta. Se il marcatore non può essere salvato risponde 503 e 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