From 28892a4ce4bc919619c4f5ffec62254cff58830a Mon Sep 17 00:00:00 2001 From: defiQUG Date: Thu, 26 Mar 2026 18:56:56 -0700 Subject: [PATCH] fix(portal): NextAuth redirect loop and production NEXTAUTH_URL docs - Remove pages.signIn pointed at API route; normalize redirects for LAN callbacks - signIn callbackUrl /; auth error page Try Again to / - Add .env.example; README documents public NEXTAUTH_URL (sankofa.nexus) Made-with: Cursor --- portal/.env.example | 16 ++++++++++++ portal/README.md | 5 ++-- portal/src/app/api/auth/error/page.tsx | 2 +- portal/src/app/page.tsx | 28 ++++++++++++-------- portal/src/lib/auth.ts | 36 ++++++++++++++++++++++---- 5 files changed, 68 insertions(+), 19 deletions(-) create mode 100644 portal/.env.example diff --git a/portal/.env.example b/portal/.env.example new file mode 100644 index 0000000..f7fe259 --- /dev/null +++ b/portal/.env.example @@ -0,0 +1,16 @@ +# Copy to .env.local — never commit .env.local. + +# Public origin must match the browser URL (NPM host), not the LAN upstream IP. +# Apex: https://sankofa.nexus — or use https://portal.sankofa.nexus if that is your vhost. +NEXTAUTH_URL=https://sankofa.nexus +NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32 + +KEYCLOAK_URL=https://keycloak.sankofa.nexus +KEYCLOAK_REALM=your-realm +KEYCLOAK_CLIENT_ID=portal-client +KEYCLOAK_CLIENT_SECRET=your-client-secret + +NEXT_PUBLIC_CROSSPLANE_API=https://crossplane-api.crossplane-system.svc.cluster.local +NEXT_PUBLIC_ARGOCD_URL=https://argocd.sankofa.nexus +NEXT_PUBLIC_GRAFANA_URL=https://grafana.sankofa.nexus +NEXT_PUBLIC_LOKI_URL=https://loki.monitoring.svc.cluster.local:3100 diff --git a/portal/README.md b/portal/README.md index 7bb844b..7f5ed5a 100644 --- a/portal/README.md +++ b/portal/README.md @@ -42,7 +42,7 @@ npm install ### Configuration -Copy `.env.example` to `.env.local` and configure: +Copy [`.env.example`](.env.example) to `.env.local` and configure: ```env KEYCLOAK_URL=https://keycloak.sankofa.nexus @@ -55,7 +55,8 @@ NEXT_PUBLIC_ARGOCD_URL=https://argocd.sankofa.nexus NEXT_PUBLIC_GRAFANA_URL=https://grafana.sankofa.nexus NEXT_PUBLIC_LOKI_URL=https://loki.monitoring.svc.cluster.local:3100 -NEXTAUTH_URL=https://portal.sankofa.nexus +# Must match the browser URL (NPM vhost), not the LAN upstream — e.g. https://sankofa.nexus +NEXTAUTH_URL=https://sankofa.nexus NEXTAUTH_SECRET=your-nextauth-secret ``` diff --git a/portal/src/app/api/auth/error/page.tsx b/portal/src/app/api/auth/error/page.tsx index 0415a5b..0b5fe0f 100644 --- a/portal/src/app/api/auth/error/page.tsx +++ b/portal/src/app/api/auth/error/page.tsx @@ -35,7 +35,7 @@ function AuthErrorContent() { Go Home Try Again diff --git a/portal/src/app/page.tsx b/portal/src/app/page.tsx index 4a25a82..3346357 100644 --- a/portal/src/app/page.tsx +++ b/portal/src/app/page.tsx @@ -10,9 +10,9 @@ export default function Home() { if (status === 'loading') { return ( -
+
-
+

Loading...

@@ -21,19 +21,25 @@ export default function Home() { if (status === 'unauthenticated') { return ( -
-
-

Welcome to Portal

-

Please sign in to continue

+
+
+

+ Sankofa Phoenix +

+

Welcome to Portal

+

Sign in to open Nexus Console.

-

- Development mode: Use any email/password -

+ {process.env.NODE_ENV === 'development' && ( +

+ Development: use any email/password with your dev IdP configuration. +

+ )}
); diff --git a/portal/src/lib/auth.ts b/portal/src/lib/auth.ts index f62be4f..7c95181 100644 --- a/portal/src/lib/auth.ts +++ b/portal/src/lib/auth.ts @@ -2,6 +2,24 @@ import { NextAuthOptions } from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import KeycloakProvider from 'next-auth/providers/keycloak'; +/** Prefer NEXTAUTH_URL (public origin behind NPM) so redirects match the browser host. */ +function canonicalAuthBaseUrl(fallback: string): string { + const raw = process.env.NEXTAUTH_URL?.trim(); + if (!raw) return fallback.replace(/\/$/, ''); + try { + return new URL(raw).origin; + } catch { + return fallback.replace(/\/$/, ''); + } +} + +function isPrivateOrLocalHost(hostname: string): boolean { + if (hostname === 'localhost' || hostname === '127.0.0.1') return true; + if (hostname.startsWith('192.168.')) return true; + if (hostname.startsWith('10.')) return true; + return /^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname); +} + // Check if Keycloak is configured const isKeycloakConfigured = process.env.KEYCLOAK_URL && @@ -48,10 +66,18 @@ export const authOptions: NextAuthOptions = { providers, callbacks: { async redirect({ url, baseUrl }) { - // Prevent redirect loops - only allow redirects within the same origin - if (url.startsWith('/')) return `${baseUrl}${url}`; - if (new URL(url).origin === baseUrl) return url; - return baseUrl; + const canonical = canonicalAuthBaseUrl(baseUrl); + if (url.startsWith('/')) return `${canonical}${url}`; + try { + const target = new URL(url); + if (target.origin === canonical) return url; + if (isPrivateOrLocalHost(target.hostname)) { + return `${canonical}${target.pathname}${target.search}${target.hash}`; + } + return canonical; + } catch { + return canonical; + } }, async jwt({ token, account, profile, user }) { if (account) { @@ -91,8 +117,8 @@ export const authOptions: NextAuthOptions = { return session; }, }, + // Do not set pages.signIn to /api/auth/signin — that is the API handler and causes ERR_TOO_MANY_REDIRECTS. pages: { - signIn: '/api/auth/signin', error: '/api/auth/error', }, session: {