
# Django セッション

`keenauth.bff.django` は、Django 4.2 以降のアプリケーションにサインイン機能を追加します。サーバーが OAuth クライアントとなり、すべてのトークンをサーバー側で保持します。ブラウザが受け取るのは暗号化された HttpOnly のセッション Cookie のみで、アクセストークン、リフレッシュトークン、ID トークンは一切渡されません。これはバックエンド・フォー・フロントエンド（BFF）パターンです。`keenauth-python` の一部ですが、まだ PyPI には公開されていません。

ベアラートークンを受け取る API を保護する場合は、代わりに [API の保護](/docs/sdk/protect-api) を参照してください。

## インストール

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

## 開始する前に

1. ダッシュボードで、環境内に Web アプリケーションを作成します。
2. クライアント認証方式を選択します: `private_key_jwt`（推奨、`PrivateKeyJwt`）またはクライアントシークレット（`ClientSecretBasic`）。
3. リダイレクト URI `https://app.acme.example/auth/callback` と、ログアウト後のリダイレクト URI `https://app.acme.example/` を追加します。
4. 32 文字以上の Cookie シークレットを生成します。例: `openssl rand -base64 32`。リポジトリではなく、シークレットストアに保管してください。

## インスタンスを作成する

```python
import os
from keenauth.bff import BffOptions, ClientSecretBasic, KeenAuthBff

bff = KeenAuthBff(BffOptions(
    issuer="https://acme.eu.keenauth.app",  # your environment's issuer, exactly
    client_id="app_01j9zzzzzzzzzzzzzzzzzzzz",
    client_auth=ClientSecretBasic(os.environ["KEENAUTH_CLIENT_SECRET"]),
    app_base_url="https://app.acme.example",
    secret=os.environ["KEENAUTH_COOKIE_SECRET"],
))
```

`app_base_url` はアプリの公開アドレスです。SDK はリダイレクト URI とすべてのリダイレクト先をここから組み立てるため、リクエストの `Host` や `X-Forwarded-*` ヘッダーからは組み立てません。平文の `http` は `localhost` の場合にのみ許可されます。オプションは `KeenAuthBff` の作成時に検証されるため、設定エラーがあれば起動時にアプリが停止します。

## private_key_jwt を使用する

`private_key_jwt` を使用する場合、アプリケーションに共有シークレットはありません。秘密鍵を `PrivateKeyJwt` に渡すと、SDK はトークンエンドポイントへのリクエストごとに新しいクライアントアサーション（RFC 7523）に署名します。

```python
from keenauth.bff import PrivateKeyJwt

client_auth = PrivateKeyJwt(key=os.environ["KEENAUTH_PRIVATE_KEY"])  # unencrypted PEM
```

- 対応する鍵: EC P-256（`ES256`）、EC P-384（`ES384`）、Ed25519（`EdDSA`）、および 2048 ビット以上の RSA（既定は `PS256`。`RS256` を使用する場合は `algorithm="RS256"` を指定します）。
- 各アサーションでは、`iss` と `sub` にクライアント ID、`aud` に発行者を設定します。`jti` はランダムな値で、有効期間は 60 秒です。`kid` は公開鍵の RFC 7638 サムプリントです。
- 公開鍵をアプリケーションに登録してください。`keenauth.m2m.load_private_key(pem).public_jwk()` を使うと、JWK として取得できます。

独自の鍵管理サービス（KMS や HSM）で署名する場合は、代わりに関数を渡します: `PrivateKeyJwt(assertion=sign)`。`sign()` は呼び出されるたびに、新しく署名されたアサーションを返す必要があります。

## ミドルウェアを追加する

```python
# settings.py
KEENAUTH_BFF = "myproject.auth.bff"  # dotted path to the KeenAuthBff instance
MIDDLEWARE = [
    # ...
    "keenauth.bff.django.KeenAuthMiddleware",
]
```

ミドルウェアは以下のルートを処理し、それ以外のすべてのリクエストについてセッションを読み取り、`request.keenauth` に格納します。

```python
# views.py
from django.http import HttpResponse
from keenauth.bff.django import require_auth

@require_auth()
def orders(request):
    return HttpResponse(f"Signed in as {request.keenauth.session.user.email}")
```

## ルート

| ルート | 処理内容 |
|---|---|
| `GET /auth/login` | サインインを開始します。省略可能なクエリパラメーター: `returnTo`（ユーザーの遷移先）と `organization`（組織にサインインします）。 |
| `GET` または `POST /auth/logout` | KeenAuth でサインアウトし、アプリに戻ります。 |
| `GET /auth/callback` | サインインを完了し、セッション Cookie を設定します。 |

サインインには PKCE（S256）を用いた認可コードフローを使用します。環境がサポートしている場合は、リクエストをサーバーに事前送信（PAR）します。コールバックでは、SDK が `state`、`iss`、および ID トークン（`nonce` を含む）を検証します。サインインに成功するたびに新しいセッションが作成され、古いセッションは破棄されます。

`returnTo` にはアプリ自身のパス（例: `/orders?tab=open`）を指定する必要があります。`//evil.example` や完全な URL などそれ以外の値は無視され、ユーザーは `/` に遷移します。

## 保護されたビュー

ビューには `@require_auth()` を付けて装飾します。


セッションがない場合:

- ページへのリクエスト（`GET` または `HEAD`）は、`returnTo` を付けて `/auth/login` にリダイレクトされます。
- `/api/…` へのリクエスト、またはそれ以外のメソッドのリクエストには、problem ドキュメントを含む `401` が返されます。どのパスでも `401` で応答させたい場合は `api=True` を指定します。

`POST`、`PUT`、`PATCH`、`DELETE` など安全でないメソッドの場合、ブラウザはそのリクエストがアプリからのものであることを示す必要があります。具体的には、`Sec-Fetch-Site: same-origin` または、アプリのオリジンと一致する `Origin` ヘッダーが必要です。それ以外のリクエストには `403` が返されます。これは `SameSite=Lax` に加えて、クロスサイトリクエストフォージェリを防止するための仕組みです。

## セッションを使用して API を呼び出す

セッションコンテキストには以下が含まれます。

- `session`: `None`、またはサインイン済みのユーザーです。`session.user` には `subject`、`email`、`name`、`org_id`、`org_roles` などの型付き ID トークンクレームがあります。すべてのクレームは `session.user.claims` にあります。セッションオブジェクトにトークンが含まれることはありません。
- `access_token`: サーバー側の呼び出しに使用できる有効なアクセストークン、または `None`。ブラウザには送信しないでください。

アクセストークンの有効期限が 60 秒以内に迫ると、ビューが実行される前に SDK がトークンを更新します。更新のたびにリフレッシュトークンもローテーションされ、新しいセッション Cookie がレスポンスに設定されます。サーバーがリフレッシュトークンを拒否した場合、セッションは終了します。一時的なサーバーエラーの場合は、セッションは変更されません。1 つのプロセス内で同時に発生したリクエストは、1 回のリフレッシュを共有します。

## 組織を切り替える

`bff.switch_organization_path("org_01j9…", "/dashboard")` は、`/auth/login` へのリンクを返します。このリンクを使うと、ユーザーは指定した組織で再度サインインします。KeenAuth はメンバーシップを確認します。サインイン後は、`session.user.org_id` に組織が格納されます。

## サインアウトする

フォームから `/auth/logout` に `POST` を送信します。SDK はセッション Cookie（ストアを使用している場合は保存済みのセッションも）を削除し、リフレッシュトークンを取り消し、ブラウザを環境のログアウトエンドポイントへ送ります。そこからアプリに戻ります。ブラウザが別のサイトからのリクエストとしてマークしたリクエストは拒否されます。`Sec-Fetch-Site` を送信しないブラウザでは、自社のページからの `POST` によってのみサインアウトできます。

## セッションストレージ

既定では、セッション全体が Cookie に格納されます。Cookie は `secret` から導出した鍵を用いて AES-256-GCM で暗号化・認証されます。変更またはコピーされた Cookie は拒否されます。Cookie の名前は `__Host-keenauth_session.0` で、`Secure`、`HttpOnly`、`SameSite=Lax`、`Path=/` を指定して設定されます。`http://localhost` では `__Host-` 接頭辞と `Secure` は使用されません。大きなセッションは最大 3 つの Cookie に分割されます。

トークンをサーバー側で保持するには、`get(id)`、`set(id, session, ttl_seconds)`、`delete(id)` を持つオブジェクトを `store=` に渡します（例: Redis をバックエンドにしたもの）。この場合、Cookie には暗号化されたランダムなセッション ID のみが格納されます。サインインのたびに新しい ID が作成され、以前のレコードは削除されます。ストアにはトークンが含まれるため、アクセスを制限してください。`MemorySessionStore` は開発とテストのみを目的としています。

セッションの有効期間は最大 7 日（`session_max_age`）です。リフレッシュしても有効期間は延長されません。シークレットを変更するには、リストを渡します: `secret=[new_secret, old_secret]`。古いシークレットは `session_max_age` の経過後に削除してください。

## 現在の制限事項

- DPoP にバインドされたトークンとバックチャネルログアウトは、まだ処理されません。
- リフレッシュの共有は 1 つのサーバープロセス内に限られます。複数のプロセスを使用する場合、アプリケーション設定 `refresh_reuse_interval_s`（既定は 5 秒）により、並行するリフレッシュを成功させることができます。`0` に設定すると、2 回目のリフレッシュでセッションが終了します。
