import { NextAuthOptions } from "next-auth"; import { JWT } from "next-auth/jwt"; import KeycloakProvider from "next-auth/providers/keycloak"; import Avatar from "@assets/avatar/avatar-small.jpeg"; type KeycloakTokenResponse = { access_token: string; expires_in: number; refresh_token?: string; }; const keycloakIssuer = process.env.KEYCLOAK_ISSUER!; const keycloakClientId = process.env.KEYCLOAK_CLIENT_ID!; const keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET!; const keycloakTokenEndpoint = `${keycloakIssuer.replace(/\/$/, "")}/protocol/openid-connect/token`; const refreshAccessToken = async (token: JWT): Promise => { if (!token.refreshToken) { return { ...token, error: "RefreshAccessTokenError" }; } const body = new URLSearchParams({ grant_type: "refresh_token", client_id: keycloakClientId, client_secret: keycloakClientSecret, refresh_token: token.refreshToken, }); const response = await fetch(keycloakTokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, }); const refreshed = (await response.json()) as KeycloakTokenResponse; if (!response.ok || !refreshed.access_token || typeof refreshed.expires_in !== "number") { return { ...token, error: "RefreshAccessTokenError" }; } return { ...token, accessToken: refreshed.access_token, accessTokenExpires: Date.now() + refreshed.expires_in * 1000, refreshToken: refreshed.refresh_token ?? token.refreshToken, error: undefined, }; }; const authOptions: NextAuthOptions = { // Configure one or more authentication providers providers: [ KeycloakProvider({ clientId: keycloakClientId, clientSecret: keycloakClientSecret, issuer: keycloakIssuer, profile(profile) { return { id: profile.sub, name: profile.name ?? profile.preferred_username, email: profile.email, image: Avatar.src, }; }, }), ], secret: process.env.NEXTAUTH_SECRET, callbacks: { jwt: async ({ token, profile, account }) => { if (profile?.sub) { token.sub = profile.sub; } if (account) { if (account.access_token) { token.accessToken = account.access_token; } if (account.refresh_token) { token.refreshToken = account.refresh_token; } if (typeof account.expires_at === "number") { token.accessTokenExpires = account.expires_at * 1000; } token.error = undefined; return token; } if (typeof token.accessTokenExpires === "number" && Date.now() < token.accessTokenExpires - 30_000) { return token; } return refreshAccessToken(token); }, session: async ({ session, token }) => { if (session.user && token.sub) { session.user.id = token.sub; } if (token.accessToken) { session.accessToken = token.accessToken; } if (token.error) { session.error = token.error; } return session; }, }, }; export default authOptions;