import KeycloakProvider from "next-auth/providers/keycloak"; import type {NextAuthOptions} from "next-auth"; interface TypedJWT { access_token?: string; refresh_token?: string; [key: string]: unknown; } const { KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, KEYCLOAK_ISSUER, NEXTAUTH_SECRET, } = process.env; if (!KEYCLOAK_CLIENT_ID) throw new Error("Missing KEYCLOAK_CLIENT_ID"); if (!KEYCLOAK_CLIENT_SECRET) throw new Error("Missing KEYCLOAK_CLIENT_SECRET"); if (!KEYCLOAK_ISSUER) throw new Error("Missing KEYCLOAK_ISSUER"); if (!NEXTAUTH_SECRET) throw new Error("Missing NEXTAUTH_SECRET"); console.log("[auth] Using Keycloak provider:"); console.log(" - Client ID:", KEYCLOAK_CLIENT_ID); console.log(" - Issuer:", KEYCLOAK_ISSUER); async function isTokenValid(token: string): Promise { try { const res = await fetch(`${KEYCLOAK_ISSUER}/protocol/openid-connect/userinfo`, { headers: { Authorization: `Bearer ${token}`, }, }); return res.ok; } catch (error) { console.error("[auth] Failed to validate access token:", error); return false; } } export const authOptions: NextAuthOptions = { providers: [ KeycloakProvider({ clientId: KEYCLOAK_CLIENT_ID, clientSecret: KEYCLOAK_CLIENT_SECRET, issuer: KEYCLOAK_ISSUER, }), ], secret: NEXTAUTH_SECRET, session: { strategy: "jwt", }, callbacks: { async jwt({token, account}) { if (account) { token.access_token = account.access_token; token.refresh_token = account.refresh_token; console.log("[auth] JWT callback: new login from Keycloak"); return token; } const {access_token} = token as TypedJWT; if (access_token) { const valid = await isTokenValid(access_token); if (!valid) { console.warn("[auth] Access token invalid — clearing session"); return {}; } } console.log("[auth] JWT callback: reusing existing token"); return token; }, async session({session, token}) { const {access_token, refresh_token} = token as TypedJWT; console.log("[auth] Session callback: enriching session with tokens"); return { ...session, accessToken: access_token, refreshToken: refresh_token, }; }, }, };