import { cookies } from "next/headers" import { NextResponse } from "next/server" import { resolveServerOidcConfig, getAppOrigin, oidcServerFetchHeaders, } from "@/lib/auth/oidc-config" import { platformUserFromToken } from "@/lib/auth/jwt-claims" import { applySessionCookies, type TokenResponse, } from "@/lib/auth/session" const PKCE_COOKIE = "ulti_pkce_verifier" const STATE_COOKIE = "ulti_oauth_state" const INTENT_COOKIE = "ulti_auth_intent" const PREVIOUS_SUB_COOKIE = "ulti_auth_previous_sub" export async function GET(request: Request) { const url = new URL(request.url) const appOrigin = getAppOrigin() const code = url.searchParams.get("code") const state = url.searchParams.get("state") const oauthError = url.searchParams.get("error") if (oauthError) { const desc = url.searchParams.get("error_description") ?? oauthError return NextResponse.redirect( new URL(`/login?error=${encodeURIComponent(desc)}`, appOrigin) ) } if (!code || !state) { return NextResponse.redirect( new URL("/login?error=missing_code", appOrigin) ) } const jar = await cookies() const expectedState = jar.get(STATE_COOKIE)?.value const verifier = jar.get(PKCE_COOKIE)?.value const returnTo = jar.get("ulti_auth_return")?.value ?? "/mail/inbox" const authIntent = jar.get(INTENT_COOKIE)?.value const previousSub = jar.get(PREVIOUS_SUB_COOKIE)?.value if (!expectedState || state !== expectedState || !verifier) { return NextResponse.redirect( new URL( `/login?error=${encodeURIComponent( !expectedState || !verifier ? "invalid_state:missing_oauth_cookies" : "invalid_state:state_mismatch" )}`, appOrigin ) ) } let cfg try { cfg = await resolveServerOidcConfig() } catch (err) { const message = err instanceof Error ? err.message : "oidc_discovery_failed" return NextResponse.redirect( new URL(`/login?error=${encodeURIComponent(message)}`, appOrigin) ) } const body = new URLSearchParams({ grant_type: "authorization_code", client_id: cfg.clientId, client_secret: cfg.clientSecret, code, redirect_uri: cfg.redirectUri, code_verifier: verifier, }) let tokens: TokenResponse try { const res = await fetch(cfg.tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", ...oidcServerFetchHeaders(), }, body, }) if (!res.ok) { const text = await res.text() return NextResponse.redirect( new URL( `/login?error=${encodeURIComponent(`token_exchange_failed:${text.slice(0, 120)}`)}`, appOrigin ) ) } tokens = (await res.json()) as TokenResponse } catch (err) { const message = err instanceof Error ? err.message : "token_exchange_failed" return NextResponse.redirect( new URL(`/login?error=${encodeURIComponent(message)}`, appOrigin) ) } if (!tokens.id_token) { return NextResponse.redirect( new URL("/login?error=no_id_token", appOrigin) ) } const bearer = tokens.id_token const newUser = platformUserFromToken(bearer) const completeUrl = new URL("/auth/complete", appOrigin) completeUrl.searchParams.set("returnTo", returnTo) if ( authIntent === "add_account" && previousSub && newUser?.sub === previousSub ) { completeUrl.searchParams.set("accountNotice", "same") } const response = NextResponse.redirect(completeUrl) response.cookies.delete(PKCE_COOKIE) response.cookies.delete(STATE_COOKIE) response.cookies.delete("ulti_auth_return") response.cookies.delete(INTENT_COOKIE) response.cookies.delete(PREVIOUS_SUB_COOKIE) applySessionCookies(response, tokens, bearer) return response }