From 1322def2ea8d27684590c4a011df93c806b49ee7 Mon Sep 17 00:00:00 2001 From: WorkClub Automation Date: Mon, 9 Mar 2026 14:21:03 +0100 Subject: [PATCH] fix(auth): resolve Keycloak OIDC issuer mismatch and API proxy routing - Bypass NextAuth OIDC discovery with explicit token/userinfo endpoints using internal Docker DNS, avoiding 'issuer string did not match' errors. - Fix next.config.ts API route interception that incorrectly forwarded NextAuth routes to backend by using 'fallback' rewrites. - Add 'Use different credentials' button to login page and AuthGuard for clearing stale sessions. --- docker-compose.yml | 2 ++ frontend/next.config.ts | 18 ++++++++++------- frontend/src/app/login/page.tsx | 28 +++++++++++++++++++++----- frontend/src/auth/auth.ts | 16 ++++++++++++++- frontend/src/components/auth-guard.tsx | 15 +++++++++++++- 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1fd21dc..dd2300a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -89,6 +89,8 @@ services: KEYCLOAK_CLIENT_ID: "workclub-app" KEYCLOAK_CLIENT_SECRET: "dev-secret-workclub-api-change-in-production" KEYCLOAK_ISSUER: "http://localhost:8080/realms/workclub" + KEYCLOAK_ISSUER_INTERNAL: "http://keycloak:8080/realms/workclub" + NEXT_PUBLIC_KEYCLOAK_ISSUER: "http://localhost:8080/realms/workclub" ports: - "3000:3000" volumes: diff --git a/frontend/next.config.ts b/frontend/next.config.ts index cfec0b9..e34d821 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -3,13 +3,17 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: 'standalone', async rewrites() { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5001'; - return [ - { - source: '/api/:path*', - destination: `${apiUrl}/api/:path*`, - }, - ]; + const apiUrl = process.env.API_INTERNAL_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5001'; + return { + beforeFiles: [], + afterFiles: [], + fallback: [ + { + source: '/api/:path*', + destination: `${apiUrl}/api/:path*`, + }, + ], + }; }, }; diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 327b66f..2c91592 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -1,16 +1,17 @@ 'use client'; import { useEffect } from 'react'; -import { signIn, useSession } from 'next-auth/react'; -import { useRouter } from 'next/navigation'; -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { signIn, signOut, useSession } from 'next-auth/react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; export default function LoginPage() { const { status } = useSession(); const router = useRouter(); + const searchParams = useSearchParams(); + const hasError = searchParams.get('error') || searchParams.get('callbackUrl'); - // Redirect to dashboard if already authenticated useEffect(() => { if (status === 'authenticated') { router.push('/dashboard'); @@ -21,17 +22,34 @@ export default function LoginPage() { signIn('keycloak', { callbackUrl: '/dashboard' }); }; + const handleSwitchAccount = () => { + const keycloakLogoutUrl = `${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER || 'http://localhost:8080/realms/workclub'}/protocol/openid-connect/logout?redirect_uri=${encodeURIComponent(window.location.origin + '/login')}`; + signOut({ redirect: false }).then(() => { + window.location.href = keycloakLogoutUrl; + }); + }; + return (
WorkClub Manager - + + + {hasError && ( + +

+ Having trouble? Try "Use different credentials" to clear your session. +

+
+ )}
); diff --git a/frontend/src/auth/auth.ts b/frontend/src/auth/auth.ts index b0f6926..e09769a 100644 --- a/frontend/src/auth/auth.ts +++ b/frontend/src/auth/auth.ts @@ -19,12 +19,26 @@ declare module "next-auth" { } } +// 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! +const issuerInternal = process.env.KEYCLOAK_ISSUER_INTERNAL || issuerPublic +const oidcPublic = `${issuerPublic}/protocol/openid-connect` +const oidcInternal = `${issuerInternal}/protocol/openid-connect` + export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [ KeycloakProvider({ clientId: process.env.KEYCLOAK_CLIENT_ID!, clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!, - issuer: process.env.KEYCLOAK_ISSUER!, + issuer: issuerPublic, + authorization: { + url: `${oidcPublic}/auth`, + params: { scope: "openid email profile" }, + }, + token: `${oidcInternal}/token`, + userinfo: `${oidcInternal}/userinfo`, }) ], callbacks: { diff --git a/frontend/src/components/auth-guard.tsx b/frontend/src/components/auth-guard.tsx index f1bf441..dae05b3 100644 --- a/frontend/src/components/auth-guard.tsx +++ b/frontend/src/components/auth-guard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useSession } from 'next-auth/react'; +import { useSession, signOut } from 'next-auth/react'; import { useRouter } from 'next/navigation'; import { ReactNode, useEffect } from 'react'; import { useTenant } from '../contexts/tenant-context'; @@ -47,10 +47,23 @@ export function AuthGuard({ children }: { children: ReactNode }) { } if (clubs.length === 0 && status === 'authenticated') { + const handleSwitchAccount = () => { + const keycloakLogoutUrl = `${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER || 'http://localhost:8080/realms/workclub'}/protocol/openid-connect/logout?redirect_uri=${encodeURIComponent(window.location.origin + '/login')}`; + signOut({ redirect: false }).then(() => { + window.location.href = keycloakLogoutUrl; + }); + }; + return (

No Clubs Found

Contact admin to get access to a club

+
); }