Files
work-club-manager/frontend/src/auth/auth.ts
T

103 lines
3.1 KiB
TypeScript
Raw Normal View History

import NextAuth 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
clubs?: Record<string, string>
isAdmin?: boolean
}
accessToken?: string
}
interface JWT {
clubs?: Record<string, string>
accessToken?: string
isAdmin?: boolean
}
}
// In Docker, the Next.js server reaches Keycloak via internal hostname
// (keycloak:8080) but the browser uses localhost:8080. Explicit endpoint
// URLs bypass OIDC discovery, avoiding issuer mismatch validation errors.
const issuerPublic = process.env.KEYCLOAK_ISSUER || 'http://localhost:30808/realms/workclub'
const issuerInternal = process.env.KEYCLOAK_ISSUER_INTERNAL || issuerPublic
const oidcPublic = `${issuerPublic}/protocol/openid-connect`
const oidcInternal = `${issuerInternal.replace(':8080', ':8081')}/protocol/openid-connect`
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
KeycloakProvider({
clientId: process.env.KEYCLOAK_CLIENT_ID || 'workclub-app',
issuer: issuerPublic,
authorization: {
url: `${oidcPublic}/auth`,
params: { scope: "openid email profile" },
},
token: `${oidcInternal}/token`,
userinfo: `${oidcInternal}/userinfo`,
jwks_endpoint: `${oidcInternal}/certs`,
})
],
trustHost: true,
cookies: {
pkceCodeVerifier: {
name: "authjs.pkce.code_verifier",
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: false,
},
},
state: {
name: "authjs.state",
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: false,
},
},
},
debug: true,
callbacks: {
async jwt({ token, account }) {
if (account && account.access_token) {
// Add clubs claim from Keycloak access token
token.clubs = (account as { clubs?: Record<string, string> }).clubs || {}
token.accessToken = account.access_token
}
// Always check admin status from the access token if available
if (token.accessToken) {
try {
const payload = JSON.parse(Buffer.from((token.accessToken as string).split('.')[1], 'base64').toString());
const roles = (payload.realm_access?.roles as string[]) || [];
token.isAdmin = roles.includes('admin');
console.log('[Auth Debug] Checking admin status:', { roles, isAdmin: token.isAdmin });
} catch (e) {
console.error('[Auth Debug] Failed to check admin status:', e);
token.isAdmin = false;
}
} else {
console.log('[Auth Debug] No access token available');
}
return token
},
async session({ session, token }) {
// Expose clubs to client
if (session.user) {
session.user.clubs = token.clubs as Record<string, string> | undefined
session.user.isAdmin = token.isAdmin as boolean | undefined
}
session.accessToken = token.accessToken as string | undefined
return session
}
}
})