Skip to content

Commit 9f770c2

Browse files
committed
fix(auth): invalidate sessions on logout via pepper rotation
On logout, rotate the user's api_token_pepper so any previously-issued JWT becomes immediately invalid. Add a pepper check in the jwt callback's pass-through path so the NextAuth session endpoint also enforces revocation — not just the getUserFromAuth API path. Closes Pylon #6353. Trade-off: logout is effectively 'log out everywhere' since the pepper is per-user, not per-session.
1 parent b61446e commit 9f770c2

File tree

1 file changed

+42
-1
lines changed

1 file changed

+42
-1
lines changed

src/lib/user.server.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ import { secondsInDay } from 'date-fns/constants';
3333
import type { AdapterUser } from 'next-auth/adapters';
3434
import assert from 'node:assert';
3535
import type { Organization, User } from '@kilocode/db/schema';
36+
import { kilocode_users } from '@kilocode/db/schema';
3637
import type { AuthProviderId } from '@kilocode/db/schema-types';
38+
import { eq } from 'drizzle-orm';
39+
import crypto from 'node:crypto';
3740
import PostHogClient from '@/lib/posthog';
3841
import { captureException } from '@sentry/nextjs';
3942
import {
@@ -637,7 +640,20 @@ const authOptions: NextAuthOptions = {
637640
async jwt({ token, account, user, trigger, profile }) {
638641
let accountInfo: CreateOrUpdateUserArgs | undefined = undefined;
639642
try {
640-
if (!trigger) return token;
643+
if (!trigger) {
644+
// SECURITY: Validate that the pepper baked into the JWT still matches the
645+
// current pepper in the database. If the pepper was rotated (on logout or
646+
// via "Reset API Key"), this rejects the now-stale token and forces
647+
// re-authentication, closing the session-invalidation gap described in
648+
// Pylon #6353.
649+
if (token.kiloUserId && token.pepper) {
650+
const currentUser = await findUserById(token.kiloUserId, readDb);
651+
if (!currentUser || currentUser.api_token_pepper !== token.pepper) {
652+
return null;
653+
}
654+
}
655+
return token;
656+
}
641657
if (!account) throw new Error(`TRAP: No account found: ${trigger}`);
642658

643659
accountInfo = createAccountInfo(account, user, profile);
@@ -677,6 +693,31 @@ const authOptions: NextAuthOptions = {
677693
return session;
678694
},
679695
},
696+
events: {
697+
// SECURITY: Rotate the user's api_token_pepper on logout so that any JWT
698+
// tokens issued before this logout are immediately rejected by the pepper
699+
// check in the jwt callback above. Without this, a captured token would
700+
// remain valid for up to 30 days after the user logs out (Pylon #6353).
701+
//
702+
// Trade-off: because the pepper is per-user (not per-session), this
703+
// invalidates ALL of the user's active sessions across all devices.
704+
// Logging out of one device is effectively a "log out everywhere" action.
705+
async signOut({ token }) {
706+
const kiloUserId = token?.kiloUserId;
707+
if (!kiloUserId) return;
708+
try {
709+
await db
710+
.update(kilocode_users)
711+
.set({ api_token_pepper: crypto.randomUUID() })
712+
.where(eq(kilocode_users.id, kiloUserId));
713+
} catch (error) {
714+
captureException(error, {
715+
tags: { operation: 'session_pepper_rotation_on_signout' },
716+
extra: { kiloUserId },
717+
});
718+
}
719+
},
720+
},
680721
pages: {
681722
signIn: '/users/sign_in',
682723
error: '/users/sign_in',

0 commit comments

Comments
 (0)