
# Django API を保護する

このクイックスタートでは、KeenAuth のアクセストークンで Django API を保護します。Python SDK の `@protect` ビューデコレーターを追加し、1 つのビューでスコープを必須にし、SDK のテストキットで API をオフラインでテストします。例では Django 5.2 を使用し、データベースは使用しません。

API には 3 つのエンドポイントがあります:

| エンドポイント | 必要条件 |
|---|---|
| `GET /api/public` | 不要 |
| `GET /api/private` | API に対する有効なアクセストークン |
| `GET /api/messages` | `messages:read` スコープを持つ有効なアクセストークン |

## 始める前に

- KeenAuth の環境。その発行者（issuer）は `https://your-environment.eu.keenauth.app` のような形式です。
- 環境に登録された API（[API を作成する](/docs/api/management/apis/create)）。その `identifier`（例: `https://api.example.com`）が、API が受け付けるトークンのオーディエンスです。
- その API 上の `messages:read` スコープ。API を呼び出すアプリケーションに付与されていること。
- Python 3.10 以降。

## 1. SDK をインストールする

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

SDK はまだ公開されていません。公開されるまでは、例では SDK のソースからインストールします。例ではすべてのパッケージを `requirements.txt` 内で正確なバージョンに固定しています。

## 2. 設定する

`project/settings.py` はすべての値を環境変数から読み込みます:

| 変数 | 必須 | 意味 |
|---|---|---|
| `DJANGO_SECRET_KEY` | はい | Django のシークレットキー。生成したうえで、決してコミットしないでください |
| `DJANGO_ALLOWED_HOSTS` | いいえ | カンマ区切りのホスト名。既定値は `localhost,127.0.0.1` |
| `KEENAUTH_ISSUER` | はい | 環境の発行者 |
| `KEENAUTH_AUDIENCE` | はい | API の識別子 |
| `PUBLIC_BASE_URL` | はい | クライアントが呼び出す URL（[プロキシ経由での利用](#プロキシ経由での利用)を参照） |
| `KEENAUTH_WEBHOOK_SECRET` | 推奨 | `idp_whsec_` の署名シークレット（`token.revoked` を配信する Webhook エンドポイントのもの。ローテーション中はカンマ区切りで 2 つ指定します）。これがないと API は取り消されたトークンを検知できません |

## 3. ビューを保護する

`@protect(protector)` は有効なトークンを必須とし、検証済みのクレームを `request.auth` に格納します。`scopes=[...]` はさらにスコープも必須にします。チェックに失敗すると、ビューが実行される前に `application/problem+json` レスポンスを返します。

```python
@require_GET
def public(request: HttpRequest) -> JsonResponse:
    # No token needed.
    return JsonResponse({"message": "Hello from a public endpoint."})


@require_GET
@protect(protector)
def private(request: HttpRequest) -> JsonResponse:
    # Any valid access token for this API; the verified claims are in request.auth.
    return JsonResponse({"message": "Hello from a private endpoint.", "sub": request.auth.subject})


@require_GET
@protect(protector, scopes=["messages:read"])
def messages(request: HttpRequest) -> JsonResponse:
    # A valid access token that also carries the messages:read scope.
    return JsonResponse({"messages": ["Hello from a scoped endpoint."]})
```

`api/auth.py` は起動時に設定からプロテクターを構築します（`ApiConfig.ready` は `api/apps.py` 内）。そのため、設定が欠けているとリクエストを受ける前にサーバーが停止します。検証器は、最初にトークンを受け取ったときに、環境から署名鍵を取得します。

```python
@cache
def get_protector() -> Protector:
    if not settings.KEENAUTH_ISSUER or not settings.KEENAUTH_AUDIENCE or not settings.KEENAUTH_PUBLIC_BASE_URL:
        raise ImproperlyConfigured("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=settings.KEENAUTH_ISSUER, audience=settings.KEENAUTH_AUDIENCE))
    return Protector(
        verifier,
        public_base_url=settings.KEENAUTH_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),
    )


@cache
def get_webhook() -> Optional[RevocationWebhook]:
    """The token.revoked webhook, or None when KEENAUTH_WEBHOOK_SECRET is not set. A malformed secret raises
    WebhookSecretError (without echoing it), so ApiConfig.ready stops the server at startup."""
    if not settings.KEENAUTH_WEBHOOK_SECRETS:
        return None
    return RevocationWebhook(settings.KEENAUTH_WEBHOOK_SECRETS, revocation)


class ProtectorProxy:
    """Hands each request to `get_protector()`, so the `@protect` decorators can be applied at import time."""

    def authenticate(self, req: RequestInfo, requirement: Optional[Requirement] = None) -> AccessClaims:
        return get_protector().authenticate(req, requirement)


protector = ProtectorProxy()
```

## 4. 実行して API を呼び出す

```sh
export DJANGO_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(50))')"
export KEENAUTH_ISSUER=https://your-environment.eu.keenauth.app
export KEENAUTH_AUDIENCE=https://api.example.com
export PUBLIC_BASE_URL=https://api.example.com
python manage.py runserver 8080
```

`runserver` は開発専用です。本番環境では WSGI サーバーの下で `project.wsgi` を実行してください。

最初の呼び出しには、`messages:read` スコープを API に持つマシン間（machine-to-machine）アプリケーションの client credentials グラントでトークンを取得します:

```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. ネットワークなしでテストする

`keenauth.testing` は公開テスト鍵で署名したトークンを発行し、`testing.verifier` はそれらのトークンのみを受け付けます。テストでは `get_protector` を、その検証器を使うプロテクターに置き換えるため、ネットワーク、KeenAuth アカウント、設定内のテスト用スイッチのいずれも不要です。[オフラインテストキット](/docs/sdk/testing)を参照してください。

```python
class ProtectedAPITest(SimpleTestCase):
    def setUp(self) -> None:
        self.cache = MemoryRevocationCache()
        test_protector = Protector(testing.verifier(audience=AUDIENCE), public_base_url=BASE_URL, revocation=self.cache)
        patcher = mock.patch.object(auth, "get_protector", return_value=test_protector)
        patcher.start()
        self.addCleanup(patcher.stop)
```

```python
    def test_scope(self) -> None:
        self.assert_problem(self.get("/api/messages", testing.mint_access_token(AUDIENCE)), 403, "insufficient_scope",
                            'scope="messages:read"')
        tok = testing.mint_access_token(AUDIENCE, claims={"scope": "profile messages:read"})
        self.assertEqual(self.get("/api/messages", tok).status_code, 200)
```

```sh
set -a; . ./.env.example; set +a
DJANGO_SECRET_KEY=insecure-test-only python manage.py test
```

例のテストでは、有効なトークンが受け入れられること、ならびに、トークンがない、期限切れ、対象が別 API、スコープ不足、URL クエリ内のトークン、取り消されたトークンがいずれも以下のステータスで拒否されることを確認します。また、`PUBLIC_BASE_URL` 向けの証明を伴う DPoP バインドのトークンを送信し、本番用プロテクターがテスト発行者を拒否することも確認します。さらに、署名付きの `token.revoked` 配信も送信します。有効なものはトークンを取り消し、偽造・改ざん・古いものは `400` を返して何も取り消さず、同じものを繰り返した場合の効果は 1 回だけです。

## レスポンス

| リクエスト | ステータス | 本文中の `code` |
|---|---|---|
| `Authorization` ヘッダーがない | `401` | `missing_token` |
| 期限切れのトークン、別 API 向けのトークン、不正な署名、取り消されたトークン | `401` | `invalid_token` |
| 必要なスコープがないトークン | `403` | `insufficient_scope` |
| URL クエリ内のトークン（`?access_token=`） | `400` | `invalid_request` |

本文は `application/problem+json` で、`WWW-Authenticate` ヘッダーには標準のチャレンジが入ります。Django は複数のチャレンジを 1 つのヘッダーにまとめます。完全な表は [ミドルウェアで API を保護する](/docs/sdk/protect-api#応答)にあります。

## プロキシ経由での利用

クライアントが DPoP を使用する場合、ミドルウェアは各証明がクライアントの呼び出した URL 向けに作成されたことを確認します。`PUBLIC_BASE_URL` を、クライアントが呼び出す外部 URL（例: `https://api.example.com`）に設定してください。プロキシやロードバランサーの背後でも同様です。これは必須で、`https` を使用する必要があります（`http` は `localhost`、`127.0.0.1`、`[::1]` の場合のみ）。設定がないとアプリは起動を拒否します。ミドルウェア自身は `X-Forwarded-*` ヘッダーを読み取りません。Django は `SECURE_PROXY_SSL_HEADER` または `USE_X_FORWARDED_HOST` を設定した場合にのみそれらを適用し、`PUBLIC_BASE_URL` を設定すると、確認される URL はそれらに依存しません。

## 取り消されたトークン

アクセストークンは期限切れまで有効なままなので、KeenAuth はトークンが取り消された際に API に通知します。例では、インメモリの取り消しキャッシュをプロテクターに渡し、それを更新する `token.revoked` Webhook を `POST /webhooks/keenauth`（`api/webhook.py`）で提供します。

1. ダッシュボードで、URL `https://<your host>/webhooks/keenauth` を持ち、`token.revoked` を購読する Webhook エンドポイントを作成し、その署名シークレットを `KEENAUTH_WEBHOOK_SECRET` としてエクスポートします。
2. ハンドラーは生のボディに対して SDK で署名を検証し、署名が不正な場合、またはタイムスタンプが 5 分を超えてずれている場合は `400` を返します。イベントのセレクター（`access_token`、`session`、`user`、`client`、`org`、`org_user`、`client_user`）を取り消しマーカーに対応づけ、`204` を返します。その後ミドルウェアは、`not_before` より前に発行された一致するすべてのトークンを拒否します。
3. KeenAuth は少なくとも 1 回配信するため、ハンドラーは各 `webhook-id` を 1 回だけ適用します。マーカーを保存できない場合は `503` を返し、KeenAuth はイベントを再配信します。他のイベント種別や、どのアクセストークンにも一致しないセレクター（`refresh_family`、`api`）は無視します。

ハンドラーはイベント ID、セレクターの種別、理由をログに記録しますが、ボディ、署名、シークレットは決して記録しません。各ワーカープロセスは独自のキャッシュを持ちます。ワーカーやインスタンスが複数ある場合は、代わりに共有ストアを使用してください。[取り消しキャッシュ](/docs/sdk/protect-api#取り消しキャッシュtokenrevoked)を参照してください。

## 次のステップ

- [ミドルウェアで API を保護する](/docs/sdk/protect-api): Flask、ステップアップ要件、DPoP、すべてのオプション。
- [トークンを検証する](/docs/sdk/token-verification): ミドルウェアの下で動作する検証器。
- [オフラインテストキット](/docs/sdk/testing)。
