From b47b25578f0ba187aa52355398c4d493db93b1a7 Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:44:32 +1100 Subject: [PATCH 1/2] Add Keycloak and Okta SSO configuration --- .env.example-dev | 10 ++++ deploy/docker/.env.example-prod | 6 +++ docs/installation.md | 4 +- src/app/(public-routes)/auth/signin/page.tsx | 4 ++ src/env.ts | 14 ++++++ src/features/shared/auth/sign-in-page.tsx | 53 ++++++++++++++++---- src/server/auth/config.ts | 27 +++++++++- 7 files changed, 103 insertions(+), 15 deletions(-) diff --git a/.env.example-dev b/.env.example-dev index f155419..13a1db1 100644 --- a/.env.example-dev +++ b/.env.example-dev @@ -22,6 +22,16 @@ AUTH_TRUST_HOST=true #GOOGLE_CLIENT_ID= #GOOGLE_CLIENT_SECRET= +# Optional: Keycloak OAuth (if configured via environment) +#KEYCLOAK_CLIENT_ID= +#KEYCLOAK_CLIENT_SECRET= +#KEYCLOAK_ISSUER= + +# Optional: Okta OAuth (if configured via environment) +#OKTA_CLIENT_ID= +#OKTA_CLIENT_SECRET= +#OKTA_ISSUER= + # Optional: enable demo login (default: disabled) # Set to "true" to expose a demo admin login button on the sign-in screen. # WARNING: This will allow login with to anyone who can acccess the page. diff --git a/deploy/docker/.env.example-prod b/deploy/docker/.env.example-prod index 95a6e70..15038b7 100644 --- a/deploy/docker/.env.example-prod +++ b/deploy/docker/.env.example-prod @@ -15,6 +15,12 @@ ENABLE_DEMO_MODE=false # SSO Configuration #GOOGLE_CLIENT_ID= #GOOGLE_CLIENT_SECRET= +#KEYCLOAK_CLIENT_ID= +#KEYCLOAK_CLIENT_SECRET= +#KEYCLOAK_ISSUER= +#OKTA_CLIENT_ID= +#OKTA_CLIENT_SECRET= +#OKTA_ISSUER= # Optional: logging level (default: debug in dev, info in prod) #LOG_LEVEL=info diff --git a/docs/installation.md b/docs/installation.md index 68525b9..a83030e 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -26,9 +26,9 @@ Minimum values to edit: ### Choose authentication mode -RTAP supports SSO or a demo login button. +RTAP supports SSO or a demo login button. Supported SSO providers today are Google, Keycloak, and Okta. If you need another provider, open an issue and we can add it. -- **SSO (recommended):** configure your provider's details (like Google client ID/secret) using the variable names provided in the .env file. +- **SSO (recommended):** configure your provider's details (like client ID/secret + issuer when required) using the variable names provided in the .env file. - **Demo mode:** set `ENABLE_DEMO_MODE=true`. This exposes a “Sign in as Demo Admin” button and **anyone with access to the sign-in page can log in without an account**. Use only for isolated testing or demos. For Google SSO, configure the following in the Google Cloud console: diff --git a/src/app/(public-routes)/auth/signin/page.tsx b/src/app/(public-routes)/auth/signin/page.tsx index 01b3ce1..24edb43 100644 --- a/src/app/(public-routes)/auth/signin/page.tsx +++ b/src/app/(public-routes)/auth/signin/page.tsx @@ -12,10 +12,14 @@ export default async function SignInPage(props: { searchParams?: Promise<{ callb const { callbackUrl = "/", error } = (await props.searchParams) ?? {}; const demoEnabled = env.ENABLE_DEMO_MODE === "true"; const googleEnabled = Boolean(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET); + const keycloakEnabled = Boolean(env.KEYCLOAK_CLIENT_ID && env.KEYCLOAK_CLIENT_SECRET && env.KEYCLOAK_ISSUER); + const oktaEnabled = Boolean(env.OKTA_CLIENT_ID && env.OKTA_CLIENT_SECRET && env.OKTA_ISSUER); return ( = [ + { id: "google", label: "Continue with Google" }, + { id: "keycloak", label: "Continue with Keycloak" }, + { id: "okta", label: "Continue with Okta" }, +]; + +export default function SignInPageClient({ + googleEnabled, + keycloakEnabled, + oktaEnabled, + demoEnabled, + callbackUrl, + initialError, +}: Props) { const router = useRouter(); - const [loading, setLoading] = useState<"demo" | "google" | null>(null); + const [loading, setLoading] = useState<"demo" | OAuthProviderId | null>(null); const [error, setError] = useState(initialError ?? null); const toMessage = (err?: string | null) => { @@ -45,11 +62,11 @@ export default function SignInPageClient({ googleEnabled, demoEnabled, callbackU } }; - const handleGoogle = async () => { - setLoading("google"); + const handleOAuth = async (provider: OAuthProviderId) => { + setLoading(provider); setError(null); try { - const res = await signIn("google", { callbackUrl, redirect: false }); + const res = await signIn(provider, { callbackUrl, redirect: false }); if (res?.error) { setError(toMessage(res.error)); } else if (res?.url) { @@ -60,7 +77,15 @@ export default function SignInPageClient({ googleEnabled, demoEnabled, callbackU } }; - const nothingEnabled = !googleEnabled && !demoEnabled; + const oauthEnabled: Record = { + google: googleEnabled, + keycloak: keycloakEnabled, + okta: oktaEnabled, + }; + + const enabledOauthOptions = oauthOptions.filter((option) => oauthEnabled[option.id]); + const nothingEnabled = enabledOauthOptions.length === 0 && !demoEnabled; + const showSeparator = demoEnabled && enabledOauthOptions.length > 0; return (
@@ -90,7 +115,7 @@ export default function SignInPageClient({ googleEnabled, demoEnabled, callbackU )} - {googleEnabled && demoEnabled && ( + {showSeparator && (
@@ -99,11 +124,17 @@ export default function SignInPageClient({ googleEnabled, demoEnabled, callbackU
)} - {googleEnabled && ( - - )} + ))} {nothingEnabled && (
diff --git a/src/server/auth/config.ts b/src/server/auth/config.ts index 0ba5745..834cef8 100644 --- a/src/server/auth/config.ts +++ b/src/server/auth/config.ts @@ -2,6 +2,8 @@ import { type DefaultSession, type NextAuthConfig } from "next-auth"; import type { Adapter } from "next-auth/adapters"; import type { JWT as NextAuthJWT } from "next-auth/jwt"; import GoogleProvider from "next-auth/providers/google"; +import KeycloakProvider from "next-auth/providers/keycloak"; +import OktaProvider from "next-auth/providers/okta"; import CredentialsProvider from "next-auth/providers/credentials"; import { PrismaAdapter } from "@auth/prisma-adapter"; import { type UserRole } from "@prisma/client"; @@ -40,6 +42,7 @@ declare module "@auth/core/adapters" { type AugmentedJWT = NextAuthJWT & { role?: UserRole }; const demoModeEnabled = env.ENABLE_DEMO_MODE === "true"; +const oauthProviders = new Set(["google", "keycloak", "okta"]); const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null; @@ -189,7 +192,7 @@ export const authConfig = { }), ] : []), - // Conditionally register Google provider when env credentials are available. + // Conditionally register providers when env credentials are available. // Actual enablement is enforced via DB in the signIn callback/UI. ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET ? [ @@ -202,6 +205,26 @@ export const authConfig = { }), ] : []), + ...(process.env.KEYCLOAK_CLIENT_ID && process.env.KEYCLOAK_CLIENT_SECRET && process.env.KEYCLOAK_ISSUER + ? [ + KeycloakProvider({ + clientId: process.env.KEYCLOAK_CLIENT_ID, + clientSecret: process.env.KEYCLOAK_CLIENT_SECRET, + issuer: process.env.KEYCLOAK_ISSUER, + allowDangerousEmailAccountLinking: true, + }), + ] + : []), + ...(process.env.OKTA_CLIENT_ID && process.env.OKTA_CLIENT_SECRET && process.env.OKTA_ISSUER + ? [ + OktaProvider({ + clientId: process.env.OKTA_CLIENT_ID, + clientSecret: process.env.OKTA_CLIENT_SECRET, + issuer: process.env.OKTA_ISSUER, + allowDangerousEmailAccountLinking: true, + }), + ] + : []), ], session: { strategy: "jwt", @@ -227,7 +250,7 @@ export const authConfig = { return Boolean(resolved); } - if (provider === "google") { + if (oauthProviders.has(provider)) { const emailAddr = (user as { email?: string | null } | undefined)?.email?.toLowerCase(); if (!emailAddr) return false; try { From 5ef4a43f478dfdc9c73fc09b7046c2bc8ac79356 Mon Sep 17 00:00:00 2001 From: initstring <26131150+initstring@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:35:00 +1100 Subject: [PATCH 2/2] Make env files consistent --- .env.example-dev | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.env.example-dev b/.env.example-dev index 13a1db1..a8cf9bb 100644 --- a/.env.example-dev +++ b/.env.example-dev @@ -17,17 +17,12 @@ AUTH_SECRET="REPLACE_WITH_A_SECURE_RANDOM_VALUE" AUTH_URL=http://localhost:3000 AUTH_TRUST_HOST=true -# Optional: Google OAuth (if configured via environment) -# Registers Google when provided; access is controlled by pre-provisioned users. +# SSO Configuration #GOOGLE_CLIENT_ID= #GOOGLE_CLIENT_SECRET= - -# Optional: Keycloak OAuth (if configured via environment) #KEYCLOAK_CLIENT_ID= #KEYCLOAK_CLIENT_SECRET= #KEYCLOAK_ISSUER= - -# Optional: Okta OAuth (if configured via environment) #OKTA_CLIENT_ID= #OKTA_CLIENT_SECRET= #OKTA_ISSUER=