
# SDK serveur Python

`keenauth-python` est le SDK serveur pour Python 3.10 ou une version ultérieure. Il appelle l'API de gestion avec des méthodes typées et ajoute le comportement commun à tous les SDK serveur KeenAuth : authentification avec une clé d'API ou un jeton machine-à-machine `private_key_jwt`, nouvelles tentatives, pagination par curseur, erreurs typées, vérification des signatures de webhooks et avertissements de dépréciation. Le paquet n'est pas encore publié sur PyPI.

Pour la connexion dans une application web, voir [Django](/docs/sdk/django), [Flask](/docs/sdk/flask) et [FastAPI](/docs/sdk/fastapi). Pour protéger une API qui reçoit des jetons porteurs, voir [Protéger votre API](/docs/sdk/protect-api).

## Installation

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

Le signataire `private_key_jwt` nécessite le paquet `cryptography`. Installez-le avec l'extra `m2m` :

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

## Appeler l'API de gestion avec une clé d'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)
```

Chaque classe d'API générée est disponible en tant qu'attribut : `client.users`, `client.organizations`, `client.api_keys` et ainsi de suite. L'URL de base doit utiliser `https` ; `http` n'est accepté que pour `localhost`. Le SDK ne suit jamais une redirection, afin que votre identifiant ne soit jamais envoyé à un autre hôte.

## Utiliser un jeton de gestion private_key_jwt

Une application de gestion s'authentifie avec sa clé privée au lieu d'une clé d'API. Le SDK signe une assertion client (RFC 7523), obtient un jeton d'API de gestion auprès du point de terminaison de jetons de votre environnement et le renouvelle 60 secondes avant son expiration :

```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'assertion a `iss` et `sub` définis à `client_id`, `aud` défini à l'émetteur, un `jti` aléatoire et une durée de vie de 60 secondes. Son `kid` est l'empreinte RFC 7638 de la clé publique.
- Clés prises en charge : EC P-256 (`ES256`), EC P-384 (`ES384`), Ed25519 (`EdDSA`) et RSA d'au moins 2048 bits (`PS256` par défaut, ou `algorithm="RS256"`).
- Enregistrez la clé publique sur l'application de gestion. `keenauth.m2m.load_private_key(pem).public_jwk()` la renvoie sous forme de JWK.
- Le jeton est mis en cache. Lorsque plusieurs threads ont besoin d'un nouveau jeton en même temps, une seule requête est envoyée au point de terminaison de jetons.
- Une requête refusée lève `keenauth.m2m.TokenError` avec `status`, `error` et `error_description`. Le message ne contient jamais l'assertion ni le jeton.

## Nouvelles tentatives

Le SDK relance une requête qui a échoué avec `429`, `502`, `503` ou `504`, ou avec une erreur de connexion ou un délai dépassé. Il ne relance que les requêtes `GET`, `PUT`, `DELETE` et `POST` qui portent un `Idempotency-Key`. Un `PATCH` qui porte `If-Match` n'est relancé que sur `429` : le limiteur de débit refuse une requête avant son exécution, et `If-Match` empêche une modification d'être appliquée deux fois. Tout autre `PATCH` n'est jamais relancé. Il effectue au maximum 3 tentatives au total.

- Si la réponse contient un en-tête `Retry-After` (secondes ou date HTTP), le SDK attend ce délai. Si `Retry-After` dépasse 60 secondes, le SDK n'attend pas et vous renvoie la réponse.
- Sans `Retry-After`, il attend 0,5 seconde, puis 1 seconde, chacune avec une gigue aléatoire de ±25 %.
- Le SDK ajoute un `Idempotency-Key` UUIDv4 aléatoire à chaque opération `POST` qui en accepte un, sauf si vous passez votre propre `idempotency_key=`. Une nouvelle tentative envoie la même clé, de sorte que le serveur n'exécute l'opération qu'une seule fois.

## Pagination

Les méthodes de liste renvoient une page : `data` et `list_metadata` avec les curseurs `after` et `before`. `paginate` parcourt chaque page et ne récupère la page suivante que lorsque vous l'atteignez :

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

- Les autres arguments, comme les filtres et `order`, sont envoyés avec chaque requête de page.
- Passez `before=` pour parcourir en arrière à partir d'un curseur. `after` et `before` ne peuvent pas être utilisés ensemble.
- `pages(...)` accepte les mêmes arguments et renvoie des pages entières au lieu d'éléments.

## Erreurs

Une réponse d'erreur en `application/problem+json` (RFC 9457) lève `keenauth.ProblemError`. Utilisez ses champs `code`, `status`, `detail`, `request_id` et `errors`. Un `code` que cette version du SDK ne connaît pas est tout de même renvoyé sous forme de chaîne, de sorte que les nouveaux codes d'erreur du serveur ne cassent pas votre code.

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

## Vérifier les webhooks

KeenAuth signe les webhooks avec le schéma Standard Webhooks. `WebhookVerifier` vérifie les en-têtes `webhook-id`, `webhook-timestamp` et `webhook-signature` à l'aide du secret `idp_whsec_` de votre point de terminaison :

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

- Transmettez le corps brut de la requête, avant tout analyse JSON ; un octet modifié fait échouer la vérification.
- L'horodatage doit se situer à 5 minutes près de l'horloge de votre serveur, dans un sens comme dans l'autre.
- Pendant la rotation du secret, transmettez les deux : `WebhookVerifier([new_secret, old_secret])`. Une requête est acceptée lorsqu'une signature de l'en-tête correspond à l'un des secrets.
- Les erreurs sont typées : `WebhookHeadersError`, `WebhookTimestampError`, `WebhookSignatureError` et `WebhookSecretError` (un secret mal formé, levé à la création du vérificateur). Chacune possède un champ `reason`.
- La comparaison s'effectue en temps constant, et le message d'erreur ne contient jamais le secret ni la signature.

## Événements et revendications typés

`keenauth.events` contient une dataclass gelée pour chaque type d'événement client, générée à partir des schémas publics d'événements. `parse_event` vérifie un corps déjà validé et renvoie la classe de son type. `keenauth.webhooks.verify_event` vérifie et analyse en un seul appel. Affinez avec `match` ou `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` accepte des octets, une chaîne ou un mapping déjà décodé. Il vérifie l'enveloppe et les membres que le schéma du type déclare. Un membre absent ou de type incorrect lève `EventParseError` avec son chemin, par exemple `data.object.reason`.
- Un type inconnu, ou un type connu avec un `schema_version` que ce SDK ne modélise pas, correspond à un `UnknownEvent`. Ce n'est pas une erreur.
- Les membres que le schéma ne déclare pas sont conservés dans `raw` (sur l'événement et sur `data.object`).
- Les tableaux sont des tuples. Les valeurs des champs énumérés ne sont pas vérifiées par rapport aux valeurs connues.
- Les modèles passent `mypy --strict`.

`keenauth.claims` type les revendications d'un jeton vérifié : `parse_id_token_claims` (OpenID Connect Core) et `parse_access_token_claims` (RFC 9068). Elles vérifient les types des revendications mais ne vérifient rien ; appelez-les donc sur les `claims` renvoyés par le vérificateur :

```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` est toujours un tuple. Une revendication incompatible lève `ClaimsParseError` avec son nom.

## Avertissements de dépréciation

Lorsqu'une réponse contient un en-tête `Deprecation` (RFC 9745) ou `Sunset` (RFC 8594), le SDK écrit un avertissement par processus dans le journal `keenauth` avec le module standard `logging`. L'avertissement indique la méthode, la ressource et les valeurs des en-têtes ; il ne contient jamais d'identifiants, de chaînes de requête ni d'identifiants d'accès. La requête elle-même n'est pas affectée.

```python
import logging

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

## En-tête de télémétrie

Chaque requête porte l'en-tête `Idp-Client: keenauth-python/<version> (python/<version>)`. Il ne contient aucune donnée personnelle et ne peut pas être désactivé ; KeenAuth l'utilise pour le support et les avis de dépréciation.
