import KeycloakProvider from "next-auth/providers/keycloak"; import type {NextAuthOptions} from "next-auth"; import {getAccessToken} from "@/lib/api/auth/tokenUtils"; 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, refresh_token} = token as TypedJWT; if (access_token) { // Use centralized getAccessToken function const tokenResult = await getAccessToken(access_token, refresh_token); if (tokenResult.accessToken) { token.access_token = tokenResult.accessToken; if (tokenResult.refreshToken) { token.refresh_token = tokenResult.refreshToken; } if (tokenResult.refreshed) { console.log("[auth] Token refreshed successfully in JWT callback"); return token; } // If token wasn't refreshed, fall back to network validation const valid = await isTokenValid(tokenResult.accessToken); if (!valid) { console.warn("[auth] Access token invalid — clearing session"); return {}; } } else { console.warn("[auth] No valid access token available — clearing session"); return {}; } } return token; }, async session({session, token}) { const {access_token, refresh_token} = token as TypedJWT; return { ...session, accessToken: access_token, refreshToken: refresh_token, }; }, }, };