
# FastAPI のセッション

`keenauth.bff.fastapi` は FastAPI アプリケーションにサインインを追加します。サーバーが OAuth クライアントとなり、すべてのトークンをサーバー上で保持します。ブラウザが受け取るのは暗号化された HttpOnly のセッション Cookie だけで、アクセストークン、リフレッシュトークン、ID トークンは一切見えません。これは backend-for-frontend (BFF) パターンです。`keenauth-python` の一部ですが、まだ PyPI には公開されていません。

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

## インストール

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

## 開始する前に

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 です。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
from fastapi import Depends, FastAPI
from keenauth.bff import SessionContext
from keenauth.bff.fastapi import RequireSession, install

app = FastAPI()
install(app, bff)

@app.get("/orders")
def orders(ctx: SessionContext = Depends(RequireSession(bff))):
    return {"email": ctx.session.user.email}
```

`install` は以下のルートを提供し、それ以外のリクエストごとにセッションを読み取って `request.state.keenauth` に格納します。SDK のネットワーク呼び出しは同期的です。`install` はイベントループをブロックしないように、それらを Starlette のスレッドプールで実行します。

## ルート

| ルート | 動作 |
|---|---|
| `GET /auth/login` | サインインを開始します。任意のクエリパラメータとして、`returnTo`（ユーザーの遷移先）と `organization`（組織にサインイン）を指定できます。 |
| `GET /auth/callback` | サインインを完了させ、セッション Cookie を設定します。 |
| `GET` または `POST /auth/logout` | KeenAuth でサインアウトし、アプリに戻ります。 |

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

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

## 保護されたビュー

パスオペレーションに `Depends(RequireSession(bff))` を追加します。これはセッションコンテキストを返します。


セッションがない場合:

- ページのリクエスト（`GET` または `HEAD`）は、`returnTo` を付けて `/auth/login` にリダイレクトされます。
- `/api/…` へのリクエスト、または他のメソッドのリクエストには、問題ドキュメント付きの `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 に格納されます。`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 回目のリフレッシュでセッションが終了します。
