一覧SD-07-1 · セッションとトークンの安全性

セッション管理とログアウト

IDを消す場所、期限を判定する場所、サーバーに残る有効状態の関係を説明します。

説明

セッションIDは、ログイン済みの利用者と後続の要求を結び付ける値です。サーバーはIDに対応する利用者と期限を保存し、要求のたびに有効性を確認します。

匿名状態のIDをログイン時に更新し、古いIDでのアクセスを試します。その後、無操作の期限と発行からの期限を別々に確認し、ログアウトによる失効まで追います。固定のIDは比較用の試料です。運用時は予測困難な乱数で発行します。

IDを消す場所、期限を判定する場所、サーバーに残る有効状態の関係を説明します。

1匿名IDの保存先を調べる

DBには生のIDではなくSHA-256値と状態を保存します。

from sd07 import * db = database() issue(db匿名セッションを作る,"anon-public","anonymous",0) print(db.execute("SELECT length(hash)保存項目を調べる,subject,created,last,active FROM sessions").fetchall())

64桁のハッシュ値、匿名の主体、発行・最終利用時刻0が保存されています。クライアントは生IDを持ち、サーバーはハッシュで行を検索します。

2ログイン時にIDを更新する

認証成功後の処理としてIDを更新します。

rotate_session旧IDを失効させ新IDを発行する(db,"anon-public","login-public","alice",10) print(access(db,"anon-public"旧IDを再利用する,11)) print(access(db,"login-public",11))

旧IDはNone、新IDはaliceです。匿名時に第三者が知ったIDを認証後も使い続けないための更新です。

3Cookieの送信・参照条件を確認する

Cookie属性とDBの期限は別の役割です。次はHTTPSで返す設定例を標準パーサーで読みます。

cookie = SimpleCookie()Set-Cookieの属性を解析する cookie.load公開試料の設定を読む("__Host-sid=login-public; Secure; HttpOnly; Path=/; SameSite=Lax") print({k:cookie["__Host-sid"][k] for k in ("secure","httponly","path","samesite","domain")})

SecureはHTTPS送信、HttpOnlyはスクリプトからの参照制限、Path=/は全パス、Domain省略は発行ホスト限定です。これらの属性だけではDBの有効状態は変わりません。

4無操作期限の直前と境界を試す

最後の利用は11。60秒の無操作期限を設定しています。期限直前の利用は時刻を更新します。

print(access(db,"login-public",70期限前に利用する)) print(db.execute("SELECT last FROM sessions WHERE subject='alice'").fetchone()) print(access(db,"login-public",130更新後の期限ちょうどで試す))

70で成功してlastが70へ変わります。130では70+60に達し、Noneです。期限判定はサーバーで行っています。

5利用を続けても発行からの期限は延長しない

別IDを時刻0で発行し、50秒ごとに利用します。

issue(db,"active-public","alice",0) print([access(db,"active-public",t) for t in (50,100,150,200,250,299,300)無操作期限内で利用を繰り返す])

299まではalice、300はNoneです。lastは更新されてもcreated+300の上限は固定され、長時間の継続利用を制限します。

6ログアウトでサーバー側の状態を失効する

新しいIDを発行し、利用後にログアウトします。

issue(db,"logout-public","alice",400) print(access(db,"logout-public",401)) print(logout(db保存されたセッションを失効させる,"logout-public")) print(access(db,"logout-public",402残存コピーの再利用を試す))

更新行数1でactiveが0となり、同じIDを送ってもNoneです。Cookieの削除だけでは残存コピーの再利用を止められないため、サーバー側も更新します。

7認証済み行の最終状態を確認する

期限切れと明示的な失効は異なる条件として残っています。

print(db.execute("SELECT created,last,active発行・利用・失効の条件を照合する FROM sessions WHERE subject='alice' ORDER BY created").fetchall())

期限切れの行はactiveが1でも期限条件で拒否されます。ログアウトした行はactiveが0です。運用では不要な期限切れ行の削除も行います。

まとめ

ログイン時にIDを更新し、旧IDを失効させます。

無操作期限と絶対期限をサーバーで評価します。

ログアウトではCookieだけでなく保存状態を失効させます。

操作履歴と解説を振り返れます。

Python 3.15 / 固定の公開試料

1
from sd07 import * db = database() issue(db,"anon-public","anonymous",0) print(db.execute("SELECT length(hash),subject,created,last,active FROM sessions").fetchall())
[(64, 'anonymous', 0, 0, 1)]
2
rotate_session(db,"anon-public","login-public","alice",10) print(access(db,"anon-public",11)) print(access(db,"login-public",11))
None
alice
3
cookie = SimpleCookie() cookie.load("__Host-sid=login-public; Secure; HttpOnly; Path=/; SameSite=Lax") print({k:cookie["__Host-sid"][k] for k in ("secure","httponly","path","samesite","domain")})
{'secure': True, 'httponly': True, 'path': '/', 'samesite': 'Lax', 'domain': ''}
4
print(access(db,"login-public",70)) print(db.execute("SELECT last FROM sessions WHERE subject='alice'").fetchone()) print(access(db,"login-public",130))
alice
(70,)
None
5
issue(db,"active-public","alice",0) print([access(db,"active-public",t) for t in (50,100,150,200,250,299,300)])
['alice', 'alice', 'alice', 'alice', 'alice', 'alice', None]
6
issue(db,"logout-public","alice",400) print(access(db,"logout-public",401)) print(logout(db,"logout-public")) print(access(db,"logout-public",402))
alice
1
None
7
print(db.execute("SELECT created,last,active FROM sessions WHERE subject='alice' ORDER BY created").fetchall())
[(0, 299, 1), (10, 70, 1), (400, 401, 0)]

演習完了