From 7c67e53b1e4476b68e5ae1bf0deb666a376bdbec Mon Sep 17 00:00:00 2001 From: chyoo Date: Mon, 6 Jul 2026 09:52:51 +0900 Subject: [PATCH] sync and revoke auth refresh sessions --- src/auth/repository.ts | 11 +++++++++++ src/auth/routes.ts | 13 ++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/auth/repository.ts b/src/auth/repository.ts index ccf92c2..af45d65 100644 --- a/src/auth/repository.ts +++ b/src/auth/repository.ts @@ -516,6 +516,17 @@ export async function revokeRefreshToken(refreshToken: string): Promise { ]) } +export async function revokeRefreshTokensForUser(userId: string): Promise { + await prisma.refreshToken.updateMany({ + where: { + userId, + revokedAt: null, + expiresAt: { gt: new Date() } + }, + data: { revokedAt: new Date() } + }) +} + export async function createHandoffToken(userId: string, audienceApp: string): Promise { const token = createOpaqueToken() await redis.set( diff --git a/src/auth/routes.ts b/src/auth/routes.ts index 4fd4ccd..2bfbd61 100644 --- a/src/auth/routes.ts +++ b/src/auth/routes.ts @@ -4,7 +4,7 @@ import { z } from 'zod' import { config } from '../config' import { sendEmailVerificationCode } from '../mail' import { verifyPassword } from '../security/password' -import { signAccessToken } from '../security/tokens' +import { signAccessToken, verifyAccessToken } from '../security/tokens' import type { AuthUser } from '../types' import { authRequired, type AuthVariables } from './middleware' import { @@ -25,6 +25,7 @@ import { markLastLogin, recordFailedLogin, revokeRefreshToken, + revokeRefreshTokensForUser, rotateRefreshToken, storeEmailVerificationCode, storePasswordResetCode, @@ -381,6 +382,16 @@ authRoutes.post('/logout', async (c) => { await revokeRefreshToken(refreshToken) } + const bearerToken = c.req.header('authorization')?.match(/^Bearer\s+(.+)$/i)?.[1] + if (bearerToken) { + try { + const claims = await verifyAccessToken(bearerToken) + await revokeRefreshTokensForUser(claims.sub) + } catch { + // Logout should remain idempotent even when the access token is already invalid. + } + } + clearRefreshCookie(c) return c.json({ ok: true }) })