Files
cronmaster/app/api/oidc/callback/route.ts
2025-11-11 11:33:51 +00:00

156 lines
4.7 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { jwtVerify, createRemoteJWKSet } from "jose";
import {
createSession,
getSessionCookieName,
} from "@/app/_utils/session-utils";
export async function GET(request: NextRequest) {
const appUrl = process.env.APP_URL || request.nextUrl.origin;
if (process.env.SSO_MODE !== "oidc") {
return NextResponse.redirect(`${appUrl}/login`);
}
let issuer = process.env.OIDC_ISSUER || "";
if (issuer && !issuer.endsWith("/")) {
issuer = `${issuer}/`;
}
const clientId = process.env.OIDC_CLIENT_ID || "";
if (!issuer || !clientId) {
return NextResponse.redirect(`${appUrl}/login`);
}
const code = request.nextUrl.searchParams.get("code");
const state = request.nextUrl.searchParams.get("state");
const savedState = request.cookies.get("oidc_state")?.value;
const verifier = request.cookies.get("oidc_verifier")?.value;
const nonce = request.cookies.get("oidc_nonce")?.value;
if (!code || !state || !savedState || state !== savedState || !verifier) {
if (process.env.DEBUGGER) {
console.log("[OIDC Callback] Missing or invalid parameters", {
hasCode: !!code,
hasState: !!state,
hasSavedState: !!savedState,
statesMatch: state === savedState,
hasVerifier: !!verifier,
});
}
return NextResponse.redirect(`${appUrl}/login`);
}
try {
const discoveryUrl = issuer.endsWith("/")
? `${issuer}.well-known/openid-configuration`
: `${issuer}/.well-known/openid-configuration`;
const discoveryRes = await fetch(discoveryUrl, { cache: "no-store" });
if (!discoveryRes.ok) {
if (process.env.DEBUGGER) {
console.log("[OIDC Callback] Discovery failed");
}
return NextResponse.redirect(`${appUrl}/login`);
}
const discovery = (await discoveryRes.json()) as {
token_endpoint: string;
jwks_uri: string;
issuer: string;
};
const tokenEndpoint = discovery.token_endpoint;
const jwksUri = discovery.jwks_uri;
const oidcIssuer = discovery.issuer;
const JWKS = createRemoteJWKSet(new URL(jwksUri));
const redirectUri = `${appUrl}/api/oidc/callback`;
const clientSecret = process.env.OIDC_CLIENT_SECRET;
const body = new URLSearchParams();
body.set("grant_type", "authorization_code");
body.set("code", code);
body.set("redirect_uri", redirectUri);
body.set("client_id", clientId);
body.set("code_verifier", verifier);
if (clientSecret) {
body.set("client_secret", clientSecret);
}
const tokenRes = await fetch(tokenEndpoint, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body,
});
if (!tokenRes.ok) {
if (process.env.DEBUGGER) {
console.log("[OIDC Callback] Token request failed:", tokenRes.status);
}
return NextResponse.redirect(`${appUrl}/login`);
}
const token = (await tokenRes.json()) as { id_token?: string };
const idToken = token.id_token;
if (!idToken) {
if (process.env.DEBUGGER) {
console.log("[OIDC Callback] No id_token in response");
}
return NextResponse.redirect(`${appUrl}/login`);
}
let claims: { [key: string]: any };
try {
const { payload } = await jwtVerify(idToken, JWKS, {
issuer: oidcIssuer,
audience: clientId,
clockTolerance: 5,
});
claims = payload;
} catch (error) {
console.error("[OIDC Callback] ID Token validation failed:", error);
return NextResponse.redirect(`${appUrl}/login`);
}
if (nonce && claims.nonce && claims.nonce !== nonce) {
if (process.env.DEBUGGER) {
console.log("[OIDC Callback] Nonce mismatch");
}
return NextResponse.redirect(`${appUrl}/login`);
}
if (process.env.DEBUGGER) {
console.log("[OIDC Callback] Successfully authenticated user:", {
sub: claims.sub,
email: claims.email,
preferred_username: claims.preferred_username,
});
}
const sessionId = await createSession("oidc");
const response = NextResponse.redirect(`${appUrl}/`);
const cookieName = getSessionCookieName();
const isSecure =
process.env.NODE_ENV === "production" && process.env.HTTPS === "true";
response.cookies.set(cookieName, sessionId, {
httpOnly: true,
secure: isSecure,
sameSite: "lax",
path: "/",
maxAge: 30 * 24 * 60 * 60,
});
response.cookies.delete("oidc_verifier");
response.cookies.delete("oidc_state");
response.cookies.delete("oidc_nonce");
return response;
} catch (error) {
console.error("[OIDC Callback] Error:", error);
return NextResponse.redirect(`${appUrl}/login`);
}
}