sync and revoke auth refresh sessions
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-06 09:52:51 +09:00
parent 28b34f82de
commit 7c67e53b1e
2 changed files with 23 additions and 1 deletions
+11
View File
@@ -516,6 +516,17 @@ export async function revokeRefreshToken(refreshToken: string): Promise<void> {
]) ])
} }
export async function revokeRefreshTokensForUser(userId: string): Promise<void> {
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<string> { export async function createHandoffToken(userId: string, audienceApp: string): Promise<string> {
const token = createOpaqueToken() const token = createOpaqueToken()
await redis.set( await redis.set(
+12 -1
View File
@@ -4,7 +4,7 @@ import { z } from 'zod'
import { config } from '../config' import { config } from '../config'
import { sendEmailVerificationCode } from '../mail' import { sendEmailVerificationCode } from '../mail'
import { verifyPassword } from '../security/password' import { verifyPassword } from '../security/password'
import { signAccessToken } from '../security/tokens' import { signAccessToken, verifyAccessToken } from '../security/tokens'
import type { AuthUser } from '../types' import type { AuthUser } from '../types'
import { authRequired, type AuthVariables } from './middleware' import { authRequired, type AuthVariables } from './middleware'
import { import {
@@ -25,6 +25,7 @@ import {
markLastLogin, markLastLogin,
recordFailedLogin, recordFailedLogin,
revokeRefreshToken, revokeRefreshToken,
revokeRefreshTokensForUser,
rotateRefreshToken, rotateRefreshToken,
storeEmailVerificationCode, storeEmailVerificationCode,
storePasswordResetCode, storePasswordResetCode,
@@ -381,6 +382,16 @@ authRoutes.post('/logout', async (c) => {
await revokeRefreshToken(refreshToken) 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) clearRefreshCookie(c)
return c.json({ ok: true }) return c.json({ ok: true })
}) })