
# Proteggi un'API FastAPI

Questa guida rapida protegge un'API FastAPI con token di accesso KeenAuth. Aggiungi la dipendenza `RequireAuth` dell'SDK Python, richiedi una scope su una route e testi l'API offline con il kit di test dell'SDK.

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 la scope `messages:read` |

## Prima di iniziare

- Un ambiente KeenAuth. Il suo issuer ha il formato `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.
- La scope `messages:read` su quell'API, concessa all'applicazione che la chiama.
- Python 3.10 o successivo.

## 1. Installa l'SDK

```sh
pip install "keenauth-python[fastapi]" uvicorn
```

L'SDK non è ancora pubblicato. Finché non lo sarà, l'esempio lo installa dal codice sorgente dell'SDK. L'esempio fissa ogni pacchetto a una versione esatta in `requirements.txt`.

## 2. Configura

L'API legge la configurazione solo dalle variabili d'ambiente:

| Variabile | Obbligatoria | Significato |
|---|---|---|
| `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, durante la rotazione). Senza di essa l'API non viene mai a sapere dei token revocati |

## 3. Proteggi le tue route

`create_app` crea il protector e le route. `Depends(RequireAuth(protector))` richiede un token valido e restituisce i suoi claim verificati; `scopes=[...]` richiede inoltre delle scope. `install(app)` rende gli errori come risposte `application/problem+json`, con un header `WWW-Authenticate` per ogni challenge.

```python
def create_app(
    verifier: AccessTokenVerifier,
    *,
    public_base_url: str,
    revocation: Optional[RevocationChecker] = None,
    webhook_secrets: Sequence[str] = (),
) -> FastAPI:
    """Builds the API. The verifier is a parameter so tests can pass the offline test kit's verifier. With
    webhook_secrets (the idp_whsec_ secrets of your webhook endpoint) the token.revoked webhook is served and feeds
    `revocation`, which must then be a MemoryRevocationCache."""
    protector = Protector(
        verifier,
        public_base_url=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),
    )
    app = FastAPI()
    install(app)  # problem+json responses and one WWW-Authenticate header per challenge

    @app.get("/api/public")
    def public() -> dict[str, Any]:
        # No token needed.
        return {"message": "Hello from a public endpoint."}

    @app.get("/api/private")
    def private(claims: AccessClaims = Depends(RequireAuth(protector))) -> dict[str, Any]:
        # Any valid access token for this API.
        return {"message": "Hello from a private endpoint.", "sub": claims.subject}

    @app.get("/api/messages")
    def messages(claims: AccessClaims = Depends(RequireAuth(protector, scopes=["messages:read"]))) -> dict[str, Any]:
        # A valid access token that also carries the messages:read scope.
        return {"messages": ["Hello from a scoped endpoint."]}

    # token.revoked deliveries from KeenAuth.
    if webhook_secrets:
        if not isinstance(revocation, MemoryRevocationCache):
            raise ValueError("the token.revoked webhook needs a MemoryRevocationCache to feed")
        app.include_router(webhook_router(RevocationWebhook(webhook_secrets, revocation)))
    else:
        log.warning("KEENAUTH_WEBHOOK_SECRET is not set: token.revoked events are not received, so revoked tokens stay valid until they expire")

    return app
```

`app_from_env` costruisce il verifier reale dalla configurazione. Il verifier recupera le chiavi di firma dal tuo ambiente la prima volta che vede un token. La dipendenza è sincrona, quindi FastAPI la esegue nel suo thread pool.

```python
def app_from_env() -> FastAPI:
    """The production app: KEENAUTH_ISSUER, KEENAUTH_AUDIENCE and PUBLIC_BASE_URL are required."""
    issuer = os.environ.get("KEENAUTH_ISSUER", "")
    audience = os.environ.get("KEENAUTH_AUDIENCE", "")
    # The URL clients call: DPoP proofs are checked against it, never against the Host header.
    public_base_url = os.environ.get("PUBLIC_BASE_URL", "")
    if not issuer or not audience or not public_base_url:
        raise SystemExit("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=issuer, audience=audience))
    # Fed by the token.revoked webhook.
    revocation = MemoryRevocationCache()
    # The signing secret (idp_whsec_...) of the webhook endpoint that delivers token.revoked; comma separated while rotating.
    webhook_secrets = [x.strip() for x in os.environ.get("KEENAUTH_WEBHOOK_SECRET", "").split(",") if x.strip()]
    return create_app(verifier, public_base_url=public_base_url, revocation=revocation, webhook_secrets=webhook_secrets)
```

## 4. Avvia e chiama l'API

```sh
export KEENAUTH_ISSUER=https://your-environment.eu.keenauth.app
export KEENAUTH_AUDIENCE=https://api.example.com
export PUBLIC_BASE_URL=https://api.example.com
uvicorn main:app_from_env --factory --port 8080
```

Per una prima chiamata, ottieni un token con il grant client credentials di un'applicazione machine-to-machine che ha la 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` emette token firmati con una chiave di test pubblica, e `testing.verifier` accetta solo quei token. I test passano quel verifier a `create_app` e chiamano l'app con il test client di Starlette, quindi non richiedono rete né un account KeenAuth. Vedi [Kit di test offline](/docs/sdk/testing).

```python
@pytest.fixture()
def api() -> Api:
    cache = MemoryRevocationCache()
    app = create_app(
        testing.verifier(audience=AUDIENCE),
        public_base_url=BASE_URL,
        revocation=cache,
        webhook_secrets=[testing.webhook_secret()],
    )
    return TestClient(app, base_url=BASE_URL), cache
```
```python
def test_scope(api: Api) -> None:
    client, _ = api
    no_scope = testing.mint_access_token(AUDIENCE)
    assert_problem(client.get("/api/messages", headers=bearer(no_scope)), 403, "insufficient_scope", 'scope="messages:read"')
    with_scope = testing.mint_access_token(AUDIENCE, claims={"scope": "profile messages:read"})
    assert client.get("/api/messages", headers=bearer(with_scope)).status_code == 200
```

```sh
pip install --no-deps -r requirements-dev.txt
python -m pytest -q
```

I test dell'esempio verificano che un token valido venga accettato e che un token mancante, scaduto o con audience errata, una 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 `app_from_env` rifiuti l'issuer di test. I test inviano inoltre consegne `token.revoked` firmate: una valida revoca il token, una falsificata, alterata o obsoleta riceve `400` e non revoca nulla, e una ripetuta produce un solo effetto.

## Risposte

| Richiesta | Stato | `code` nel corpo |
|---|---|---|
| Nessun header `Authorization` | `401` | `missing_token` |
| Token scaduto, token per un'altra API, firma non valida, token revocato | `401` | `invalid_token` |
| Token senza la scope richiesta | `403` | `insufficient_scope` |
| Token nella query dell'URL (`?access_token=`) | `400` | `invalid_request` |

Il corpo è `application/problem+json` e l'header `WWW-Authenticate` contiene la challenge standard. 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. È obbligatoria e deve usare `https` (`http` solo per `localhost`, `127.0.0.1` o `[::1]`); l'app rifiuta di avviarsi senza di essa. Il middleware non legge mai gli header `X-Forwarded-*` da solo. Uvicorn, però, applica `X-Forwarded-Proto` all'URL della richiesta quando la connessione proviene da un indirizzo in `--forwarded-allow-ips` (per impostazione predefinita `127.0.0.1,::1`); con `PUBLIC_BASE_URL` impostato, l'URL verificato non dipende da questo.

## 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` (`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 corpo grezzo e risponde `400` quando non è valida o quando il timestamp differisce di più di cinque minuti. Mappa il selettore dell'evento (`access_token`, `session`, `user`, `client`, `org`, `org_user`, `client_user`) a un marcatore di revoca e risponde `204`; il middleware rifiuta quindi 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 corpo, una firma o il segreto. Ogni processo worker ha una 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 tutte le opzioni.
- [Verificare i token](/docs/sdk/token-verification): il verifier sotto il middleware.
- [Kit di test offline](/docs/sdk/testing).
