実行環境¶
Lambda上のアプリケーションは、pocket.toml(または pocket.runtime.toml)とAWSリソースから設定情報を取得します。
pocket.runtime.toml が存在する場合はそちらが優先されます。pocket runtime-config コマンドでビルド専用設定を除外したランタイム用 TOML を生成できます(設定ファイル - pocket runtime-config を参照)。
POCKET_STAGE¶
POCKET_STAGE は Lambda ランタイムでの実行ステージ判定 に使われる環境変数です。
CloudFormation が Lambda 関数の環境変数として自動設定するため、通常はユーザーが手動で設定する必要はありません。
各フレームワークのユーティリティ関数(Django の set_envs() や Rust の set_envs())は、この変数の有無でローカル環境と Lambda 環境を判別します。POCKET_STAGE が未設定の場合、AWS リソースへのアクセスはスキップされます。
デプロイ対象指定とは別物です
pocket コマンドの --stage オプションのデフォルト値は POCKET_DEPLOY_STAGE 環境変数から読まれます(CLI を参照)。
POCKET_STAGE をローカルで設定すると、ローカル実行プロセスが「自分は Lambda 上のそのステージで動いている」と誤解する原因になります。デプロイ対象を環境変数で指定したい場合は POCKET_DEPLOY_STAGE を使ってください。
セットされる環境変数¶
set_envs() は、Django・Rust いずれの実装でも、以下の環境変数をセットします。
| 環境変数 | 説明 |
|---|---|
POCKET_PROJECT_NAME |
プロジェクト名 |
POCKET_REGION |
AWS リージョン |
POCKET_HOSTS |
API Gateway ホスト一覧(カンマ区切り。Django では ALLOWED_HOSTS に追加される) |
POCKET_{HANDLER}_HOST |
各ハンドラーのホスト |
POCKET_{HANDLER}_ENDPOINT |
各ハンドラーの URL |
POCKET_{HANDLER}_QUEUEURL |
SQS キュー URL |
POCKET_CLOUDFRONT_{NAME}_DOMAIN |
CloudFront ディストリビューションのドメイン名 |
POCKET_DSQL_ENDPOINT |
DSQL クラスターのエンドポイント |
POCKET_DSQL_REGION |
DSQL クラスターのリージョン |
POCKET_DSQL_TOKEN |
DSQL IAM 認証トークン(set_envs() 呼び出し時に生成) |
| シークレットキー | Secrets Manager / SSM のシークレット値 |
SPA トークン認証¶
CloudFront 配信の SPA にログイン必須機能を追加する場合、SPA トークン認証モジュールを使用します。 HMAC-SHA256 トークンを Cookie にセットし、CloudFront Function で検証します。
gate は default behavior の全リクエストに効く (asset 配信も含む)
CloudFront Function (viewer-request) は require_token = true のルートの
default behavior に attach されるため、そのルート配下の全リクエスト
(/_app/*.js 等の拡張子付き asset も含む) に対して token 検証が走ります。
asset 単位で gate を bypass する設定は入っていません。
一見「token expired のユーザーは asset の数だけ redirect される (thundering herd 的にループ寸前)」のように見えますが、実際にはなりません: ブラウザは index.html を先に取得 → パース → 必要な asset を要求 の順で 動くため、document 取得時点で middleware が token を refresh すれば、 後続の asset リクエストはすべて valid token で 200 を返します。1 ユーザー につき 1 回の token refresh で済む モデルです。
Python (Django)¶
from pocket.django.spa_auth import generate_token, verify_token, spa_login, spa_logout
# トークン生成(ログイン時)
token = generate_token("user123") # デフォルト有効期限: 7日
# トークン検証(任意のバックエンド処理で)
user_id = verify_token(token) # 有効なら user_id、無効なら None
ログイン・ログアウトでは、Django の View でレスポンスに Cookie をセットします。
from django.http import HttpResponseRedirect
from pocket.django.spa_auth import spa_login, spa_logout
def login_view(request):
# Django 認証でユーザーを検証後...
response = HttpResponseRedirect(request.GET.get("next", "/"))
spa_login(response, str(request.user.id))
return response
def logout_view(request):
response = HttpResponseRedirect("/")
spa_logout(response)
return response
SpaTokenCookieMiddleware (必須)¶
require_token = true のルートを公開するときは、SpaTokenCookieMiddleware
を MIDDLEWARE に追加してください。入れないと無限 redirect ループに
陥ります。
# settings.py
MIDDLEWARE = [
# ...
"django.contrib.auth.middleware.AuthenticationMiddleware",
"pocket.django.spa_auth.SpaTokenCookieMiddleware",
# ...
]
middleware の動作:
- 認証済み response:
pocket-spa-tokencookie が無い / 期限切れならspa_login()で発行 (= self-heal) - 未認証 response: 残存 cookie があれば
spa_logout()で削除 SPA_TOKEN_SECRET環境変数が未設定の環境 (gating 未 deploy のローカル等) は no-op
なぜ必要か (詳細)¶
Django session のデフォルト寿命 (SESSION_COOKIE_AGE = 14 日) が SPA token
の寿命 (DEFAULT_MAX_AGE = 7 日) より長いため、8 日目以降に「session 有り
+ token 無し」の状態が全ユーザーに発生します。この状態で require_token
ルートにアクセスすると:
- CloudFront Function が token 無を検出 →
login_path(/accounts/login/?next=/) に 302 - ログインページが「既にログイン済み」を検出し、ログイン処理を通さずに
next(/) へ 302 (allauth.RedirectAuthenticatedUserMixinや Django 標準LoginView.redirect_authenticated_user=Trueの挙動) - → 1 へ戻り 無限ループ
middleware が 2 の bounce response 経路で必ず token を補填するため、1 往復 余分に bounce するだけでループが断ち切られます (ユーザーには visible な 追加遷移には見えません)。
拡張 (subclass で sliding refresh や短命 token に対応)¶
SpaTokenCookieMiddleware は次の 2 つの拡張点を持ちます:
| メソッド | デフォルト | 用途 |
|---|---|---|
_should_issue(request) -> bool |
cookie 無 or verify_token で失効判定 |
残り寿命が短い時にも発行する sliding refresh 等 |
_max_age() -> int |
DEFAULT_MAX_AGE (7 日) |
短命 token を使う場合に settings 等から返す |
例: 残り寿命が半分を切ったら sliding refresh する subclass:
import time
from django.conf import settings
from pocket.django.spa_auth import (
COOKIE_NAME,
SpaTokenCookieMiddleware as BaseMiddleware,
)
class SpaTokenCookieMiddleware(BaseMiddleware):
def _should_issue(self, request):
if super()._should_issue(request):
return True
token = request.COOKIES[COOKIE_NAME]
remaining = int(token.split(":")[1]) - time.time()
return remaining < self._max_age() / 2
def _max_age(self):
return settings.SPA_TOKEN_MAX_AGE
MIDDLEWARE で指定するパスを subclass 側に差し替えれば、デフォルトの
挙動はそのまま保ちつつ拡張できます。
Rust (Loco)¶
Rust アプリケーションでは pocket-spa-auth crate を使用できます。
use pocket_spa_auth::{generate_token, verify_token, login_cookie_value, logout_cookie_value};
let secret = std::env::var("SPA_TOKEN_SECRET").unwrap();
// トークン生成
let token = generate_token("user123", &secret, 604800);
// トークン検証
if let Some(user_id) = verify_token(&token, &secret) {
println!("認証成功: {}", user_id);
}
// Cookie 値の生成
let set_cookie = login_cookie_value(&token, 604800);
let delete_cookie = logout_cookie_value();
API リファレンス¶
Python
| 関数 / クラス | 引数 | 戻り値 | 説明 |
|---|---|---|---|
generate_token(user_id) |
user_id: str, secret: str\|None, max_age: int |
str |
HMAC-SHA256 トークンを生成 |
verify_token(token) |
token: str, secret: str\|None |
str\|None |
トークンを検証し、有効なら user_id を返す |
spa_login(response, user_id) |
response, user_id: str, secret: str\|None, max_age: int |
— | レスポンスにトークン Cookie をセット |
spa_logout(response) |
response |
— | レスポンスからトークン Cookie を削除 |
SpaTokenCookieMiddleware |
Django middleware | — | 認証済み response に token 自動補填、未認証 response から残存 cookie 削除 (詳細は上記) |
secretを省略するとos.environ["SPA_TOKEN_SECRET"]を使用しますmax_ageのデフォルトは604800(7日間)です- Cookie 名は
pocket-spa-tokenで、HttpOnly,Secure,SameSite=Laxが設定されます