リフレッシュトークンの更新と再利用対策
個別のトークンの期限と、同じ認証から発行した系列の失効を分けて管理します。
説明
access tokenは有効期間が短くても、refresh tokenで新しいものを取得して利用を続けられます。refresh tokenを回転させる方式では、使用済みの値が再び来たことを検知して、その系列を失効させる設計ができます。
固定のトークン台帳をSQLで確認し、r1からr2へ更新します。その後で古いr1を再提示し、新しいr2も止まることと、access tokenの期限判定だけでは即時失効を反映しないことを比較します。
個別のトークンの期限と、同じ認証から発行した系列の失効を分けて管理します。
1系列と使用済み状態を読む
family 1に未使用のr1があります。この例のr1などは台帳の識別子であり、実際の秘密値として使う短い文字列ではありません。
from tokens import *
db = store()独立したトークン台帳を作る
print(db.execute("SELECT * FROM families").fetchall())
print(db.execute("SELECT * FROM refresh使用済み状態を確認する").fetchall())系列は有効、r1は未使用です。発行済みの値を系列へ対応付けています。
2短期access tokenの期限を確認する
例のaccess tokenは基準時刻から300秒で期限切れになります。
access = jwt({"iss":"https://idp.example.test","sub":"alice","aud":"orders-api","exp":300,"scope":"orders:read"})
print(api(access,"orders:read",now=299期限の直前で検証する))
print(api(access,"orders:read",now=300期限境界で検証する))299ではallowed、300ではexpiredです。refresh tokenの状態はこの署名と期限だけの検証には含まれません。
3r1を消費してr2を発行する
回転処理は、使用済み化と新規登録を同じトランザクションで行います。
print(rotate(db,"r1")古い値を消費して次の値を発行する)
print(db.execute("SELECT * FROM refresh ORDER BY token").fetchall())r1のusedは1、r2は0です。古い値を捨てるだけでなく、再利用を検知するための記録を残します。
4r2でさらに更新する
現在有効なr2を使います。
print(rotate(db,"r2")次の世代へ更新する)
print(db.execute("SELECT * FROM refresh ORDER BY token").fetchall())r3が発行され、r1とr2は使用済みです。系列は同じfamily 1のままです。
5古いr1を再提示する
利用者が既にr3へ進んだ後、r1が再び提示された場合です。
print(rotate(db,"r1")使用済みの値を再提示する)
print(db.execute("SELECT * FROM families系列の失効状態を確認する").fetchall())reuse_detectedでfamily 1を失効させました。正当な側と漏えいした値を使う側を判別できないため、この例では系列全体を止めます。
6最新のr3も拒否する
未使用だったr3でも、系列が失効していれば更新しません。
print(rotate(db,"r3")最新世代の使用を試す)
print(db.execute("SELECT COUNT(*)新しい値が発行されていないことを確認する FROM refresh").fetchone())family_revokedで、トークン数は3のままです。失効した系列から新しい値を増やしていません。
7access tokenへの反映方法を比較する
時刻60ではaccess token自体の署名と期限はまだ正しい状態です。オンラインの系列照会を追加した場合と比較します。
print(api(access,"orders:read",now=60)署名と期限だけで検証する)
print(active(db,60)系列の失効状態も照会する)署名と期限だけならallowedですが、系列照会ではFalseです。即時失効が必要なら照会や拒否リストなどが必要で、短期化だけでは期限までの時間が残ります。
まとめ
refresh tokenを世代と系列で管理します。
使用済み値の再利用を検知し、系列を止める方針を定めます。
access tokenの即時失効には、期限以外の状態確認が必要になる場合があります。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from tokens import *
db = store()
print(db.execute("SELECT * FROM families").fetchall())
print(db.execute("SELECT * FROM refresh").fetchall())[(1, 0)]
[('r1', 1, 0)]access = jwt({"iss":"https://idp.example.test","sub":"alice","aud":"orders-api","exp":300,"scope":"orders:read"})
print(api(access,"orders:read",now=299))
print(api(access,"orders:read",now=300))allowed expired
print(rotate(db,"r1"))
print(db.execute("SELECT * FROM refresh ORDER BY token").fetchall())r2
[('r1', 1, 1), ('r2', 1, 0)]print(rotate(db,"r2"))
print(db.execute("SELECT * FROM refresh ORDER BY token").fetchall())r3
[('r1', 1, 1), ('r2', 1, 1), ('r3', 1, 0)]print(rotate(db,"r1"))
print(db.execute("SELECT * FROM families").fetchall())reuse_detected [(1, 1)]
print(rotate(db,"r3"))
print(db.execute("SELECT COUNT(*) FROM refresh").fetchone())family_revoked (3,)
print(api(access,"orders:read",now=60))
print(active(db,60))allowed False
演習完了