
# SDK server Python

`keenauth-python` è l'SDK server per Python 3.10 o successivo. Chiama l'API di gestione con metodi tipizzati e aggiunge il comportamento che tutti gli SDK server di KeenAuth condividono: autenticazione con una chiave API o con un token machine-to-machine `private_key_jwt`, retry, paginazione con cursore, errori tipizzati, verifica delle firme dei webhook e avvisi di deprecazione. Il pacchetto non è ancora pubblicato su PyPI.

Per accedere in un'app web, vedi [Django](/docs/sdk/django), [Flask](/docs/sdk/flask) e [FastAPI](/docs/sdk/fastapi). Per proteggere un'API che riceve bearer token, vedi [Proteggere la tua API](/docs/sdk/protect-api).

## Installazione

```sh
pip install keenauth-python
```

Il signer `private_key_jwt` richiede il pacchetto `cryptography`. Installalo con l'extra `m2m`:

```sh
pip install "keenauth-python[m2m]"
```

## Chiamare l'API di gestione con una chiave API

```python
import os
from keenauth import KeenAuth

client = KeenAuth("https://api.eu.keenauth.com/v1", os.environ["KEENAUTH_API_KEY"])

page = client.users.list_users(limit=20)
for user in page.data:
    print(user.id)
```

Ogni classe API generata è disponibile come attributo: `client.users`, `client.organizations`, `client.api_keys` e così via. L'URL di base deve usare `https`; `http` semplice è accettato solo per `localhost`. L'SDK non segue mai un redirect, quindi la tua credenziale non viene mai inviata a un altro host.

## Usare un token di gestione private_key_jwt

Un'applicazione di gestione si autentica con la sua chiave privata invece che con una chiave API. L'SDK firma un'asserzione client (RFC 7523), ottiene un token dell'API di gestione dall'endpoint dei token del tuo ambiente e lo rinnova 60 secondi prima della scadenza:

```python
import os
from keenauth import KeenAuth
from keenauth.m2m import ManagementTokenSource

source = ManagementTokenSource(
    issuer="https://acme.eu.keenauth.app",          # your environment's default host
    client_id="app_01j9zzzzzzzzzzzzzzzzzzzz",       # a management application
    private_key=os.environ["KEENAUTH_PRIVATE_KEY"],  # unencrypted PEM
    resource="https://api.eu.keenauth.com",          # the management API of your region
    scopes=["users:read", "users:write"],            # optional; default: the grant's default scopes
)
client = KeenAuth("https://api.eu.keenauth.com/v1", token_source=source)
```

- L'asserzione ha `iss` e `sub` impostati a `client_id`, `aud` impostato all'issuer, un `jti` casuale e una durata di 60 secondi. Il suo `kid` è il thumbprint RFC 7638 della chiave pubblica.
- Chiavi supportate: EC P-256 (`ES256`), EC P-384 (`ES384`), Ed25519 (`EdDSA`) e RSA di almeno 2048 bit (`PS256` per impostazione predefinita, oppure `algorithm="RS256"`).
- Registra la chiave pubblica sull'applicazione di gestione. `keenauth.m2m.load_private_key(pem).public_jwk()` la restituisce come JWK.
- Il token viene messo in cache. Quando più thread hanno bisogno di un nuovo token nello stesso momento, solo una richiesta arriva all'endpoint dei token.
- Una richiesta rifiutata genera `keenauth.m2m.TokenError` con `status`, `error` e `error_description`. Il messaggio non contiene mai l'asserzione o il token.

## Retry

L'SDK ritenta una richiesta fallita con `429`, `502`, `503` o `504`, oppure con un errore di connessione o un timeout. Ritenta solo richieste `GET`, `PUT`, `DELETE` e `POST` che includono un `Idempotency-Key`. Una `PATCH` che include `If-Match` viene ritentata solo su `429`: il rate limiter rifiuta una richiesta prima che venga eseguita, e `If-Match` impedisce che una modifica venga applicata due volte. Ogni altra `PATCH` non viene mai ritentata. Effettua al massimo 3 tentativi in totale.

- Se la risposta ha un header `Retry-After` (secondi o data HTTP), l'SDK attende quel tempo. Se `Retry-After` supera 60 secondi, l'SDK non attende e ti restituisce la risposta.
- Senza `Retry-After`, attende 0,5 secondi e poi 1 secondo, ciascuno con un jitter casuale di ±25%.
- L'SDK aggiunge un `Idempotency-Key` UUIDv4 casuale a ogni operazione `POST` che lo accetta, a meno che tu non passi il tuo `idempotency_key=`. Un retry invia la stessa chiave, quindi il server esegue l'operazione una sola volta.

## Paginazione

I metodi di elenco restituiscono una pagina: `data` e `list_metadata` con i cursori `after` e `before`. `paginate` scorre tutte le pagine e richiede la pagina successiva solo quando la raggiungi:

```python
from keenauth import paginate

for user in paginate(client.users.list_users, limit=100, status="active"):
    if user.email == "ada@example.com":
        break  # no further page is requested
```

- Gli altri argomenti, come filtri e `order`, vengono inviati con ogni richiesta di pagina.
- Passa `before=` per scorrere all'indietro a partire da un cursore. `after` e `before` non possono essere usati insieme.
- `pages(...)` accetta gli stessi argomenti e restituisce intere pagine invece di singoli elementi.

## Errori

Una risposta di errore in `application/problem+json` (RFC 9457) genera `keenauth.ProblemError`. Usa i suoi campi `code`, `status`, `detail`, `request_id` ed `errors`. Un `code` che questa versione dell'SDK non conosce viene comunque restituito come stringa, quindi i nuovi codici di errore del server non rompono il tuo codice.

```python
from keenauth import ProblemError

try:
    client.users.get_user("usr_01j9zzzzzzzzzzzzzzzzzzzz")
except ProblemError as e:
    if e.code == "not_found":
        ...
    print(e.request_id)
```

## Verificare i webhook

KeenAuth firma i webhook con lo schema Standard Webhooks. `WebhookVerifier` controlla gli header `webhook-id`, `webhook-timestamp` e `webhook-signature` rispetto al segreto `idp_whsec_` del tuo endpoint:

```python
import os
from flask import Flask, request
from keenauth.webhooks import WebhookVerificationError, WebhookVerifier

app = Flask(__name__)
verifier = WebhookVerifier(os.environ["KEENAUTH_WEBHOOK_SECRET"])

@app.post("/webhooks/keenauth")
def keenauth_webhook():
    try:
        verifier.verify(request.get_data(), request.headers)
    except WebhookVerificationError as e:
        return {"error": e.reason}, 400
    event = request.get_json()
    ...
    return "", 204
```

- Passa il corpo grezzo della richiesta, prima di qualsiasi parsing JSON; un byte modificato fa fallire il controllo.
- Il timestamp deve trovarsi entro 5 minuti dall'orologio del tuo server, in entrambe le direzioni.
- Mentre ruoti il segreto, passane due: `WebhookVerifier([new_secret, old_secret])`. Una richiesta viene accettata quando una qualsiasi firma nell'header corrisponde a un qualsiasi segreto.
- Gli errori sono tipizzati: `WebhookHeadersError`, `WebhookTimestampError`, `WebhookSignatureError` e `WebhookSecretError` (un segreto malformato, sollevato quando il verifier viene creato). Ognuno ha un campo `reason`.
- Il confronto avviene in tempo costante, e il messaggio di errore non contiene mai il segreto o la firma.

## Eventi e claim tipizzati

`keenauth.events` contiene una dataclass frozen per ogni tipo di evento cliente, generata dagli schemi pubblici degli eventi. `parse_event` verifica un corpo già validato e restituisce la classe del suo tipo. `keenauth.webhooks.verify_event` verifica e analizza in un'unica chiamata. Restringi il tipo con `match` o `isinstance`:

```python
from keenauth.events import EmailSendingSuspendedEvent, UnknownEvent, UserCreatedEvent
from keenauth.webhooks import verify_event

event = verify_event(verifier, request.get_data(), request.headers)
match event:
    case UserCreatedEvent():
        print(event.data.object.id)
    case EmailSendingSuspendedEvent():
        print(event.data.object.reason)
    case UnknownEvent():
        pass  # a type this SDK version does not know yet; event.type and event.raw are still available
```

- `parse_event` accetta byte, una stringa o un mapping già decodificato. Verifica la busta e i membri che lo schema del tipo dichiara. Un membro mancante o di tipo errato genera `EventParseError` con il suo percorso, come `data.object.reason`.
- Un tipo sconosciuto, o un tipo noto con un `schema_version` che questo SDK non modella, diventa un `UnknownEvent`. Non è un errore.
- I membri che lo schema non dichiara vengono conservati in `raw` (sull'evento e su `data.object`).
- Gli array sono tuple. I valori dei campi enumerati non vengono verificati rispetto ai valori noti.
- I modelli superano `mypy --strict`.

`keenauth.claims` tipizza i claim di un token verificato: `parse_id_token_claims` (OpenID Connect Core) e `parse_access_token_claims` (RFC 9068). Controllano i tipi dei claim ma non verificano nulla, quindi chiamali sui `claims` che restituisce il verifier:

```python
from keenauth.claims import parse_access_token_claims

at = verifier.verify_access_token(token)
claims = parse_access_token_claims(at.claims)
claims.client_id     # str
claims.org_roles     # tuple[str, ...] | None
claims.custom        # your own claims
```

`aud` è sempre una tupla. Un claim non valido genera `ClaimsParseError` con il suo nome.

## Avvisi di deprecazione

Quando una risposta ha un header `Deprecation` (RFC 9745) o `Sunset` (RFC 8594), l'SDK scrive un avviso per processo nel logger `keenauth` con il modulo standard `logging`. L'avviso indica il metodo, la risorsa e i valori dell'header; non contiene mai ID, query string o credenziali. La richiesta stessa non è influenzata.

```python
import logging

logging.getLogger("keenauth").setLevel(logging.WARNING)
```

## Header di telemetria

Ogni richiesta include `Idp-Client: keenauth-python/<version> (python/<version>)`. Non contiene dati personali e non può essere disattivato; KeenAuth lo usa per il supporto e per gli avvisi di deprecazione.
