トークンの失効と再利用対策
単体の失効、更新系列の失効、期限切れがそれぞれどのトークンに及ぶか確認します。
説明
署名付きアクセストークンは、署名と期限だけを調べる方式では発行後のログアウトを知りません。早期に止めるには失効情報の照合など、現在状態を確認する仕組みが必要です。
一つのアクセストークンを失効させ、状態照会の有無による違いを比較します。その後、更新トークンを一回で交換し、使用済みの値が再び提示されたときに同じ系列を止める処理を追います。
単体の失効、更新系列の失効、期限切れがそれぞれどのトークンに及ぶか確認します。
1二つの系列を用意する
系列1はalice、系列2は別端末bobの試料として扱います。
from sd07 import *
db = database()
a,b = token(),token(sub="bob"別系列のトークンを用意する,jti="b1",family=2)
print(accepted(db署名・期限・失効を検査する,a,150),accepted(db,b,150))どちらも有効です。失効情報の照会は署名と時刻を検証した後に行います。
2一つのトークンIDを失効させる
jtiはトークンを識別する値です。a1を期限200まで失効表へ入れます。
db.execute("INSERT INTO denya1を失効させる VALUES('a1',200)")
db.commit()
print(accepted(db,a,150),accepted(db,a,150,online=False失効照会を省いた結果と比較する))
print(accepted(db,b,150))状態照会ありはrevoked_jti、署名と期限だけならaliceです。bobは継続できます。失効表の照会を省くAPIには、この失効は届きません。
3更新トークンを一度だけ交換する
更新トークンはハッシュで保存します。交換処理はBEGIN IMMEDIATEから同じトランザクションで使用済み記録と新規発行を確定します。
db.execute("INSERT INTO refresh VALUES(?,1,0)",(digest("r1-public"),))
db.commit()
print(refresh(db旧値の消費と新値の発行を確定する,"r1-public","r2-public"))
print(db.execute("SELECT used,COUNT(*) FROM refresh GROUP BY used新旧の状態を集計する ORDER BY used").fetchall())使用済み1件と未使用1件になりました。交換前に使用済み更新を確定する途中状態を外へ出さないようにします。
4古い更新トークンの再利用を検知する
交換済みのr1をもう一度送ります。
print(refresh(db,"r1-public","r3-public"消費済みの値を再び交換する))
print(db.execute("SELECT * FROM family ORDER BY id").fetchall())reuse_detectedとなり系列1が失効します。系列2は0のままです。再送と盗用をこの情報だけで見分けることは難しく、正規クライアントの同時更新も設計対象になります。
5最新の更新値も同じ系列なら止める
r2は未使用ですが系列1に属します。別jtiのアクセストークンも同じ系列として試します。
print(refresh(db,"r2-public","r4-public"最新の更新値を試す))
print(accepted(db,token(jti="a2")別IDの同系列アクセストークンを試す,150))
print(accepted(db,b,150))更新と系列1のアクセスはrevoked_family、bobは継続します。a1だけの失効と異なり、同じ系列全体を対象にしています。
6状態照会を省く側では期限まで残る
同じa2を署名と期限だけで調べます。
print(accepted(db,token(jti="a2"),199,online=False期限直前の残存を確認する))
print(accepted(db,token(jti="a2"),200,online=False署名付きの終了時刻を確認する))199はalice、200はexpiredです。短い有効期限は残存時間を減らしますが、オンライン失効と同じ即時性を持つわけではありません。
7拒否後に新規発行が増えていないことを確認する
発行された更新値はr1とr2だけのはずです。
print(db.execute("SELECT COUNT(*),SUM(used)消費済みの件数を確認する FROM refresh").fetchone())
print(db.execute("SELECT COUNT(*) FROM refresh WHERE hash IN (?,?)拒否した発行先の不存在を検査する",(digest("r3-public"),digest("r4-public"))).fetchone())全2件、使用済み1件です。拒否した交換でr3・r4は保存されていません。
まとめ
署名と期限だけの検証へ失効情報は自動では届きません。
一度使った更新値の再利用を検知し、系列の扱いを決めます。
交換を拒否した後の保存状態も確認します。
操作履歴と解説を振り返れます。
Python 3.15 / 固定の公開試料
from sd07 import *
db = database()
a,b = token(),token(sub="bob",jti="b1",family=2)
print(accepted(db,a,150),accepted(db,b,150))alice bob
db.execute("INSERT INTO deny VALUES('a1',200)")
db.commit()
print(accepted(db,a,150),accepted(db,a,150,online=False))
print(accepted(db,b,150))revoked_jti alice bob
db.execute("INSERT INTO refresh VALUES(?,1,0)",(digest("r1-public"),))
db.commit()
print(refresh(db,"r1-public","r2-public"))
print(db.execute("SELECT used,COUNT(*) FROM refresh GROUP BY used ORDER BY used").fetchall())r2-public [(0, 1), (1, 1)]
print(refresh(db,"r1-public","r3-public"))
print(db.execute("SELECT * FROM family ORDER BY id").fetchall())reuse_detected [(1, 1), (2, 0)]
print(refresh(db,"r2-public","r4-public"))
print(accepted(db,token(jti="a2"),150))
print(accepted(db,b,150))revoked_family revoked_family bob
print(accepted(db,token(jti="a2"),199,online=False))
print(accepted(db,token(jti="a2"),200,online=False))alice expired
print(db.execute("SELECT COUNT(*),SUM(used) FROM refresh").fetchone())
print(db.execute("SELECT COUNT(*) FROM refresh WHERE hash IN (?,?)",(digest("r3-public"),digest("r4-public"))).fetchone())(2, 1) (0,)
演習完了