認証とセキュリティ¶
Secret key¶
session、signed token、OAuth stateに使うsecretはUTF-8で32バイト以上必須です。 repository、image、log、exception responseへ含めず、productionではsecret managerから 読み込みます。
Signed cookie session¶
SessionMiddlewareを通るレスポンスには Vary: Cookie を付加し、Cookieごとに
キャッシュを分離します。既存のVary指定は保持します。機密情報を保存させたくない
レスポンスには、アプリケーション側で Cache-Control: no-store も指定してください。
JSONリクエストの入れ子は128階層までです。上限超過やパーサーの数値制限に
違反する入力は400になります。get_json(silent=True) はこれらの場合も None を返します。
from higuma import SessionMiddleware
app.add_middleware(
SessionMiddleware,
os.environ["HIGUMA_SECRET_KEY"],
cookie_name="higuma_session",
max_age=14 * 24 * 60 * 60,
secure=True,
httponly=True,
samesite="Lax",
)
cookieは署名されますが暗号化されません。password、token、個人情報をsessionへ保存しないで ください。改変、不正形式、期限切れcookieは空sessionとして扱われます。serialized cookieは 4093 bytesまでです。
session.permanent = Falseのbrowser sessionはMax-Ageを付けず、Trueのsessionだけ
persistent cookieになります。login/logout時はsessionをclearして固定化攻撃を防ぎます。
Session login¶
auth = AuthManager(
app,
secret_key=os.environ["HIGUMA_SECRET_KEY"],
session_options={"secure": True, "samesite": "Lax"},
)
@auth.load_user
def load_user(user_id):
return find_user(user_id)
@app.get("/me")
@auth.login_required
def me():
return {"id": current_user.id}
AuthManagerはSessionMiddlewareも登録します。同じappへ別のSessionMiddlewareを
重ねないでください。user loaderはrequestごとに文字列IDを受け、userまたはNoneを返します。
user objectはidとis_authenticatedを持たせます。
login_user(user, remember=False): sessionをrotateしてfresh loginにするconfirm_login(): 既存loginをfreshに戻す。未loginならUnauthorizedlogout_user(): sessionをclearするlogin_required: 未loginを401にするfresh_login_required: non-fresh loginを401にするroles_required/permissions_required: claim不足を403にする
role / permission decoratorはuser objectのroles / permissions iterableを読みます。
match_all=Falseならいずれか一つで許可します。空のrequired claimはValueErrorです。
Password¶
hasher = PasswordHasher()
stored = hasher.hash(password)
if hasher.verify(password, stored):
if hasher.needs_rehash(stored):
stored = hasher.hash(password)
標準形式はrandom salt付きscryptです。verify()は不正formatやresource上限外の値に対して
Falseを返します。password hashとparameterはdatabaseへ保存し、plain passwordは保存・log
しないでください。
Signed token¶
signer = TokenSigner(os.environ["HIGUMA_SECRET_KEY"], salt="email-verify")
token = signer.dumps({"user_id": "usr_123"})
payload = signer.loads(token, max_age=900)
用途ごとに異なるsaltを使います。tokenは署名されますが暗号化されません。期限切れ、改変、
不正formatはValueErrorです。
CSRF¶
cookie sessionを使うbrowser applicationでは、session middlewareの内側に
CSRFProtectionを追加します。
app.add_middleware(SessionMiddleware, os.environ["HIGUMA_SECRET_KEY"], secure=True)
app.add_middleware(CSRFProtection)
@app.get("/form")
def form():
return {"csrf_token": csrf_token()}
POST / PUT / PATCH / DELETEではform field _csrf_token、またはheader
X-CSRF-Tokenを送信します。token不一致は403です。custom field_nameを設定した場合、
csrf_token()はactive protectionのfield設定へ追従します。
Rate limit¶
default keyはrequest.remote_addrです。状態はprocess-local memoryにあり、複数process間で
共有されません。厳密なglobal quotaにはreverse proxyやshared storeを使います。
max_keys到達後の未知keyは共通bucketへ集約され、無制限にmemoryを増やしません。
OAuth 2.0 + PKCE¶
OAuth flowにはSessionMiddlewareが必須です。stateとPKCE verifierをbrowser sessionへ
束縛し、複数のpending loginを保持しながら各stateを一度だけ消費します。
app.add_middleware(
SessionMiddleware,
os.environ["HIGUMA_SECRET_KEY"],
secure=True,
)
google = OAuth2Client.google(
client_id=os.environ["GOOGLE_CLIENT_ID"],
client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
redirect_uri="https://example.com/auth/google/callback",
secret_key=os.environ["HIGUMA_SECRET_KEY"],
)
@app.get("/login/google")
def login_google():
return redirect(google.authorization_url())
@app.get("/auth/google/callback")
def google_callback():
google.validate_state(request.args["state"])
token = google.fetch_token(request.args["code"])
return google.userinfo(token["access_token"])
callbackではtoken交換前にvalidate_state()を呼びます。google、line、discord factoryと
custom provider constructorが同じflowを使います。provider responseは1 MiBまでです。
OIDC nonceは送信しますが、higumaはID token signature / claim validatorではありません。
CORS、Host、security header¶
credential付きCORSはexact originを列挙し、*を使いません。
app.add_middleware(
CORSMiddleware,
allow_origins=("https://app.example.com",),
allow_credentials=True,
)
app.add_middleware(TrustedHostMiddleware, ("example.com", "*.example.com"))
app.add_middleware(
SecurityHeadersMiddleware,
strict_transport_security="max-age=31536000; includeSubDomains",
)
CSPはapplicationが実際に読み込むscript、style、image、API originへ合わせて設計してください。 HSTSはHTTPSが全subdomainで継続利用できる場合だけ有効にします。
Reverse proxy¶
X-Forwarded-ForとX-Forwarded-Protoは標準では無視されます。直接接続元proxyだけを
CIDRまたはIPで信頼します。proxyは外部から届いたforwarded headerを必ず上書きします。
ProxyHeadersMiddlewareを設定せずX-Forwarded-Forを直接読むことは禁止です。
multi-process supervisorのclient IP制約はデプロイを参照してください。
Checklist¶
秘密鍵は32バイト以上の str または bytes を指定します。整数や配列からの暗黙変換は
拒否します。CSRFトークンの不正な非ASCII入力は403になります。
TrustedHostMiddleware は空のHost、不正なポートやIPv6構文も拒否します。
OAuthのtoken・userinfo HTTPリクエストはリダイレクトを追跡しません。
認証情報の別ホストやHTTPへの転送を防ぐため、プロバイダーの最終HTTPS URLを設定してください。
- password、session、OAuth token、secretをlogしない
- HTTPS、secure / httponly cookie、適切なSameSiteを使う
- credential付きCORSとWebSocket Originをexact matchにする
- uploadに一意なserver-side name、size/type/quota/scanを適用する
- raw SQLへuser inputを文字列連結しない
debug=False、custom error response、reverse proxy limitを本番で確認する