From fabf6a5c38b1c58d3380ed0dea35fca487d95827 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 14:23:06 +0000 Subject: [PATCH] fix: state-replay race, share session options, idealo cheerio type --- src/app/api/auth/callback/route.ts | 9 +++++++-- src/lib/auth/session.ts | 8 +++----- src/lib/scrapers/idealo.ts | 2 +- src/middleware.ts | 7 ++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/app/api/auth/callback/route.ts b/src/app/api/auth/callback/route.ts index 8cb3462..b94fe74 100644 --- a/src/app/api/auth/callback/route.ts +++ b/src/app/api/auth/callback/route.ts @@ -14,8 +14,14 @@ export async function GET(req: NextRequest) { return NextResponse.json({ error: 'state mismatch' }, { status: 400 }) } + // Consume the pending login atomically before doing the token exchange + // to prevent state replay on concurrent callbacks. + const { codeVerifier } = pending + delete session.loginInProgress + await session.save() + const redirectUri = `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/callback` - const tokens = await exchangeCode({ code, codeVerifier: pending.codeVerifier, redirectUri }) + const tokens = await exchangeCode({ code, codeVerifier, redirectUri }) const claims = await verifyIdToken(tokens.id_token) if (!isAllowedUser(claims.sub)) { @@ -26,7 +32,6 @@ export async function GET(req: NextRequest) { session.userId = claims.sub session.email = claims.email session.name = claims.name - delete session.loginInProgress await session.save() return NextResponse.redirect(new URL('/', req.url)) diff --git a/src/lib/auth/session.ts b/src/lib/auth/session.ts index 06a4bc9..ae02b2b 100644 --- a/src/lib/auth/session.ts +++ b/src/lib/auth/session.ts @@ -9,7 +9,7 @@ export interface SessionData { loginInProgress?: { state: string; codeVerifier: string } } -const opts: SessionOptions = { +export const sessionOptions: SessionOptions = { password: process.env.SESSION_PASSWORD || '', cookieName: 'preis_tracker_session', cookieOptions: { @@ -21,11 +21,9 @@ const opts: SessionOptions = { } export async function getSession() { - if (!opts.password || (typeof opts.password === 'string' && opts.password.length < 32)) { + if (!sessionOptions.password || (typeof sessionOptions.password === 'string' && sessionOptions.password.length < 32)) { throw new Error('SESSION_PASSWORD must be set to a 32+ char value') } const c = await cookies() - return getIronSession(c, opts) + return getIronSession(c, sessionOptions) } - -export const sessionOptions = opts diff --git a/src/lib/scrapers/idealo.ts b/src/lib/scrapers/idealo.ts index 74fd253..fff610e 100644 --- a/src/lib/scrapers/idealo.ts +++ b/src/lib/scrapers/idealo.ts @@ -49,7 +49,7 @@ export const idealoScraper: PriceScraper = { }, } -function extractJsonLdPrice($: cheerio.CheerioAPI): string | null { +function extractJsonLdPrice($: cheerio.Root): string | null { const scripts = $('script[type="application/ld+json"]') for (let i = 0; i < scripts.length; i++) { const raw = $(scripts[i]).contents().text() diff --git a/src/middleware.ts b/src/middleware.ts index c257baf..08b9c4e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { getIronSession } from 'iron-session' -import type { SessionData } from '@/lib/auth/session' +import { sessionOptions, type SessionData } from '@/lib/auth/session' const PUBLIC_PREFIXES = ['/api/auth/', '/api/cron/', '/_next/', '/favicon'] @@ -9,10 +9,7 @@ export async function middleware(req: NextRequest) { if (PUBLIC_PREFIXES.some((p) => pathname.startsWith(p))) return NextResponse.next() const res = NextResponse.next() - const session = await getIronSession(req, res, { - password: process.env.SESSION_PASSWORD || '', - cookieName: 'preis_tracker_session', - }) + const session = await getIronSession(req, res, sessionOptions) if (!session.userId) { if (pathname.startsWith('/api/')) {