FastAPI のセッション
keenauth.bff.fastapi は FastAPI アプリケーションにサインインを追加します。サーバーが OAuth クライアントとなり、すべてのトークンをサーバー上で保持します。ブラウザが受け取るのは暗号化された HttpOnly のセッション Cookie だけで、アクセストークン、リフレッシュトークン、ID トークンは一切見えません。これは backend-for-frontend (BFF) パターンです。keenauth-python の一部ですが、まだ PyPI には公開されていません。
ベアラートークンを受け取る API を保護する場合は、代わりに API の保護 を参照してください。
インストール
pip install "keenauth-python[fastapi]"
開始する前に
- ダッシュボードで、環境内に Web アプリケーションを作成してください。
- クライアント認証方式を選択してください。推奨は
private_key_jwt(PrivateKeyJwt)で、クライアントシークレット(ClientSecretBasic)も利用できます。 - リダイレクト URI
https://app.acme.example/auth/callbackと、ログアウト後のリダイレクト URIhttps://app.acme.example/を追加してください。 - 32 文字以上の Cookie 用シークレットを生成してください。例えば
openssl rand -base64 32を使用できます。リポジトリには含めず、シークレットストアで管理してください。
インスタンスの作成
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)を署名して生成します。
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() は呼び出しごとに新しい署名済みアサーションを返す必要があります。
ミドルウェアのインストール
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 回目のリフレッシュでセッションが終了します。