
# Flask セッション

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

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

## インストール

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

## 始める前に

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` にはissuer を設定します。さらにランダムな `jti` を付与し、有効期間は 60 秒です。`kid` は公開鍵の RFC 7638 サムプリントです。
- 公開鍵はアプリケーションに登録してください。`keenauth.m2m.load_private_key(pem).public_jwk()` は公開鍵を JWK として返します。

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

## フックの登録

```python
from flask import Flask, g
from keenauth.bff.flask import init_app, require_auth

app = Flask(__name__)
init_app(app, bff)

@app.get("/orders")
@require_auth(bff)
def orders():
    return f"Signed in as {g.keenauth.session.user.email}"
```

`init_app` は以下のルートを提供し、それ以外のリクエストごとにセッションを読み込んで `g.keenauth` に格納します。この SDK は Flask 独自の `session` Cookie を使用しません。

## ルート

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

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

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

## 保護されたビュー

ルートのデコレーターの下に `@require_auth(bff)` を付けてビューを装飾します。

セッションがない場合は次のとおりです。

- ページのリクエスト (`GET` または `HEAD`) は、`returnTo` を付けて `/auth/login` にリダイレクトされます。
- `/api/…` へのリクエスト、またはそれ以外のメソッドのリクエストには、問題ドキュメントを含む `401` が返されます。`api=True` を指定すると、どのパスでも `401` を返すようになります。

`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 は拒否されます。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 回目のリフレッシュでセッションが終了します。
