fix(auth): revoke current access token session

This commit is contained in:
shuaiplus
2026-07-06 13:49:06 +08:00
parent fd46dffc34
commit 2df43ccdb0
4 changed files with 49 additions and 0 deletions
+29
View File
@@ -163,6 +163,30 @@ function withWebRefreshCookie(request: Request, response: Response, refreshToken
});
}
async function revokePresentedAccessTokenSession(request: Request, env: Env, storage: StorageService): Promise<void> {
const authHeader = request.headers.get('Authorization');
if (!authHeader) return;
const auth = new AuthService(env);
const verified = await auth.verifyAccessTokenWithUser(authHeader);
if (!verified) return;
const deviceIdentifier = String(verified.payload.did || '').trim();
if (deviceIdentifier) {
const nextSessionStamp = generateUUID();
await storage.rotateDeviceSessionStamp(verified.user.id, deviceIdentifier, nextSessionStamp);
await storage.deleteRefreshTokensByDevice(verified.user.id, deviceIdentifier);
AuthService.invalidateDeviceCache(verified.user.id, deviceIdentifier);
return;
}
verified.user.securityStamp = generateUUID();
verified.user.updatedAt = new Date().toISOString();
await storage.saveUser(verified.user);
await storage.deleteRefreshTokensByUserId(verified.user.id);
AuthService.invalidateUserCache(verified.user.id);
}
function buildPreloginResponse(
email: string,
kdfType: number,
@@ -1010,6 +1034,11 @@ export async function handlePrelogin(request: Request, env: Env): Promise<Respon
// RFC 7009 allows returning 200 even if token is unknown.
export async function handleRevocation(request: Request, env: Env): Promise<Response> {
const storage = new StorageService(env.DB);
try {
await revokePresentedAccessTokenSession(request, env, storage);
} catch {
// RFC 7009 revocation is best-effort and should not reveal token state.
}
let body: Record<string, string>;
const contentType = request.headers.get('content-type') || '';
+14
View File
@@ -97,6 +97,20 @@ export async function touchDeviceLastSeen(
return Number(result.meta.changes ?? 0) > 0;
}
export async function rotateDeviceSessionStamp(
db: D1Database,
userId: string,
deviceIdentifier: string,
sessionStamp: string
): Promise<boolean> {
const now = new Date().toISOString();
const result = await db
.prepare('UPDATE devices SET session_stamp = ?, updated_at = ? WHERE user_id = ? AND device_identifier = ?')
.bind(sessionStamp, now, userId, deviceIdentifier)
.run();
return Number(result.meta.changes ?? 0) > 0;
}
export async function updateDeviceKeys(
db: D1Database,
userId: string,
+5
View File
@@ -109,6 +109,7 @@ import {
isKnownDevice as getKnownStoredDevice,
isKnownDeviceByEmail as getKnownStoredDeviceByEmail,
saveTrustedTwoFactorDeviceToken as saveStoredTrustedDeviceToken,
rotateDeviceSessionStamp as rotateStoredDeviceSessionStamp,
touchDeviceLastSeen as touchStoredDeviceLastSeen,
upsertDevice as saveStoredDevice,
updateDeviceName as updateStoredDeviceName,
@@ -761,6 +762,10 @@ export class StorageService {
return findStoredDevice(this.db, userId, deviceIdentifier);
}
async rotateDeviceSessionStamp(userId: string, deviceIdentifier: string, sessionStamp: string): Promise<boolean> {
return rotateStoredDeviceSessionStamp(this.db, userId, deviceIdentifier, sessionStamp);
}
async updateDeviceKeys(
userId: string,
deviceIdentifier: string,
+1
View File
@@ -402,6 +402,7 @@ export async function revokeCurrentSession(session: SessionState | null): Promis
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...(session?.accessToken ? { Authorization: `Bearer ${session.accessToken}` } : {}),
...(session?.authMode === 'web-cookie' ? { [WEB_SESSION_HEADER]: '1' } : {}),
},
body: body.toString(),