diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index a9a4d88..c54aea2 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,22 +1,145 @@ -import NextAuth from "next-auth"; -import { authOptions } from "@/lib/auth"; +import NextAuth, { NextAuthOptions } from "next-auth"; +import KeycloakProvider from "next-auth/providers/keycloak"; -const handler = NextAuth({ - ...authOptions, - debug: true, // Enable debug logging +declare module "next-auth" { + interface Session { + user: { + id: string; + name?: string | null; + email?: string | null; + image?: string | null; + username: string; + first_name: string; + last_name: string; + role: string[]; + }; + accessToken: string; + } + + interface JWT { + accessToken: string; + refreshToken: string; + accessTokenExpires: number; + role: string[]; + username: string; + first_name: string; + last_name: string; + } +} + +function getRequiredEnvVar(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +} + +export const authOptions: NextAuthOptions = { + providers: [ + KeycloakProvider({ + clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"), + clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"), + issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"), + profile(profile) { + return { + id: profile.sub, + name: profile.name ?? profile.preferred_username, + email: profile.email, + first_name: profile.given_name ?? '', + last_name: profile.family_name ?? '', + username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '', + role: profile.groups ?? [], + } + }, + }), + ], + session: { + strategy: "jwt", + maxAge: 30 * 24 * 60 * 60, // 30 days + }, callbacks: { - ...authOptions.callbacks, - async redirect({ url, baseUrl }) { - console.log('Redirect callback:', { url, baseUrl }); - // Allows relative callback URLs - if (url.startsWith("/")) return `${baseUrl}${url}`; - // Allows callback URLs on the same origin - else if (new URL(url).origin === baseUrl) return url; - return baseUrl; + async jwt({ token, account, profile }) { + if (account && profile) { + token.accessToken = account.access_token; + token.refreshToken = account.refresh_token; + token.accessTokenExpires = account.expires_at! * 1000; + token.role = (profile as any).groups ?? []; + token.username = (profile as any).preferred_username ?? profile.email?.split('@')[0] ?? ''; + token.first_name = (profile as any).given_name ?? ''; + token.last_name = (profile as any).family_name ?? ''; + } + + // Return previous token if not expired + if (Date.now() < (token.accessTokenExpires as number)) { + return token; + } + + try { + const clientId = getRequiredEnvVar("KEYCLOAK_CLIENT_ID"); + const clientSecret = getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"); + + const response = await fetch( + `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + client_id: clientId, + client_secret: clientSecret, + refresh_token: token.refreshToken as string, + }), + } + ); + + const tokens = await response.json(); + + if (!response.ok) { + throw new Error("RefreshAccessTokenError"); + } + + return { + ...token, + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token ?? token.refreshToken, + accessTokenExpires: Date.now() + tokens.expires_in * 1000, + }; + } catch (error) { + return { + ...token, + error: "RefreshAccessTokenError", + }; + } + }, + async session({ session, token }) { + if (token.error) { + throw new Error("RefreshAccessTokenError"); + } + + session.accessToken = token.accessToken; + session.user = { + ...session.user, + id: token.sub as string, + first_name: token.first_name ?? '', + last_name: token.last_name ?? '', + username: token.username ?? '', + role: token.role ?? [], + }; + + return session; } }, -}); + pages: { + signIn: '/signin', + error: '/signin', + }, + debug: process.env.NODE_ENV === 'development', +}; +const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; interface JWT { diff --git a/lib/auth.ts b/lib/auth.ts deleted file mode 100644 index 5ae2268..0000000 --- a/lib/auth.ts +++ /dev/null @@ -1,140 +0,0 @@ -import NextAuth, { NextAuthOptions } from "next-auth"; -import KeycloakProvider from "next-auth/providers/keycloak"; - -declare module "next-auth" { - interface Session { - user: { - id: string; - name?: string | null; - email?: string | null; - image?: string | null; - username: string; - first_name: string; - last_name: string; - role: string[]; - }; - accessToken: string; - } - - interface JWT { - accessToken: string; - refreshToken: string; - accessTokenExpires: number; - role: string[]; - username: string; - first_name: string; - last_name: string; - } -} - -function getRequiredEnvVar(name: string): string { - const value = process.env[name]; - if (!value) { - throw new Error(`Missing required environment variable: ${name}`); - } - return value; -} - -export const authOptions: NextAuthOptions = { - providers: [ - KeycloakProvider({ - clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"), - clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"), - issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"), - profile(profile) { - return { - id: profile.sub, - name: profile.name ?? profile.preferred_username, - email: profile.email, - first_name: profile.given_name ?? '', - last_name: profile.family_name ?? '', - username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '', - role: profile.groups ?? [], - } - }, - }), - ], - session: { - strategy: "jwt", - maxAge: 30 * 24 * 60 * 60, // 30 days - }, - callbacks: { - async jwt({ token, account, profile }) { - if (account && profile) { - token.accessToken = account.access_token!; - token.refreshToken = account.refresh_token!; - token.accessTokenExpires = account.expires_at! * 1000; - token.role = (profile as any).groups ?? []; - token.username = (profile as any).preferred_username ?? profile.email?.split('@')[0] ?? ''; - token.first_name = (profile as any).given_name ?? ''; - token.last_name = (profile as any).family_name ?? ''; - } - - // Return previous token if not expired - if (Date.now() < (token.accessTokenExpires as number)) { - return token; - } - - try { - const clientId = getRequiredEnvVar("KEYCLOAK_CLIENT_ID"); - const clientSecret = getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"); - - const response = await fetch( - `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, - { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - client_id: clientId, - client_secret: clientSecret, - refresh_token: token.refreshToken as string, - }), - } - ); - - const tokens = await response.json(); - - if (!response.ok) { - throw new Error("RefreshAccessTokenError"); - } - - return { - ...token, - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token ?? token.refreshToken, - accessTokenExpires: Date.now() + tokens.expires_in * 1000, - }; - } catch (error) { - return { - ...token, - error: "RefreshAccessTokenError", - }; - } - }, - async session({ session, token }) { - if (token.error) { - throw new Error("RefreshAccessTokenError"); - } - - session.accessToken = token.accessToken; - session.user = { - ...session.user, - id: token.sub as string, - first_name: token.first_name ?? '', - last_name: token.last_name ?? '', - username: token.username ?? '', - role: token.role ?? [], - }; - - return session; - } - }, - pages: { - signIn: '/signin', - error: '/signin', - }, - debug: process.env.NODE_ENV === 'development', -}; \ No newline at end of file