
# 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](/docs/api/management/apis/create)). Il suo `identifier`, per esempio `https://api.example.com`, è l'audience dei token che la tua API accetta.
- Lo scope `messages:read` su quell'API, concesso all'applicazione che la chiama.
- Python 3.10 o successivo.

## 1. Installa l'SDK

```sh
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](#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.

```python
@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.

```python
@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

```sh
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:

```sh
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](/docs/sdk/testing).

```python
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)
```

```python
    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)
```

```sh
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](/docs/sdk/protect-api#risposte).

## 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](/docs/sdk/protect-api#cache-di-revoca-tokenrevoked).

## Prossimi passi

- [Proteggere la tua API con il middleware](/docs/sdk/protect-api): Flask, requisiti di step-up, DPoP e ogni opzione.
- [Verificare i token](/docs/sdk/token-verification): il verificatore sotto il middleware.
- [Kit di test offline](/docs/sdk/testing).
