一覧SC-10-3 · SSOと認証連携

リフレッシュトークンの更新と再利用対策

個別のトークンの期限と、同じ認証から発行した系列の失効を分けて管理します。

説明

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 / 公開試料と固定時刻

1
from tokens import * db = store() print(db.execute("SELECT * FROM families").fetchall()) print(db.execute("SELECT * FROM refresh").fetchall())
[(1, 0)]
[('r1', 1, 0)]
2
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
3
print(rotate(db,"r1")) print(db.execute("SELECT * FROM refresh ORDER BY token").fetchall())
r2
[('r1', 1, 1), ('r2', 1, 0)]
4
print(rotate(db,"r2")) print(db.execute("SELECT * FROM refresh ORDER BY token").fetchall())
r3
[('r1', 1, 1), ('r2', 1, 1), ('r3', 1, 0)]
5
print(rotate(db,"r1")) print(db.execute("SELECT * FROM families").fetchall())
reuse_detected
[(1, 1)]
6
print(rotate(db,"r3")) print(db.execute("SELECT COUNT(*) FROM refresh").fetchone())
family_revoked
(3,)
7
print(api(access,"orders:read",now=60)) print(active(db,60))
allowed
False

演習完了