Flask セッション
keenauth.bff.flask は、Flask 2.3 以降のアプリケーションにサインイン機能を追加します。お使いのサーバーが OAuth クライアントとなり、すべてのトークンをサーバー側で保持します。ブラウザが受け取るのは暗号化された HttpOnly のセッション Cookie だけで、アクセストークン、リフレッシュトークン、ID トークンがブラウザに渡ることはありません。これは backend-for-frontend (BFF) パターンです。このパッケージは keenauth-python の一部であり、まだ PyPI には公開されていません。
その代わりに、ベアラートークンを受け取る API を保護する場合は、API の保護をご覧ください。
インストール
pip install "keenauth-python[flask]"
始める前に
- ダッシュボードで、環境内に 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 (既定は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() は呼び出しごとに、新しく署名されたアサーションを返す必要があります。
フックの登録
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 回目のリフレッシュでセッションが終了します。