39 lines
1.4 KiB
TypeScript
39 lines
1.4 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { getSession } from '@/lib/auth/session'
|
|
import { exchangeCode, verifyIdToken, isAllowedUser } from '@/lib/auth/zitadel'
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const url = new URL(req.url)
|
|
const code = url.searchParams.get('code')
|
|
const state = url.searchParams.get('state')
|
|
if (!code || !state) return NextResponse.json({ error: 'missing code/state' }, { status: 400 })
|
|
|
|
const session = await getSession()
|
|
const pending = session.loginInProgress
|
|
if (!pending || pending.state !== state) {
|
|
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, redirectUri })
|
|
const claims = await verifyIdToken(tokens.id_token)
|
|
|
|
if (!isAllowedUser(claims.sub)) {
|
|
session.destroy()
|
|
return NextResponse.json({ error: 'user not allowed' }, { status: 403 })
|
|
}
|
|
|
|
session.userId = claims.sub
|
|
session.email = claims.email
|
|
session.name = claims.name
|
|
await session.save()
|
|
|
|
return NextResponse.redirect(new URL('/', req.url))
|
|
}
|