
# Python 서버 SDK

`keenauth-python`은 Python 3.10 이상을 위한 서버 SDK입니다. 타입이 지정된 메서드로 관리 API를 호출하며, 모든 KeenAuth 서버 SDK가 공통으로 제공하는 기능을 추가로 제공합니다. 여기에는 API 키 또는 `private_key_jwt` 머신 간 토큰을 사용한 인증, 재시도, 커서 페이지네이션, 타입이 지정된 오류, 웹훅 서명 검사, 지원 중단 경고가 포함됩니다. 이 패키지는 아직 PyPI에 게시되지 않았습니다.

웹 앱에서 로그인을 구현하려면 [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로 반환합니다.
- 토큰은 캐시됩니다. 여러 스레드가 동시에 새 토큰을 필요로 하면 토큰 엔드포인트로 요청이 한 번만 전송됩니다.
- 요청이 거부되면 `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%의 무작위 지터가 적용됩니다.
- SDK는 `idempotency_key=`를 직접 전달하지 않는 한, 키를 받을 수 있는 모든 `POST` 작업에 무작위 UUIDv4 `Idempotency-Key`를 추가합니다. 재시도 시에도 같은 키가 전송되므로 서버는 작업을 한 번만 실행합니다.

## 페이지네이션

목록 메서드는 하나의 페이지를 반환합니다. 이 페이지에는 `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)
```

## 웹훅 검증

KeenAuth는 Standard Webhooks 방식으로 웹훅에 서명합니다. `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을 파싱하기 전의 원본 요청 본문을 전달하십시오. 바이트 하나라도 변경되면 검사에 실패합니다.
- 타임스탬프는 서버 시계와 앞뒤로 5분 이내여야 합니다.
- 시크릿을 교체하는 동안에는 두 값을 모두 전달하십시오. 예: `WebhookVerifier([new_secret, old_secret])`. 헤더의 서명 중 하나라도 어떤 시크릿과 일치하면 요청이 수락됩니다.
- 오류는 타입으로 구분됩니다. `WebhookHeadersError`, `WebhookTimestampError`, `WebhookSignatureError`, `WebhookSecretError`(잘못된 시크릿이며, 검증기를 생성할 때 발생합니다)가 있습니다. 각 오류에는 `reason` 필드가 있습니다.
- 비교는 일정한 시간에 수행되며, 오류 메시지에는 시크릿이나 서명이 포함되지 않습니다.

## 타입이 지정된 이벤트와 클레임

`keenauth.events`에는 공개 이벤트 스키마에서 생성된 모든 고객 이벤트 유형에 대한 frozen dataclass가 있습니다. `parse_event`는 검증된 본문을 확인하고 해당 유형의 클래스를 반환합니다. `keenauth.webhooks.verify_event`는 한 번의 호출로 검증과 파싱을 함께 수행합니다. `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`은 bytes, 문자열, 이미 디코딩된 매핑을 받습니다. 봉투와 해당 유형의 스키마에 선언된 멤버를 확인합니다. 멤버가 없거나 타입이 잘못되면 `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` 모듈을 통해 `keenauth` 로거에 경고를 기록합니다. 경고에는 메서드, 리소스, 헤더 값이 표시되며, ID, 쿼리 문자열, 자격 증명은 포함되지 않습니다. 요청 자체는 영향을 받지 않습니다.

```python
import logging

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

## 텔레메트리 헤더

모든 요청에는 `Idp-Client: keenauth-python/<version> (python/<version>)` 헤더가 포함됩니다. 이 헤더에는 개인 정보가 포함되지 않으며 끌 수 없습니다. KeenAuth는 이 헤더를 지원 및 지원 중단 안내에 사용합니다.
