
# Python サーバー SDK

`keenauth-python` は Python 3.10 以降向けのサーバー SDK です。型付きメソッドで管理 API を呼び出し、すべての KeenAuth サーバー SDK に共通する機能を提供します。具体的には、API キーまたは `private_key_jwt` のマシン間 (M2M) トークンによる認証、リトライ、カーソルによるページネーション、型付きエラー、Webhook の署名検証、非推奨警告です。このパッケージはまだ PyPI に公開されていません。

Web アプリでのサインインについては、[Django](/docs/sdk/django)、[Flask](/docs/sdk/flask)、[FastAPI](/docs/sdk/fastapi) を参照してください。ベアラートークンを受け取る API を保護するには、[API の保護](/docs/sdk/protect-api) を参照してください。

## インストール

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

`private_key_jwt` の署名には `cryptography` パッケージが必要です。`m2m` エクストラを指定してインストールします:

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

## API キーで管理 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)
```

生成されたすべての API クラスは属性として利用できます: `client.users`、`client.organizations`、`client.api_keys` など。ベース URL は `https` を使用する必要があり、平文の `http` は `localhost` の場合のみ許可されます。SDK はリダイレクトを追跡しないため、認証情報が別のホストに送信されることはありません。

## private_key_jwt の管理トークンを使用する

管理アプリケーションは API キーの代わりに秘密鍵で認証します。SDK はクライアントアサーション (RFC 7523) に署名し、環境のトークンエンドポイントから管理 API トークンを取得し、有効期限の 60 秒前に更新します:

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

- アサーションでは `iss` と `sub` に `client_id` を、`aud` に発行者を設定し、ランダムな `jti` と 60 秒の有効期間を持たせます。`kid` は公開鍵の RFC 7638 サムプリントです。
- 対応する鍵: EC P-256 (`ES256`)、EC P-384 (`ES384`)、Ed25519 (`EdDSA`)、および 2048 ビット以上の RSA (既定は `PS256`、または `algorithm="RS256"`)。
- 公開鍵は管理アプリケーションに登録してください。`keenauth.m2m.load_private_key(pem).public_jwk()` は公開鍵を JWK として返します。
- トークンはキャッシュされます。複数のスレッドが同時に新しいトークンを必要とした場合、トークンエンドポイントへのリクエストは 1 回だけ送信されます。
- 拒否されたリクエストは `keenauth.m2m.TokenError` を送出し、`status`、`error`、`error_description` を持ちます。メッセージにアサーションやトークンが含まれることはありません。

## リトライ

SDK は `429`、`502`、`503`、`504` で失敗したリクエスト、または接続エラーやタイムアウトになったリクエストをリトライします。リトライするのは `GET`、`PUT`、`DELETE` と、`Idempotency-Key` を含む `POST` リクエストのみです。`If-Match` を含む `PATCH` は `429` の場合のみリトライされます。レート制限は処理前にリクエストを拒否するため、また `If-Match` により変更が二重に適用されることを防ぐためです。それ以外の `PATCH` はリトライされません。合計で最大 3 回試行します。

- レスポンスに `Retry-After` ヘッダー (秒数または HTTP 日付) がある場合、SDK はその時間だけ待機します。`Retry-After` が 60 秒を超える場合、SDK は待機せずにレスポンスを返します。
- `Retry-After` がない場合、0.5 秒、次に 1 秒待機します。それぞれに ±25% のランダムなジッターが加わります。
- 受け付ける `POST` 操作には、`idempotency_key=` を指定しない限り、SDK がランダムな UUIDv4 の `Idempotency-Key` を付与します。リトライでは同じキーが送信されるため、サーバーは操作を 1 回だけ実行します。

## ページネーション

一覧メソッドは 1 ページを返します。`data` と、`after` および `before` カーソルを含む `list_metadata` です。`paginate` はすべてのページを順にたどり、必要になったときにのみ次のページを取得します:

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

- フィルターや `order` などの他の引数は、すべてのページのリクエストに送信されます。
- カーソルから逆方向にたどるには `before=` を指定します。`after` と `before` は同時に使用できません。
- `pages(...)` は同じ引数を受け取り、項目ではなくページ全体を返します。

## エラー

`application/problem+json` (RFC 9457) 形式のエラーレスポンスは `keenauth.ProblemError` を送出します。`code`、`status`、`detail`、`request_id`、`errors` フィールドを使用できます。この SDK バージョンが認識しない `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)
```

## Webhook を検証する

KeenAuth は Standard Webhooks 方式で Webhook に署名します。`WebhookVerifier` は `webhook-id`、`webhook-timestamp`、`webhook-signature` ヘッダーを、エンドポイントの `idp_whsec_` シークレットと照合して検証します:

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

- JSON の解析より前の、生のリクエストボディを渡してください。1 バイトでも変更されると検証に失敗します。
- タイムスタンプは、サーバーの時刻から前後 5 分以内である必要があります。
- シークレットをローテーションする間は、両方を渡してください: `WebhookVerifier([new_secret, old_secret])`。ヘッダー内のいずれかの署名がいずれかのシークレットと一致すれば、リクエストは受け付けられます。
- エラーは型付きです: `WebhookHeadersError`、`WebhookTimestampError`、`WebhookSignatureError`、`WebhookSecretError` (シークレットの形式が不正な場合。検証器の作成時に送出されます)。各エラーは `reason` フィールドを持ちます。
- 比較は定数時間で行われ、エラーメッセージにシークレットや署名が含まれることはありません。

## 型付きイベントとクレーム

`keenauth.events` には、公開されているイベントスキーマから生成された、すべての顧客向けイベントタイプごとの frozen dataclass があります。`parse_event` は検証済みのボディを検査し、そのタイプに対応するクラスを返します。`keenauth.webhooks.verify_event` は検証と解析を 1 回の呼び出しで行います。`match` または `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` はバイト列、文字列、または既にデコードされたマッピングを受け付けます。エンベロープと、タイプのスキーマで宣言されたメンバーを検査します。メンバーが欠けている場合や型が誤っている場合は、`data.object.reason` のようなパスとともに `EventParseError` を送出します。
- 未知のタイプ、またはこの SDK が対応していない `schema_version` を持つ既知のタイプは `UnknownEvent` になります。これはエラーではありません。
- スキーマで宣言されていないメンバーは `raw` (イベントおよび `data.object` の両方) に保持されます。
- 配列はタプルになります。列挙型フィールドの値は、既知の値と照合されません。
- モデルは `mypy --strict` に合格します。

`keenauth.claims` は、検証済みトークンのクレームに型を付けます: `parse_id_token_claims` (OpenID Connect Core) と `parse_access_token_claims` (RFC 9068) です。これらはクレームの型を検査しますが、検証は行いません。検証器が返す `claims` に対して呼び出してください:

```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` は常にタプルです。対応しないクレームがあると、そのクレーム名を含む `ClaimsParseError` が送出されます。

## 非推奨警告

レスポンスに `Deprecation` (RFC 9745) または `Sunset` (RFC 8594) ヘッダーがある場合、SDK は標準の `logging` モジュールを使い、プロセスごとに 1 回、`keenauth` ロガーへ警告を出力します。警告にはメソッド、リソース、ヘッダーの値が含まれます。ID、クエリ文字列、認証情報が含まれることはありません。リクエスト自体には影響しません。

```python
import logging

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

## テレメトリヘッダー

すべてのリクエストには `Idp-Client: keenauth-python/<version> (python/<version>)` が含まれます。個人データは含まれず、無効にすることはできません。KeenAuth はサポートおよび非推奨の通知に使用します。
