feat: add overlap grace period for refresh tokens to handle concurrent requests

This commit is contained in:
shuaiplus
2026-02-25 00:22:31 +08:00
parent 6621738b02
commit 026aea03dc
3 changed files with 29 additions and 2 deletions
+3
View File
@@ -6,6 +6,9 @@
// Refresh token lifetime in milliseconds. // Refresh token lifetime in milliseconds.
// 刷新令牌有效期(毫秒)。 // 刷新令牌有效期(毫秒)。
refreshTokenTtlMs: 30 * 24 * 60 * 60 * 1000, refreshTokenTtlMs: 30 * 24 * 60 * 60 * 1000,
// Grace window for previous refresh token after rotation (ms).
// 刷新令牌轮换后的旧令牌宽限窗口(毫秒)。
refreshTokenOverlapGraceMs: 60 * 1000,
// Refresh token random byte length. // Refresh token random byte length.
// 刷新令牌随机字节长度。 // 刷新令牌随机字节长度。
refreshTokenRandomBytes: 32, refreshTokenRandomBytes: 32,
+6 -2
View File
@@ -244,8 +244,12 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
return identityErrorResponse('Invalid refresh token', 'invalid_grant', 400); return identityErrorResponse('Invalid refresh token', 'invalid_grant', 400);
} }
// Revoke old refresh token (prevent reuse) // Keep a short overlap window for old refresh token to absorb
await storage.deleteRefreshToken(refreshToken); // concurrent refresh requests from multiple client contexts.
await storage.constrainRefreshTokenExpiry(
refreshToken,
Date.now() + LIMITS.auth.refreshTokenOverlapGraceMs
);
const { accessToken, user } = result; const { accessToken, user } = result;
const newRefreshToken = await auth.generateRefreshToken(user.id); const newRefreshToken = await auth.generateRefreshToken(user.id);
+20
View File
@@ -643,6 +643,26 @@ export class StorageService {
await this.db.prepare('DELETE FROM refresh_tokens WHERE token = ?').bind(tokenKey).run(); await this.db.prepare('DELETE FROM refresh_tokens WHERE token = ?').bind(tokenKey).run();
} }
// Keep a short overlap window for rotated refresh token to reduce
// multi-context refresh races (e.g. browser extension popup/background).
// Expiry is only tightened, never extended.
async constrainRefreshTokenExpiry(token: string, maxExpiresAtMs: number): Promise<void> {
const tokenKey = await this.refreshTokenKey(token);
await this.db.prepare(
'UPDATE refresh_tokens ' +
'SET expires_at = CASE WHEN expires_at > ? THEN ? ELSE expires_at END ' +
'WHERE token = ?'
).bind(maxExpiresAtMs, maxExpiresAtMs, tokenKey).run();
// Best-effort legacy plaintext support for older rows.
await this.db.prepare(
'UPDATE refresh_tokens ' +
'SET expires_at = CASE WHEN expires_at > ? THEN ? ELSE expires_at END ' +
'WHERE token = ?'
).bind(maxExpiresAtMs, maxExpiresAtMs, token).run();
}
private async trustedTwoFactorTokenKey(token: string): Promise<string> { private async trustedTwoFactorTokenKey(token: string): Promise<string> {
const digest = await this.sha256Hex(token); const digest = await this.sha256Hex(token);
return `sha256:${digest}`; return `sha256:${digest}`;