From 30ae230b23bb4b80b99f14b6e1193d9ce4229b0e Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Thu, 13 Nov 2025 18:02:25 +0100 Subject: [PATCH 01/14] chore: set up auth instance --- lib/auth.ts | 23 ++++++++ package.json | 1 + pnpm-lock.yaml | 153 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 lib/auth.ts diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..0cb7a01 --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,23 @@ +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + // No database configuration - running in stateless mode + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + cookieCache: { + enabled: true, + maxAge: 30 * 24 * 60 * 60, // 30 days cache duration + strategy: "jwe", // Use encrypted tokens for better security + refreshCache: true, // Enable stateless refresh + }, + }, + socialProviders: { + // Configure your OIDC provider here + // Replace the env vars in .env.local with your actual OIDC provider details + oidc: { + clientId: process.env.OIDC_CLIENT_ID || "", + clientSecret: process.env.OIDC_CLIENT_SECRET || "", + issuer: process.env.OIDC_ISSUER_URL || "", // Your OIDC provider's issuer URL + }, + }, +}); diff --git a/package.json b/package.json index b51ed37..7f77007 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "better-auth": "1.4.0-beta.20", "next": "16.0.2", "react": "19.2.0", "react-dom": "19.2.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 835cadf..91c6ca0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + better-auth: + specifier: 1.4.0-beta.20 + version: 1.4.0-beta.20(next@16.0.2(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) next: specifier: 16.0.2 version: 16.0.2(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -166,6 +169,27 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@better-auth/core@1.4.0-beta.20': + resolution: {integrity: sha512-0LO/j7Rur5vRVF8ELkXd4Ko8ewh+ONhzWVQqHgzOfmXCFODFjtwrFNRN0cinDFnyauCzJ4ADMc+lWqrZUc8ktQ==} + peerDependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + better-call: 1.0.26 + jose: ^6.1.0 + kysely: ^0.28.5 + nanostores: ^1.0.1 + + '@better-auth/telemetry@1.4.0-beta.20': + resolution: {integrity: sha512-1HyiSKDoCp/KyO4C9P2mVXDGjeEHch1z1CfhDh+et17LtDLFbiCZZvnP8dbXYhx+LGO0MTUhfn8f4zIE4UT7Gw==} + peerDependencies: + '@better-auth/core': 1.4.0-beta.20 + + '@better-auth/utils@0.3.0': + resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} + + '@better-fetch/fetch@1.1.18': + resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} + '@biomejs/biome@2.3.5': resolution: {integrity: sha512-HvLhNlIlBIbAV77VysRIBEwp55oM/QAjQEin74QQX9Xb259/XP/D5AGGnZMOyF1el4zcvlNYYR3AyTMUV3ILhg==} engines: {node: '>=14.21.3'} @@ -614,6 +638,14 @@ packages: cpu: [x64] os: [win32] + '@noble/ciphers@2.0.1': + resolution: {integrity: sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@rolldown/pluginutils@1.0.0-beta.47': resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} @@ -936,6 +968,38 @@ packages: resolution: {integrity: sha512-73lC1ugzwoaWCLJ1LvOgrR5xsMLTqSKIEoMHVtL9E/HNk0PXtTM76ZIm84856/SF7Nv8mPZxKoBsgpm0tR1u1Q==} hasBin: true + better-auth@1.4.0-beta.20: + resolution: {integrity: sha512-fvwCl998QlcY1jjs1aFk1g7S2msSAeyz8ahyb+vMz+3V7RFc1t1n2A5I16D9C4m0/QvjDzMVj+0vFI8auNXMZA==} + peerDependencies: + '@lynx-js/react': '*' + '@sveltejs/kit': '*' + next: '*' + react: '*' + react-dom: '*' + solid-js: '*' + svelte: '*' + vue: '*' + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@sveltejs/kit': + optional: true + next: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vue: + optional: true + + better-call@1.0.26: + resolution: {integrity: sha512-/5AaTPC8IRXV5yWxpAI7eR2RNiGHq4q/mjBm9DiikIkmJhzuqO1Ub66oYYqJ3eBFF+6BfdtZFRnuW0me8T0Emg==} + bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} @@ -984,6 +1048,9 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1072,6 +1139,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.1.1: + resolution: {integrity: sha512-GWSqjfOPf4cWOkBzw5THBjtGPhXKqYnfRBzh4Ni+ArTrQQ9unvmsA3oFLqaYKoKe5sjWmGu5wVKg9Ft1i+LQfg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1094,6 +1164,10 @@ packages: engines: {node: '>=6'} hasBin: true + kysely@0.28.8: + resolution: {integrity: sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==} + engines: {node: '>=20.0.0'} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -1189,6 +1263,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanostores@1.0.1: + resolution: {integrity: sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==} + engines: {node: ^20.0.0 || >=22.0.0} + next@16.0.2: resolution: {integrity: sha512-zL8+UBf+xUIm8zF0vYGJYJMYDqwaBrRRe7S0Kob6zo9Kf+BdqFLEECMI+B6cNIcoQ+el9XM2fvUExwhdDnXjtw==} engines: {node: '>=20.9.0'} @@ -1267,6 +1345,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rou3@0.5.1: + resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1286,6 +1367,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1511,6 +1595,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + snapshots: '@acemir/cssom@0.9.23': {} @@ -1649,6 +1736,27 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@better-auth/core@1.4.0-beta.20(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.26)(jose@6.1.1)(kysely@0.28.8)(nanostores@1.0.1)': + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + '@standard-schema/spec': 1.0.0 + better-call: 1.0.26 + jose: 6.1.1 + kysely: 0.28.8 + nanostores: 1.0.1 + zod: 4.1.12 + + '@better-auth/telemetry@1.4.0-beta.20(@better-auth/core@1.4.0-beta.20(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.26)(jose@6.1.1)(kysely@0.28.8)(nanostores@1.0.1))': + dependencies: + '@better-auth/core': 1.4.0-beta.20(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.26)(jose@6.1.1)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + + '@better-auth/utils@0.3.0': {} + + '@better-fetch/fetch@1.1.18': {} + '@biomejs/biome@2.3.5': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.3.5 @@ -1931,6 +2039,10 @@ snapshots: '@next/swc-win32-x64-msvc@16.0.2': optional: true + '@noble/ciphers@2.0.1': {} + + '@noble/hashes@2.0.1': {} + '@rolldown/pluginutils@1.0.0-beta.47': {} '@rollup/rollup-android-arm-eabi@4.53.2': @@ -2208,6 +2320,33 @@ snapshots: baseline-browser-mapping@2.8.26: {} + better-auth@1.4.0-beta.20(next@16.0.2(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + '@better-auth/core': 1.4.0-beta.20(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.26)(jose@6.1.1)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/telemetry': 1.4.0-beta.20(@better-auth/core@1.4.0-beta.20(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.26)(jose@6.1.1)(kysely@0.28.8)(nanostores@1.0.1)) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + '@noble/ciphers': 2.0.1 + '@noble/hashes': 2.0.1 + '@standard-schema/spec': 1.0.0 + better-call: 1.0.26 + defu: 6.1.4 + jose: 6.1.1 + kysely: 0.28.8 + nanostores: 1.0.1 + zod: 4.1.12 + optionalDependencies: + next: 16.0.2(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + better-call@1.0.26: + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + rou3: 0.5.1 + set-cookie-parser: 2.7.2 + bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 @@ -2252,6 +2391,8 @@ snapshots: decimal.js@10.6.0: {} + defu@6.1.4: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -2345,6 +2486,8 @@ snapshots: jiti@2.6.1: {} + jose@6.1.1: {} + js-tokens@4.0.0: {} jsdom@27.2.0: @@ -2378,6 +2521,8 @@ snapshots: json5@2.2.3: {} + kysely@0.28.8: {} + lightningcss-android-arm64@1.30.2: optional: true @@ -2445,6 +2590,8 @@ snapshots: nanoid@3.3.11: {} + nanostores@1.0.1: {} + next@16.0.2(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@next/env': 16.0.2 @@ -2542,6 +2689,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.2 fsevents: 2.3.3 + rou3@0.5.1: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -2555,6 +2704,8 @@ snapshots: semver@7.7.3: optional: true + set-cookie-parser@2.7.2: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -2742,3 +2893,5 @@ snapshots: xmlchars@2.2.0: {} yallist@3.1.1: {} + + zod@4.1.12: {} From 316204ea1d8e67948ef34c73bcf32d81fe1cacbb Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Thu, 13 Nov 2025 18:15:18 +0100 Subject: [PATCH 02/14] add auth api --- src/app/api/auth/[...all]/route.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/app/api/auth/[...all]/route.ts diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..e11351a --- /dev/null +++ b/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@/lib/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { GET, POST } = toNextJsHandler(auth.handler); From 0fd48fda76936e7cd76b37c2d0bedabfe931382a Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Thu, 13 Nov 2025 18:16:36 +0100 Subject: [PATCH 03/14] add auth client --- lib/auth-client.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 lib/auth-client.ts diff --git a/lib/auth-client.ts b/lib/auth-client.ts new file mode 100644 index 0000000..e1d5a2f --- /dev/null +++ b/lib/auth-client.ts @@ -0,0 +1,8 @@ +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:3000", +}); + +// You can also export specific methods if you prefer +export const { signIn, signOut, useSession } = authClient; From b570c0594b41b87dbd2b5b395ed48660825f406e Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Thu, 13 Nov 2025 18:22:20 +0100 Subject: [PATCH 04/14] add authenticated pages --- src/app/dashboard/page.tsx | 50 ++++++++++++++++++ src/app/login/page.tsx | 34 ++++++++++++ src/app/page.tsx | 105 +++++++++++++++---------------------- 3 files changed, 127 insertions(+), 62 deletions(-) create mode 100644 src/app/dashboard/page.tsx create mode 100644 src/app/login/page.tsx diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..7efb198 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,50 @@ +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; + +export default async function DashboardPage() { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session) { + redirect("/login"); + } + + return ( +
+
+

+ Hello World! πŸŽ‰ +

+ +
+

+ You are successfully authenticated! +

+ +
+

+ User Info: +

+

+ Email: {session.user.email} +

+

+ User ID: {session.user.id} +

+
+
+ +
+ +
+
+
+ ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..81753ef --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { authClient } from "@/lib/auth-client"; + +export default function LoginPage() { + const handleOIDCLogin = async () => { + await authClient.signIn.social({ + provider: "oidc", + callbackURL: "/dashboard", + }); + }; + + return ( +
+
+

+ Log In +

+ +

+ Sign in with your OIDC provider to continue +

+ + +
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 295f8fd..d9be00c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,46 @@ -import Image from "next/image"; +"use client"; + +import { useSession } from "@/lib/auth-client"; +import Link from "next/link"; export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
- ); + const { data: session, isPending } = useSession(); + + return ( +
+
+

+ Welcome to ToolHive Cloud UI +

+ + {isPending ? ( +

Loading...

+ ) : session ? ( +
+

+ You are logged in as {session.user.email} +

+ + Go to Dashboard + +
+ ) : ( +
+

+ Please log in to access the application +

+ + Log In + +
+ )} +
+
+ ); } From 82302dc39a2d1538f86ac3cadbc1133c194b7b50 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Thu, 13 Nov 2025 18:56:06 +0100 Subject: [PATCH 05/14] add oidc provider --- dev/oidc-provider.mjs | 126 +++++++++++++++ package.json | 4 +- pnpm-lock.yaml | 366 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 495 insertions(+), 1 deletion(-) create mode 100644 dev/oidc-provider.mjs diff --git a/dev/oidc-provider.mjs b/dev/oidc-provider.mjs new file mode 100644 index 0000000..5bdfdbe --- /dev/null +++ b/dev/oidc-provider.mjs @@ -0,0 +1,126 @@ +import Provider from "oidc-provider"; + +const ISSUER = "http://localhost:4000"; +const PORT = 4000; + +// Simple in-memory account storage +const accounts = { + "test-user": { + accountId: "test-user", + email: "test@example.com", + email_verified: true, + name: "Test User", + }, +}; + +// Configuration +const configuration = { + clients: [ + { + client_id: "better-auth-dev", + client_secret: "dev-secret-change-in-production", + redirect_uris: ["http://localhost:3000/api/auth/callback/oidc"], + response_types: ["code"], + grant_types: ["authorization_code", "refresh_token"], + token_endpoint_auth_method: "client_secret_post", + }, + ], + cookies: { + keys: ["some-secret-key-for-dev"], + }, + findAccount: async (ctx, id) => { + const account = accounts[id]; + if (!account) return undefined; + + return { + accountId: id, + async claims() { + return { + sub: id, + email: account.email, + email_verified: account.email_verified, + name: account.name, + }; + }, + }; + }, + // Simple interaction - auto-login for dev + interactions: { + url(ctx, interaction) { + return `/interaction/${interaction.uid}`; + }, + }, + features: { + devInteractions: { enabled: true }, // Enable dev interactions for easy testing + refreshToken: { enabled: true }, + }, + ttl: { + AccessToken: 3600, // 1 hour + RefreshToken: 86400 * 30, // 30 days + }, +}; + +const oidc = new Provider(ISSUER, configuration); + +// Simple interaction endpoint for dev - auto-login as test-user +oidc.use(async (ctx, next) => { + if (ctx.path.startsWith("/interaction/")) { + const uid = ctx.path.split("/")[2]; + const interaction = await oidc.interactionDetails(ctx.req, ctx.res); + + if (interaction.prompt.name === "login") { + // Auto-login as test-user for dev + await oidc.interactionFinished( + ctx.req, + ctx.res, + { + login: { + accountId: "test-user", + }, + }, + { mergeWithLastSubmission: false }, + ); + return; + } + + if (interaction.prompt.name === "consent") { + // Auto-consent for dev + const grant = new oidc.Grant({ + accountId: interaction.session.accountId, + clientId: interaction.params.client_id, + }); + + grant.addOIDCScope( + interaction.params.scope + ?.split(" ") + .filter((scope) => ["openid", "email", "profile"].includes(scope)) + .join(" ") || "openid email profile", + ); + + await grant.save(); + + await oidc.interactionFinished( + ctx.req, + ctx.res, + { + consent: { + grantId: grant.jti, + }, + }, + { mergeWithLastSubmission: true }, + ); + return; + } + } + await next(); +}); + +oidc.listen(PORT, () => { + console.log(`πŸ” OIDC Provider running at ${ISSUER}`); + console.log(`πŸ“ Client ID: better-auth-dev`); + console.log(`πŸ”‘ Client Secret: dev-secret-change-in-production`); + console.log(`πŸ‘€ Test user: test@example.com`); + console.log( + `\nβš™οΈ Update your .env.local with:\nOIDC_CLIENT_ID=better-auth-dev\nOIDC_CLIENT_SECRET=dev-secret-change-in-production\nOIDC_ISSUER_URL=${ISSUER}`, + ); +}); diff --git a/package.json b/package.json index 8f8d0a4..ea5aba0 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "format": "biome format --write", "test": "vitest", "type-check": "tsc --noEmit", - "prepare": "husky" + "prepare": "husky", + "oidc": "node dev/oidc-provider.mjs" }, "dependencies": { "better-auth": "1.4.0-beta.20", @@ -32,6 +33,7 @@ "husky": "^9.1.7", "jsdom": "^27.2.0", "lint-staged": "^16.0.0", + "oidc-provider": "^9.5.2", "tailwindcss": "^4", "typescript": "^5", "vite-tsconfig-paths": "^5.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a91e29..2861250 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: lint-staged: specifier: ^16.0.0 version: 16.2.6 + oidc-provider: + specifier: ^9.5.2 + version: 9.5.2 tailwindcss: specifier: ^4 version: 4.1.17 @@ -593,6 +596,14 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@koa/cors@5.0.0': + resolution: {integrity: sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==} + engines: {node: '>= 14.0.0'} + + '@koa/router@14.0.0': + resolution: {integrity: sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==} + engines: {node: '>= 20'} + '@next/env@16.0.3': resolution: {integrity: sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ==} @@ -948,6 +959,10 @@ packages: '@vitest/utils@4.0.8': resolution: {integrity: sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -1030,6 +1045,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + caniuse-lite@1.0.30001754: resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} @@ -1055,9 +1074,21 @@ packages: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookies@0.9.1: + resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} + engines: {node: '>= 0.8'} + css-tree@3.1.0: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -1085,13 +1116,31 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-equal@1.0.1: + resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} + defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1099,12 +1148,19 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.250: resolution: {integrity: sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -1129,9 +1185,16 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eta@4.4.1: + resolution: {integrity: sha512-4o6fYxhRmFmO9SJcU9PxBLYPGapvJ/Qha0ZE+Y6UE9QIUd0Wk1qaLISQ6J1bM7nOcWHhs1YmY3mfrfwkJRBTWQ==} + engines: {node: '>=20'} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -1152,6 +1215,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1175,6 +1242,18 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + http-assert@1.5.0: + resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} + engines: {node: '>= 0.8'} + + http-errors@1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1192,6 +1271,13 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + is-fullwidth-code-point@5.1.0: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} @@ -1232,6 +1318,18 @@ packages: engines: {node: '>=6'} hasBin: true + keygrip@1.1.0: + resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} + engines: {node: '>= 0.6'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + koa-compose@4.1.0: + resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + + koa@3.1.1: + resolution: {integrity: sha512-KDDuvpfqSK0ZKEO2gCPedNjl5wYpfj+HNiuVRlbhd1A88S3M0ySkdf2V/EJ4NWt5dwh5PXCdcenrKK2IQJAxsg==} + engines: {node: '>= 18'} + kysely@0.28.8: resolution: {integrity: sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==} engines: {node: '>=20.0.0'} @@ -1336,10 +1434,30 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -1356,10 +1474,19 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + nanostores@1.0.1: resolution: {integrity: sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==} engines: {node: ^20.0.0 || >=22.0.0} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + next@16.0.3: resolution: {integrity: sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w==} engines: {node: '>=20.9.0'} @@ -1384,6 +1511,13 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + oidc-provider@9.5.2: + resolution: {integrity: sha512-lTI6U7ESvf34xuu9XPUfJX6sbIXuOsV2MUPT9YoT7dzDtajufc50iWSY2t/4xB/TuFQ4t0Q++JU2Cu270d11pg==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -1391,6 +1525,13 @@ packages: parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1426,6 +1567,14 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + quick-lru@7.3.0: + resolution: {integrity: sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==} + engines: {node: '>=18'} + + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + react-dom@19.2.0: resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} peerDependencies: @@ -1461,6 +1610,9 @@ packages: rou3@0.5.1: resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1483,6 +1635,9 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1505,6 +1660,18 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -1572,6 +1739,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -1593,6 +1764,14 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1601,12 +1780,20 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.1.4: resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: @@ -2163,6 +2350,19 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@koa/cors@5.0.0': + dependencies: + vary: 1.1.2 + + '@koa/router@14.0.0': + dependencies: + debug: 4.4.3 + http-errors: 2.0.0 + koa-compose: 4.1.0 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + '@next/env@16.0.3': {} '@next/swc-darwin-arm64@16.0.3': @@ -2452,6 +2652,11 @@ snapshots: '@vitest/pretty-format': 4.0.8 tinyrainbow: 3.0.3 + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + agent-base@7.1.4: {} ansi-escapes@7.2.0: @@ -2521,6 +2726,8 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.28.0) + bytes@3.1.2: {} + caniuse-lite@1.0.30001754: {} chai@6.2.1: {} @@ -2540,8 +2747,19 @@ snapshots: commander@14.0.2: {} + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookies@0.9.1: + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 + css-tree@3.1.0: dependencies: mdn-data: 2.12.2 @@ -2566,18 +2784,32 @@ snapshots: decimal.js@10.6.0: {} + deep-equal@1.0.1: {} + defu@6.1.4: {} + delegates@1.0.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} + detect-libc@2.1.2: {} dom-accessibility-api@0.5.16: {} + ee-first@1.1.1: {} + electron-to-chromium@1.5.250: {} emoji-regex@10.6.0: {} + encodeurl@2.0.0: {} + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -2620,10 +2852,14 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + eta@4.4.1: {} + eventemitter3@5.0.1: {} expect-type@1.2.2: {} @@ -2636,6 +2872,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + fresh@0.5.2: {} + fsevents@2.3.3: optional: true @@ -2651,6 +2889,27 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + http-assert@1.5.0: + dependencies: + deep-equal: 1.0.1 + http-errors: 1.8.1 + + http-errors@1.8.1: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -2671,6 +2930,12 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + is-fullwidth-code-point@5.1.0: dependencies: get-east-asian-width: 1.4.0 @@ -2716,6 +2981,33 @@ snapshots: json5@2.2.3: {} + keygrip@1.1.0: + dependencies: + tsscmp: 1.0.6 + + koa-compose@4.1.0: {} + + koa@3.1.1: + dependencies: + accepts: 1.3.8 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookies: 0.9.1 + delegates: 1.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 2.0.0 + koa-compose: 4.1.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + kysely@0.28.8: {} lightningcss-android-arm64@1.30.2: @@ -2808,11 +3100,25 @@ snapshots: mdn-data@2.12.2: {} + media-typer@1.1.0: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + mimic-function@5.0.1: {} ms@2.1.3: {} @@ -2821,8 +3127,12 @@ snapshots: nanoid@3.3.11: {} + nanoid@5.1.6: {} + nanostores@1.0.1: {} + negotiator@0.6.3: {} + next@16.0.3(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@next/env': 16.0.3 @@ -2849,6 +3159,25 @@ snapshots: node-releases@2.0.27: {} + oidc-provider@9.5.2: + dependencies: + '@koa/cors': 5.0.0 + '@koa/router': 14.0.0 + debug: 4.4.3 + eta: 4.4.1 + jose: 6.1.1 + jsesc: 3.1.0 + koa: 3.1.1 + nanoid: 5.1.6 + quick-lru: 7.3.0 + raw-body: 3.0.1 + transitivePeerDependencies: + - supports-color + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -2857,6 +3186,10 @@ snapshots: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -2887,6 +3220,15 @@ snapshots: punycode@2.3.1: {} + quick-lru@7.3.0: {} + + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + react-dom@19.2.0(react@19.2.0): dependencies: react: 19.2.0 @@ -2937,6 +3279,8 @@ snapshots: rou3@0.5.1: {} + safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -2952,6 +3296,8 @@ snapshots: set-cookie-parser@2.7.2: {} + setprototypeof@1.2.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -2997,6 +3343,12 @@ snapshots: stackback@0.0.2: {} + statuses@1.5.0: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + std-env@3.10.0: {} string-argv@0.3.2: {} @@ -3050,6 +3402,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tough-cookie@6.0.0: dependencies: tldts: 7.0.17 @@ -3064,16 +3418,28 @@ snapshots: tslib@2.8.1: {} + tsscmp@1.0.6: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + typescript@5.9.3: {} undici-types@7.16.0: {} + unpipe@1.0.0: {} + update-browserslist-db@1.1.4(browserslist@4.28.0): dependencies: browserslist: 4.28.0 escalade: 3.2.0 picocolors: 1.1.1 + vary@1.1.2: {} + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)): dependencies: debug: 4.4.3 From efa1b8bf8ebd51aeb64044a57c18d6fb05c26244 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 14 Nov 2025 10:45:18 +0100 Subject: [PATCH 06/14] . --- OIDC_SETUP.md | 81 +++++++++++++++++++ dev/oidc-provider.mjs | 9 ++- lib/auth-client.ts | 8 -- lib/auth.ts | 23 ------ package.json | 4 +- pnpm-lock.yaml | 175 +++++++++++++++++++++++++++++++++++++++++ src/app/login/page.tsx | 67 ++++++++++------ src/app/page.tsx | 79 ++++++++++--------- src/lib/auth-client.ts | 11 +++ src/lib/auth.ts | 57 ++++++++++++++ 10 files changed, 417 insertions(+), 97 deletions(-) create mode 100644 OIDC_SETUP.md delete mode 100644 lib/auth-client.ts delete mode 100644 lib/auth.ts create mode 100644 src/lib/auth-client.ts create mode 100644 src/lib/auth.ts diff --git a/OIDC_SETUP.md b/OIDC_SETUP.md new file mode 100644 index 0000000..2b97439 --- /dev/null +++ b/OIDC_SETUP.md @@ -0,0 +1,81 @@ +# OIDC Authentication Setup + +This application supports authentication with any OIDC-compliant identity provider (Okta, Keycloak, Auth0, etc.). + +## Configuration + +1. **Configure your OIDC provider** with these settings: + - **Callback URL**: `http://localhost:3000/api/auth/oauth2/callback/oidc` (adjust domain/port for production) + - **Grant Types**: Authorization Code, Refresh Token + - **Response Type**: code + - **Scopes**: openid, email, profile + +2. **Set environment variables** in `.env.local`: + +```bash +# Better Auth Configuration +BETTER_AUTH_SECRET=your-random-secret-here +BETTER_AUTH_URL=http://localhost:3000 + +# OIDC Provider Configuration +OIDC_CLIENT_ID=your-client-id +OIDC_CLIENT_SECRET=your-client-secret +OIDC_ISSUER_URL=https://your-oidc-provider.com +``` + +## Local Development + +For local testing, this repo includes a test OIDC provider: + +1. **Start the OIDC provider**: + ```bash + pnpm oidc + ``` + +2. **Use these credentials** in `.env.local`: + ```bash + OIDC_CLIENT_ID=better-auth-dev + OIDC_CLIENT_SECRET=dev-secret-change-in-production + OIDC_ISSUER_URL=http://localhost:4000 + ``` + +3. **Run the app**: + ```bash + pnpm dev + ``` + + Or run both concurrently: + ```bash + pnpm dev + ``` + +The test provider automatically logs in as `test@example.com`. + +## Provider-Specific Guides + +### Okta + +1. Create a new App Integration (OIDC - Web Application) +2. Set Callback URL: `http://localhost:3000/api/auth/oauth2/callback/oidc` +3. Use Okta domain as OIDC_ISSUER_URL (e.g., `https://dev-12345.okta.com`) + +### Keycloak + +1. Create a new Client +2. Set Access Type: confidential +3. Set Valid Redirect URIs: `http://localhost:3000/api/auth/oauth2/callback/oidc` +4. Use Realm URL as OIDC_ISSUER_URL (e.g., `https://keycloak.example.com/realms/myrealm`) + +### Auth0 + +1. Create a Regular Web Application +2. Set Allowed Callback URLs: `http://localhost:3000/api/auth/oauth2/callback/oidc` +3. Use Auth0 domain as OIDC_ISSUER_URL (e.g., `https://your-tenant.auth0.com`) + +## Architecture + +This application uses **stateless authentication**: +- No database required +- Session data stored in encrypted JWE cookies +- 7-day session expiration with 30-day refresh window +- Works with any OIDC-compliant provider diff --git a/dev/oidc-provider.mjs b/dev/oidc-provider.mjs index 5bdfdbe..8c08be1 100644 --- a/dev/oidc-provider.mjs +++ b/dev/oidc-provider.mjs @@ -19,7 +19,13 @@ const configuration = { { client_id: "better-auth-dev", client_secret: "dev-secret-change-in-production", - redirect_uris: ["http://localhost:3000/api/auth/callback/oidc"], + redirect_uris: [ + // Better Auth genericOAuth uses /oauth2/callback/:providerId + "http://localhost:3000/api/auth/oauth2/callback/oidc", + "http://localhost:3001/api/auth/oauth2/callback/oidc", + "http://localhost:3002/api/auth/oauth2/callback/oidc", + "http://localhost:3003/api/auth/oauth2/callback/oidc", + ], response_types: ["code"], grant_types: ["authorization_code", "refresh_token"], token_endpoint_auth_method: "client_secret_post", @@ -52,7 +58,6 @@ const configuration = { }, features: { devInteractions: { enabled: true }, // Enable dev interactions for easy testing - refreshToken: { enabled: true }, }, ttl: { AccessToken: 3600, // 1 hour diff --git a/lib/auth-client.ts b/lib/auth-client.ts deleted file mode 100644 index e1d5a2f..0000000 --- a/lib/auth-client.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createAuthClient } from "better-auth/react"; - -export const authClient = createAuthClient({ - baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:3000", -}); - -// You can also export specific methods if you prefer -export const { signIn, signOut, useSession } = authClient; diff --git a/lib/auth.ts b/lib/auth.ts deleted file mode 100644 index 0cb7a01..0000000 --- a/lib/auth.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { betterAuth } from "better-auth"; - -export const auth = betterAuth({ - // No database configuration - running in stateless mode - session: { - expiresIn: 60 * 60 * 24 * 7, // 7 days - cookieCache: { - enabled: true, - maxAge: 30 * 24 * 60 * 60, // 30 days cache duration - strategy: "jwe", // Use encrypted tokens for better security - refreshCache: true, // Enable stateless refresh - }, - }, - socialProviders: { - // Configure your OIDC provider here - // Replace the env vars in .env.local with your actual OIDC provider details - oidc: { - clientId: process.env.OIDC_CLIENT_ID || "", - clientSecret: process.env.OIDC_CLIENT_SECRET || "", - issuer: process.env.OIDC_ISSUER_URL || "", // Your OIDC provider's issuer URL - }, - }, -}); diff --git a/package.json b/package.json index ea5aba0..3eeff9b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "private": true, "packageManager": "pnpm@10.22.0", "scripts": { - "dev": "next dev", + "dev": "concurrently -n \"OIDC,Next\" -c \"blue,green\" \"pnpm oidc\" \"pnpm dev:next\"", + "dev:next": "next dev", "build": "next build", "start": "next start", "lint": "biome check", @@ -30,6 +31,7 @@ "@types/react-dom": "^19", "@vitejs/plugin-react": "^5.1.1", "babel-plugin-react-compiler": "1.0.0", + "concurrently": "^9.2.1", "husky": "^9.1.7", "jsdom": "^27.2.0", "lint-staged": "^16.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2861250..2907423 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 + concurrently: + specifier: ^9.2.1 + version: 9.2.1 husky: specifier: ^9.1.7 version: 9.1.7 @@ -979,6 +982,10 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} @@ -1056,6 +1063,10 @@ packages: resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} engines: {node: '>=18'} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -1067,6 +1078,17 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -1074,6 +1096,11 @@ packages: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -1157,6 +1184,9 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -1228,6 +1258,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.4.0: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} @@ -1238,6 +1272,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -1278,6 +1316,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-fullwidth-code-point@5.1.0: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} @@ -1591,6 +1633,10 @@ packages: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1610,6 +1656,9 @@ packages: rou3@0.5.1: resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -1642,6 +1691,10 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1679,6 +1732,10 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -1687,6 +1744,10 @@ packages: resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} engines: {node: '>=20'} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-ansi@7.1.2: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} @@ -1704,6 +1765,14 @@ packages: babel-plugin-macros: optional: true + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -1751,6 +1820,10 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} @@ -1901,6 +1974,10 @@ packages: engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -1924,6 +2001,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1932,6 +2013,14 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + zod@4.1.12: resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} @@ -2667,6 +2756,10 @@ snapshots: ansi-regex@6.2.2: {} + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansi-styles@5.2.0: {} ansi-styles@6.2.3: {} @@ -2732,6 +2825,11 @@ snapshots: chai@6.2.1: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -2743,10 +2841,31 @@ snapshots: client-only@0.0.1: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + colorette@2.0.20: {} commander@14.0.2: {} + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -2808,6 +2927,8 @@ snapshots: emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} + encodeurl@2.0.0: {} enhanced-resolve@5.18.3: @@ -2879,12 +3000,16 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.4.0: {} globrex@0.1.2: {} graceful-fs@4.2.11: {} + has-flag@4.0.0: {} + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -2936,6 +3061,8 @@ snapshots: inherits@2.0.4: {} + is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@5.1.0: dependencies: get-east-asian-width: 1.4.0 @@ -3240,6 +3367,8 @@ snapshots: react@19.2.0: {} + require-directory@2.1.1: {} + require-from-string@2.0.2: {} restore-cursor@5.1.0: @@ -3279,6 +3408,10 @@ snapshots: rou3@0.5.1: {} + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} @@ -3330,6 +3463,8 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true + shell-quote@1.8.3: {} + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -3353,6 +3488,12 @@ snapshots: string-argv@0.3.2: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -3364,6 +3505,10 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-ansi@7.1.2: dependencies: ansi-regex: 6.2.2 @@ -3375,6 +3520,14 @@ snapshots: optionalDependencies: '@babel/core': 7.28.5 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + symbol-tree@3.2.4: {} tailwindcss@4.1.17: {} @@ -3412,6 +3565,8 @@ snapshots: dependencies: punycode: 2.3.1 + tree-kill@1.2.2: {} + tsconfck@3.1.6(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -3527,6 +3682,12 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -3539,8 +3700,22 @@ snapshots: xmlchars@2.2.0: {} + y18n@5.0.8: {} + yallist@3.1.1: {} yaml@2.8.1: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + zod@4.1.12: {} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 81753ef..19dfc6d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -3,32 +3,49 @@ import { authClient } from "@/lib/auth-client"; export default function LoginPage() { - const handleOIDCLogin = async () => { - await authClient.signIn.social({ - provider: "oidc", - callbackURL: "/dashboard", - }); - }; + const handleOIDCLogin = async () => { + try { + console.log("Initiating OIDC sign-in..."); + const { data, error } = await authClient.signIn.oauth2({ + providerId: "oidc", + callbackURL: "/dashboard", + }); - return ( -
-
-

- Log In -

+ if (error) { + console.error("Sign-in error from Better Auth:", error); + console.error("Error details:", JSON.stringify(error, null, 2)); + return; + } -

- Sign in with your OIDC provider to continue -

+ console.log("Sign-in successful:", data); + } catch (error) { + console.error("Unexpected error during sign-in:", error); + if (error instanceof Error) { + console.error("Error message:", error.message); + console.error("Error stack:", error.stack); + } + } + }; - -
-
- ); + return ( +
+
+

+ Log In +

+ +

+ Sign in with your OIDC provider to continue +

+ + +
+
+ ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index d9be00c..8298d6d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,46 +1,49 @@ "use client"; -import { useSession } from "@/lib/auth-client"; import Link from "next/link"; +import { useSession } from "@/lib/auth-client"; export default function Home() { - const { data: session, isPending } = useSession(); + const { data: session, isPending } = useSession(); - return ( -
-
-

- Welcome to ToolHive Cloud UI -

+ return ( +
+
+

+ Welcome to ToolHive Cloud UI +

- {isPending ? ( -

Loading...

- ) : session ? ( -
-

- You are logged in as {session.user.email} -

- - Go to Dashboard - -
- ) : ( -
-

- Please log in to access the application -

- - Log In - -
- )} -
-
- ); + {isPending ? ( +

Loading...

+ ) : session?.user ? ( +
+

+ You are logged in as{" "} + + {session.user.email || session.user.name || "User"} + +

+ + Go to Dashboard + +
+ ) : ( +
+

+ Please log in to access the application +

+ + Log In + +
+ )} +
+
+ ); } diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts new file mode 100644 index 0000000..b496c5b --- /dev/null +++ b/src/lib/auth-client.ts @@ -0,0 +1,11 @@ +import { genericOAuthClient } from "better-auth/client/plugins"; +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + // Don't specify baseURL - it will use the same origin as the page + // This avoids CORS issues when Next.js uses a different port + plugins: [genericOAuthClient()], +}); + +// You can also export specific methods if you prefer +export const { signIn, signOut, useSession } = authClient; diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..6bcb1e6 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,57 @@ +import { betterAuth } from "better-auth"; +import { genericOAuth } from "better-auth/plugins"; + +// Read from environment variables to support any OIDC provider +const OIDC_ISSUER = process.env.OIDC_ISSUER_URL || ""; +const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || ""; +const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || ""; +const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET || "ChangeMePlease"; +const BETTER_AUTH_URL = process.env.BETTER_AUTH_URL || "http://localhost:3000"; + +if (!OIDC_ISSUER || !OIDC_CLIENT_ID || !OIDC_CLIENT_SECRET) { + console.warn( + "[Better Auth] Missing OIDC configuration. Set OIDC_ISSUER_URL, OIDC_CLIENT_ID, and OIDC_CLIENT_SECRET in .env.local", + ); +} + +console.log("[Better Auth] OIDC Configuration:", { + issuer: OIDC_ISSUER, + clientId: OIDC_CLIENT_ID, + baseURL: BETTER_AUTH_URL, + discoveryUrl: `${OIDC_ISSUER}/.well-known/openid-configuration`, + callbackURL: `${BETTER_AUTH_URL}/api/auth/oauth2/callback/oidc`, +}); + +export const auth = betterAuth({ + secret: BETTER_AUTH_SECRET, + baseURL: BETTER_AUTH_URL, + trustedOrigins: [ + "http://localhost:3000", + "http://localhost:3001", + "http://localhost:3002", + "http://localhost:3003", + ], + // No database configuration - running in stateless mode + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + cookieCache: { + enabled: true, + maxAge: 30 * 24 * 60 * 60, // 30 days cache duration + strategy: "jwe", // Use encrypted tokens for better security + refreshCache: true, // Enable stateless refresh + }, + }, + plugins: [ + genericOAuth({ + config: [ + { + providerId: "oidc", + discoveryUrl: `${OIDC_ISSUER}/.well-known/openid-configuration`, + clientId: OIDC_CLIENT_ID, + clientSecret: OIDC_CLIENT_SECRET, + scopes: ["openid", "email", "profile"], + }, + ], + }), + ], +}); From 640b49209a326d79c5edfe4aadb1b0abf1643dc8 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 14 Nov 2025 10:48:17 +0100 Subject: [PATCH 07/14] . --- dev/oidc-provider.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev/oidc-provider.mjs b/dev/oidc-provider.mjs index 8c08be1..c019861 100644 --- a/dev/oidc-provider.mjs +++ b/dev/oidc-provider.mjs @@ -59,6 +59,10 @@ const configuration = { features: { devInteractions: { enabled: true }, // Enable dev interactions for easy testing }, + claims: { + email: ["email", "email_verified"], + profile: ["name"], + }, ttl: { AccessToken: 3600, // 1 hour RefreshToken: 86400 * 30, // 30 days From 0bbfd59316bf9be4be79c36b9ef22d2f7f9e10cb Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 14 Nov 2025 10:54:39 +0100 Subject: [PATCH 08/14] . --- OIDC_SETUP.md | 81 ----------------------------- dev-auth/README.md | 40 ++++++++++++++ {dev => dev-auth}/oidc-provider.mjs | 0 package.json | 2 +- 4 files changed, 41 insertions(+), 82 deletions(-) delete mode 100644 OIDC_SETUP.md create mode 100644 dev-auth/README.md rename {dev => dev-auth}/oidc-provider.mjs (100%) diff --git a/OIDC_SETUP.md b/OIDC_SETUP.md deleted file mode 100644 index 2b97439..0000000 --- a/OIDC_SETUP.md +++ /dev/null @@ -1,81 +0,0 @@ -# OIDC Authentication Setup - -This application supports authentication with any OIDC-compliant identity provider (Okta, Keycloak, Auth0, etc.). - -## Configuration - -1. **Configure your OIDC provider** with these settings: - - **Callback URL**: `http://localhost:3000/api/auth/oauth2/callback/oidc` (adjust domain/port for production) - - **Grant Types**: Authorization Code, Refresh Token - - **Response Type**: code - - **Scopes**: openid, email, profile - -2. **Set environment variables** in `.env.local`: - -```bash -# Better Auth Configuration -BETTER_AUTH_SECRET=your-random-secret-here -BETTER_AUTH_URL=http://localhost:3000 - -# OIDC Provider Configuration -OIDC_CLIENT_ID=your-client-id -OIDC_CLIENT_SECRET=your-client-secret -OIDC_ISSUER_URL=https://your-oidc-provider.com -``` - -## Local Development - -For local testing, this repo includes a test OIDC provider: - -1. **Start the OIDC provider**: - ```bash - pnpm oidc - ``` - -2. **Use these credentials** in `.env.local`: - ```bash - OIDC_CLIENT_ID=better-auth-dev - OIDC_CLIENT_SECRET=dev-secret-change-in-production - OIDC_ISSUER_URL=http://localhost:4000 - ``` - -3. **Run the app**: - ```bash - pnpm dev - ``` - - Or run both concurrently: - ```bash - pnpm dev - ``` - -The test provider automatically logs in as `test@example.com`. - -## Provider-Specific Guides - -### Okta - -1. Create a new App Integration (OIDC - Web Application) -2. Set Callback URL: `http://localhost:3000/api/auth/oauth2/callback/oidc` -3. Use Okta domain as OIDC_ISSUER_URL (e.g., `https://dev-12345.okta.com`) - -### Keycloak - -1. Create a new Client -2. Set Access Type: confidential -3. Set Valid Redirect URIs: `http://localhost:3000/api/auth/oauth2/callback/oidc` -4. Use Realm URL as OIDC_ISSUER_URL (e.g., `https://keycloak.example.com/realms/myrealm`) - -### Auth0 - -1. Create a Regular Web Application -2. Set Allowed Callback URLs: `http://localhost:3000/api/auth/oauth2/callback/oidc` -3. Use Auth0 domain as OIDC_ISSUER_URL (e.g., `https://your-tenant.auth0.com`) - -## Architecture - -This application uses **stateless authentication**: -- No database required -- Session data stored in encrypted JWE cookies -- 7-day session expiration with 30-day refresh window -- Works with any OIDC-compliant provider diff --git a/dev-auth/README.md b/dev-auth/README.md new file mode 100644 index 0000000..1fb2719 --- /dev/null +++ b/dev-auth/README.md @@ -0,0 +1,40 @@ +# Local Development OIDC Provider + +This directory contains a simple OIDC provider for local development and testing. + +## What is it? + +A minimal OIDC-compliant identity provider built with `oidc-provider` that: +- Automatically logs in a test user (`test@example.com`) +- Auto-approves all consent requests +- Supports standard OAuth 2.0 / OIDC flows + +## How to use + +Start the provider: +```bash +pnpm oidc +``` + +Or run it alongside the Next.js app: +```bash +pnpm dev +``` + +The provider runs on `http://localhost:4000` and is already configured in `.env.local`. + +## Configuration + +The provider is pre-configured with: +- **Client ID**: `better-auth-dev` +- **Client Secret**: `dev-secret-change-in-production` +- **Test User**: `test@example.com` (Test User) +- **Supported Scopes**: openid, email, profile +- **Redirect URIs**: Ports 3000-3003 supported + +## For Production + +Replace this with a real OIDC provider (Okta, Keycloak, Auth0, etc.) by updating the environment variables in `.env.local`: +- `OIDC_ISSUER_URL` +- `OIDC_CLIENT_ID` +- `OIDC_CLIENT_SECRET` diff --git a/dev/oidc-provider.mjs b/dev-auth/oidc-provider.mjs similarity index 100% rename from dev/oidc-provider.mjs rename to dev-auth/oidc-provider.mjs diff --git a/package.json b/package.json index 3eeff9b..9d3a51d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test": "vitest", "type-check": "tsc --noEmit", "prepare": "husky", - "oidc": "node dev/oidc-provider.mjs" + "oidc": "node dev-auth/oidc-provider.mjs" }, "dependencies": { "better-auth": "1.4.0-beta.20", From 5cd5e0c90d0c3fe67c0a5bf0a06b8074a8133556 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 14 Nov 2025 10:57:52 +0100 Subject: [PATCH 09/14] reformat files --- dev-auth/oidc-provider.mjs | 216 ++++++++++++++++++------------------- src/app/dashboard/page.tsx | 80 +++++++------- 2 files changed, 148 insertions(+), 148 deletions(-) diff --git a/dev-auth/oidc-provider.mjs b/dev-auth/oidc-provider.mjs index c019861..48058aa 100644 --- a/dev-auth/oidc-provider.mjs +++ b/dev-auth/oidc-provider.mjs @@ -5,131 +5,131 @@ const PORT = 4000; // Simple in-memory account storage const accounts = { - "test-user": { - accountId: "test-user", - email: "test@example.com", - email_verified: true, - name: "Test User", - }, + "test-user": { + accountId: "test-user", + email: "test@example.com", + email_verified: true, + name: "Test User", + }, }; // Configuration const configuration = { - clients: [ - { - client_id: "better-auth-dev", - client_secret: "dev-secret-change-in-production", - redirect_uris: [ - // Better Auth genericOAuth uses /oauth2/callback/:providerId - "http://localhost:3000/api/auth/oauth2/callback/oidc", - "http://localhost:3001/api/auth/oauth2/callback/oidc", - "http://localhost:3002/api/auth/oauth2/callback/oidc", - "http://localhost:3003/api/auth/oauth2/callback/oidc", - ], - response_types: ["code"], - grant_types: ["authorization_code", "refresh_token"], - token_endpoint_auth_method: "client_secret_post", - }, - ], - cookies: { - keys: ["some-secret-key-for-dev"], - }, - findAccount: async (ctx, id) => { - const account = accounts[id]; - if (!account) return undefined; + clients: [ + { + client_id: "better-auth-dev", + client_secret: "dev-secret-change-in-production", + redirect_uris: [ + // Better Auth genericOAuth uses /oauth2/callback/:providerId + "http://localhost:3000/api/auth/oauth2/callback/oidc", + "http://localhost:3001/api/auth/oauth2/callback/oidc", + "http://localhost:3002/api/auth/oauth2/callback/oidc", + "http://localhost:3003/api/auth/oauth2/callback/oidc", + ], + response_types: ["code"], + grant_types: ["authorization_code", "refresh_token"], + token_endpoint_auth_method: "client_secret_post", + }, + ], + cookies: { + keys: ["some-secret-key-for-dev"], + }, + findAccount: async (ctx, id) => { + const account = accounts[id]; + if (!account) return undefined; - return { - accountId: id, - async claims() { - return { - sub: id, - email: account.email, - email_verified: account.email_verified, - name: account.name, - }; - }, - }; - }, - // Simple interaction - auto-login for dev - interactions: { - url(ctx, interaction) { - return `/interaction/${interaction.uid}`; - }, - }, - features: { - devInteractions: { enabled: true }, // Enable dev interactions for easy testing - }, - claims: { - email: ["email", "email_verified"], - profile: ["name"], - }, - ttl: { - AccessToken: 3600, // 1 hour - RefreshToken: 86400 * 30, // 30 days - }, + return { + accountId: id, + async claims() { + return { + sub: id, + email: account.email, + email_verified: account.email_verified, + name: account.name, + }; + }, + }; + }, + // Simple interaction - auto-login for dev + interactions: { + url(ctx, interaction) { + return `/interaction/${interaction.uid}`; + }, + }, + features: { + devInteractions: { enabled: true }, // Enable dev interactions for easy testing + }, + claims: { + email: ["email", "email_verified"], + profile: ["name"], + }, + ttl: { + AccessToken: 3600, // 1 hour + RefreshToken: 86400 * 30, // 30 days + }, }; const oidc = new Provider(ISSUER, configuration); // Simple interaction endpoint for dev - auto-login as test-user oidc.use(async (ctx, next) => { - if (ctx.path.startsWith("/interaction/")) { - const uid = ctx.path.split("/")[2]; - const interaction = await oidc.interactionDetails(ctx.req, ctx.res); + if (ctx.path.startsWith("/interaction/")) { + const uid = ctx.path.split("/")[2]; + const interaction = await oidc.interactionDetails(ctx.req, ctx.res); - if (interaction.prompt.name === "login") { - // Auto-login as test-user for dev - await oidc.interactionFinished( - ctx.req, - ctx.res, - { - login: { - accountId: "test-user", - }, - }, - { mergeWithLastSubmission: false }, - ); - return; - } + if (interaction.prompt.name === "login") { + // Auto-login as test-user for dev + await oidc.interactionFinished( + ctx.req, + ctx.res, + { + login: { + accountId: "test-user", + }, + }, + { mergeWithLastSubmission: false }, + ); + return; + } - if (interaction.prompt.name === "consent") { - // Auto-consent for dev - const grant = new oidc.Grant({ - accountId: interaction.session.accountId, - clientId: interaction.params.client_id, - }); + if (interaction.prompt.name === "consent") { + // Auto-consent for dev + const grant = new oidc.Grant({ + accountId: interaction.session.accountId, + clientId: interaction.params.client_id, + }); - grant.addOIDCScope( - interaction.params.scope - ?.split(" ") - .filter((scope) => ["openid", "email", "profile"].includes(scope)) - .join(" ") || "openid email profile", - ); + grant.addOIDCScope( + interaction.params.scope + ?.split(" ") + .filter((scope) => ["openid", "email", "profile"].includes(scope)) + .join(" ") || "openid email profile", + ); - await grant.save(); + await grant.save(); - await oidc.interactionFinished( - ctx.req, - ctx.res, - { - consent: { - grantId: grant.jti, - }, - }, - { mergeWithLastSubmission: true }, - ); - return; - } - } - await next(); + await oidc.interactionFinished( + ctx.req, + ctx.res, + { + consent: { + grantId: grant.jti, + }, + }, + { mergeWithLastSubmission: true }, + ); + return; + } + } + await next(); }); oidc.listen(PORT, () => { - console.log(`πŸ” OIDC Provider running at ${ISSUER}`); - console.log(`πŸ“ Client ID: better-auth-dev`); - console.log(`πŸ”‘ Client Secret: dev-secret-change-in-production`); - console.log(`πŸ‘€ Test user: test@example.com`); - console.log( - `\nβš™οΈ Update your .env.local with:\nOIDC_CLIENT_ID=better-auth-dev\nOIDC_CLIENT_SECRET=dev-secret-change-in-production\nOIDC_ISSUER_URL=${ISSUER}`, - ); + console.log(`πŸ” OIDC Provider running at ${ISSUER}`); + console.log(`πŸ“ Client ID: better-auth-dev`); + console.log(`πŸ”‘ Client Secret: dev-secret-change-in-production`); + console.log(`πŸ‘€ Test user: test@example.com`); + console.log( + `\nβš™οΈ Update your .env.local with:\nOIDC_CLIENT_ID=better-auth-dev\nOIDC_CLIENT_SECRET=dev-secret-change-in-production\nOIDC_ISSUER_URL=${ISSUER}`, + ); }); diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 7efb198..d66a592 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,50 +1,50 @@ -import { auth } from "@/lib/auth"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; export default async function DashboardPage() { - const session = await auth.api.getSession({ - headers: await headers(), - }); + const session = await auth.api.getSession({ + headers: await headers(), + }); - if (!session) { - redirect("/login"); - } + if (!session) { + redirect("/login"); + } - return ( -
-
-

- Hello World! πŸŽ‰ -

+ return ( +
+
+

+ Hello World! πŸŽ‰ +

-
-

- You are successfully authenticated! -

+
+

+ You are successfully authenticated! +

-
-

- User Info: -

-

- Email: {session.user.email} -

-

- User ID: {session.user.id} -

-
-
+
+

+ User Info: +

+

+ Email: {session.user.email} +

+

+ User ID: {session.user.id} +

+
+
-
- -
-
-
- ); +
+ +
+
+
+ ); } From 16b4f562146f1a1bc6d7ec5ce40e8f80973173b5 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 14 Nov 2025 11:09:55 +0100 Subject: [PATCH 10/14] fix tests --- __tests__/page.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/page.test.tsx b/__tests__/page.test.tsx index 4996717..1fdaaec 100644 --- a/__tests__/page.test.tsx +++ b/__tests__/page.test.tsx @@ -2,12 +2,12 @@ import { render, screen } from "@testing-library/react"; import { expect, test } from "vitest"; import Home from "../src/app/page"; -test("Home page", () => { +test("Home page renders welcome heading", () => { render(); expect( screen.getByRole("heading", { level: 1, - name: /To get started, edit the page.tsx file./i, + name: /Welcome to ToolHive Cloud UI/i, }), ).toBeDefined(); }); From d9e1106b3a60f436c4e56efea6c4cb0cede36d58 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 14 Nov 2025 11:13:16 +0100 Subject: [PATCH 11/14] format --- dev-auth/oidc-provider.mjs | 6 +++--- src/app/api/auth/[...all]/route.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-auth/oidc-provider.mjs b/dev-auth/oidc-provider.mjs index 48058aa..a0bf281 100644 --- a/dev-auth/oidc-provider.mjs +++ b/dev-auth/oidc-provider.mjs @@ -34,7 +34,7 @@ const configuration = { cookies: { keys: ["some-secret-key-for-dev"], }, - findAccount: async (ctx, id) => { + findAccount: async (_ctx, id) => { const account = accounts[id]; if (!account) return undefined; @@ -52,7 +52,7 @@ const configuration = { }, // Simple interaction - auto-login for dev interactions: { - url(ctx, interaction) { + url(_ctx, interaction) { return `/interaction/${interaction.uid}`; }, }, @@ -74,7 +74,7 @@ const oidc = new Provider(ISSUER, configuration); // Simple interaction endpoint for dev - auto-login as test-user oidc.use(async (ctx, next) => { if (ctx.path.startsWith("/interaction/")) { - const uid = ctx.path.split("/")[2]; + const _uid = ctx.path.split("/")[2]; const interaction = await oidc.interactionDetails(ctx.req, ctx.res); if (interaction.prompt.name === "login") { diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts index e11351a..b2f604c 100644 --- a/src/app/api/auth/[...all]/route.ts +++ b/src/app/api/auth/[...all]/route.ts @@ -1,4 +1,4 @@ -import { auth } from "@/lib/auth"; import { toNextJsHandler } from "better-auth/next-js"; +import { auth } from "@/lib/auth"; export const { GET, POST } = toNextJsHandler(auth.handler); From 898c70b32f8d88efaf98e881910b2f5b98a8b2cc Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 14 Nov 2025 11:29:47 +0100 Subject: [PATCH 12/14] fix stuff based on copilot review --- src/app/dashboard/page.tsx | 2 +- src/lib/auth.ts | 42 +++++++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index d66a592..56688c9 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -28,7 +28,7 @@ export default async function DashboardPage() { User Info:

- Email: {session.user.email} + Email: {session.user.email || "Not provided"}

User ID: {session.user.id} diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 6bcb1e6..4d09bd5 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -2,15 +2,22 @@ import { betterAuth } from "better-auth"; import { genericOAuth } from "better-auth/plugins"; // Read from environment variables to support any OIDC provider -const OIDC_ISSUER = process.env.OIDC_ISSUER_URL || ""; -const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || ""; -const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || ""; -const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET || "ChangeMePlease"; +const OIDC_ISSUER = process.env.OIDC_ISSUER_URL; +const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID; +const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET; +const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET; const BETTER_AUTH_URL = process.env.BETTER_AUTH_URL || "http://localhost:3000"; +// Validate required environment variables +if (!BETTER_AUTH_SECRET) { + throw new Error( + "[Better Auth] BETTER_AUTH_SECRET is required. Set it in .env.local to a strong, random value.", + ); +} + if (!OIDC_ISSUER || !OIDC_CLIENT_ID || !OIDC_CLIENT_SECRET) { - console.warn( - "[Better Auth] Missing OIDC configuration. Set OIDC_ISSUER_URL, OIDC_CLIENT_ID, and OIDC_CLIENT_SECRET in .env.local", + throw new Error( + "[Better Auth] OIDC configuration is incomplete. Set OIDC_ISSUER_URL, OIDC_CLIENT_ID, and OIDC_CLIENT_SECRET in .env.local", ); } @@ -22,15 +29,26 @@ console.log("[Better Auth] OIDC Configuration:", { callbackURL: `${BETTER_AUTH_URL}/api/auth/oauth2/callback/oidc`, }); +// Configure trusted origins - defaults to localhost ports for development +// Set TRUSTED_ORIGINS environment variable for production (comma-separated list) +const trustedOrigins = process.env.TRUSTED_ORIGINS + ? process.env.TRUSTED_ORIGINS.split(",").map((origin) => origin.trim()) + : [ + "http://localhost:3000", + "http://localhost:3001", + "http://localhost:3002", + "http://localhost:3003", + ]; + +// Always include BETTER_AUTH_URL if not already present +if (BETTER_AUTH_URL && !trustedOrigins.includes(BETTER_AUTH_URL)) { + trustedOrigins.push(BETTER_AUTH_URL); +} + export const auth = betterAuth({ secret: BETTER_AUTH_SECRET, baseURL: BETTER_AUTH_URL, - trustedOrigins: [ - "http://localhost:3000", - "http://localhost:3001", - "http://localhost:3002", - "http://localhost:3003", - ], + trustedOrigins, // No database configuration - running in stateless mode session: { expiresIn: 60 * 60 * 24 * 7, // 7 days From 94b726c7d8c914d3843f48bdfe9590c5b717d6e6 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 14 Nov 2025 11:35:40 +0100 Subject: [PATCH 13/14] fix build isuses --- src/lib/auth.ts | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 4d09bd5..f583031 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -2,23 +2,34 @@ import { betterAuth } from "better-auth"; import { genericOAuth } from "better-auth/plugins"; // Read from environment variables to support any OIDC provider -const OIDC_ISSUER = process.env.OIDC_ISSUER_URL; -const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID; -const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET; -const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET; +const OIDC_ISSUER = process.env.OIDC_ISSUER_URL || ""; +const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || ""; +const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || ""; +const BETTER_AUTH_SECRET = + process.env.BETTER_AUTH_SECRET || "build-time-placeholder"; const BETTER_AUTH_URL = process.env.BETTER_AUTH_URL || "http://localhost:3000"; -// Validate required environment variables -if (!BETTER_AUTH_SECRET) { - throw new Error( - "[Better Auth] BETTER_AUTH_SECRET is required. Set it in .env.local to a strong, random value.", - ); +// Validate required environment variables (warnings only during build) +const isBuild = process.env.NEXT_PHASE === "phase-production-build"; + +if (!process.env.BETTER_AUTH_SECRET) { + const message = + "[Better Auth] BETTER_AUTH_SECRET is required. Set it in .env.local to a strong, random value."; + if (isBuild) { + console.warn(message); + } else { + throw new Error(message); + } } if (!OIDC_ISSUER || !OIDC_CLIENT_ID || !OIDC_CLIENT_SECRET) { - throw new Error( - "[Better Auth] OIDC configuration is incomplete. Set OIDC_ISSUER_URL, OIDC_CLIENT_ID, and OIDC_CLIENT_SECRET in .env.local", - ); + const message = + "[Better Auth] OIDC configuration is incomplete. Set OIDC_ISSUER_URL, OIDC_CLIENT_ID, and OIDC_CLIENT_SECRET in .env.local"; + if (isBuild) { + console.warn(message); + } else { + throw new Error(message); + } } console.log("[Better Auth] OIDC Configuration:", { From 4e2feacd7ec5c2dfbc12bda5397b46b00ec7779e Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 14 Nov 2025 13:18:09 +0100 Subject: [PATCH 14/14] chore: set up a keycloak based local testing with docker compose --- DOCKER_DEPLOYMENT.md | 162 +++++++++++++++++++++++++++++++++++ dev-auth/README.md | 9 ++ dev-auth/keycloak-realm.json | 91 ++++++++++++++++++++ docker-compose.yml | 53 ++++++++++++ 4 files changed, 315 insertions(+) create mode 100644 DOCKER_DEPLOYMENT.md create mode 100644 dev-auth/keycloak-realm.json create mode 100644 docker-compose.yml diff --git a/DOCKER_DEPLOYMENT.md b/DOCKER_DEPLOYMENT.md new file mode 100644 index 0000000..466cada --- /dev/null +++ b/DOCKER_DEPLOYMENT.md @@ -0,0 +1,162 @@ +# Docker Deployment (Production-like Local Stack) + +This setup runs a production-like environment locally using Docker Compose with: +- **Keycloak** as the OIDC identity provider +- **Next.js app** in production mode with Keycloak authentication + +This simulates how the application would run in production with a real identity provider. + +## Prerequisites + +- Docker and Docker Compose installed +- Ports 3002 and 8080 available + +## Quick Start + +1. **Start the stack**: + ```bash + docker compose up --build + ``` + +2. **Wait for services to be ready**: + - Keycloak: http://localhost:8080 (takes ~30 seconds) + - App: http://localhost:3002 + +3. **Log in to the app**: + - Click "Sign In with OIDC" + - Username: `test` + - Password: `test` + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ β”‚ β”‚ +β”‚ Browser │────────▢│ Next.js β”‚ +β”‚ β”‚ β”‚ (port 3002)β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ OIDC Auth + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ + β”‚ Keycloak β”‚ + β”‚ (port 8080)β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Configuration + +All configuration is done via environment variables in `docker-compose.yml`: + +### Keycloak Service +- **Admin Console**: http://localhost:8080/admin + - Username: `admin` + - Password: `admin` +- **Realm**: `toolhive` +- **Pre-configured client**: `toolhive-cloud-ui` + +### Next.js App Service +Environment variables: +- `BETTER_AUTH_SECRET`: Auth session encryption key +- `BETTER_AUTH_URL`: Public URL of the app +- `TRUSTED_ORIGINS`: Allowed CORS origins +- `OIDC_ISSUER_URL`: Keycloak realm URL (internal container URL) +- `OIDC_CLIENT_ID`: OAuth client identifier +- `OIDC_CLIENT_SECRET`: OAuth client secret + +## Test User + +A test user is pre-created in Keycloak: +- **Username**: `test` +- **Email**: `test@example.com` +- **Password**: `test` +- **Name**: Test User + +## Keycloak Admin Access + +Access the Keycloak admin console to manage users, clients, and settings: + +1. Open http://localhost:8080/admin +2. Log in with admin credentials (admin/admin) +3. Select the `toolhive` realm + +## Customization + +### Adding More Users + +1. Access Keycloak admin console +2. Go to Users β†’ Add User +3. Fill in user details +4. Go to Credentials tab and set a password + +### Modifying Client Settings + +1. Access Keycloak admin console +2. Go to Clients β†’ `toolhive-cloud-ui` +3. Modify settings as needed +4. Remember to update redirect URIs if changing ports + +### Changing Environment Variables + +Edit `docker-compose.yml` and restart: +```bash +docker compose down +docker compose up --build +``` + +## Stopping the Stack + +```bash +# Stop services +docker compose down + +# Stop and remove volumes (resets Keycloak data) +docker compose down -v +``` + +## Troubleshooting + +### Keycloak not starting +- Wait 30-60 seconds for Keycloak to initialize +- Check logs: `docker compose logs keycloak` + +### App can't connect to Keycloak +- Ensure both containers are on the same network +- Check `OIDC_ISSUER_URL` uses container name: `http://keycloak:8080` + +### Authentication redirects to wrong URL +- Browser uses `http://localhost:3002` +- Container uses `http://keycloak:8080` (internal) +- Ensure redirect URIs in Keycloak match `http://localhost:3002/api/auth/oauth2/callback/oidc` + +## Production Deployment + +For actual production deployment: + +1. **Use a proper database** for Keycloak (PostgreSQL, MySQL) +2. **Enable HTTPS** with proper certificates +3. **Use strong secrets** - generate with `openssl rand -base64 32` +4. **Configure proper hostname** in Keycloak settings +5. **Remove dev mode** from Keycloak command +6. **Set up persistent volumes** for Keycloak data +7. **Configure proper CORS** origins +8. **Use Kubernetes/cloud services** instead of Docker Compose + +## Differences from Development Mode + +| Aspect | Development (`pnpm dev`) | Production-like (Docker) | +|--------|-------------------------|--------------------------| +| Identity Provider | Test OIDC provider | Keycloak | +| Authentication | Auto-login | Real login form | +| User Management | Hardcoded test user | Keycloak admin UI | +| Configuration | `.env.local` file | Environment variables | +| Build | Development mode | Production build | +| Performance | Slow (dev mode) | Fast (optimized) | + +## Network Details + +The Docker Compose setup creates an internal bridge network (`toolhive`): +- App and Keycloak communicate via container names +- External access via published ports (3000, 8080) +- Browser connects to `localhost`, app connects to `keycloak` hostname diff --git a/dev-auth/README.md b/dev-auth/README.md index 1fb2719..7b0d036 100644 --- a/dev-auth/README.md +++ b/dev-auth/README.md @@ -32,6 +32,15 @@ The provider is pre-configured with: - **Supported Scopes**: openid, email, profile - **Redirect URIs**: Ports 3000-3003 supported +## Production-like Setup + +For a production-like local environment with Keycloak, see [`DOCKER_DEPLOYMENT.md`](../DOCKER_DEPLOYMENT.md) in the root directory. + +The Docker Compose setup includes: +- Keycloak as a real identity provider +- Next.js app in production mode +- Proper authentication flow (no auto-login) + ## For Production Replace this with a real OIDC provider (Okta, Keycloak, Auth0, etc.) by updating the environment variables in `.env.local`: diff --git a/dev-auth/keycloak-realm.json b/dev-auth/keycloak-realm.json new file mode 100644 index 0000000..12d9672 --- /dev/null +++ b/dev-auth/keycloak-realm.json @@ -0,0 +1,91 @@ +{ + "id": "toolhive", + "realm": "toolhive", + "displayName": "ToolHive Cloud UI", + "enabled": true, + "sslRequired": "none", + "registrationAllowed": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": true, + "clients": [ + { + "clientId": "toolhive-cloud-ui", + "name": "ToolHive Cloud UI", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "secret": "toolhive-client-secret", + "redirectUris": [ + "http://localhost:3000/api/auth/oauth2/callback/oidc", + "http://localhost:3001/api/auth/oauth2/callback/oidc", + "http://localhost:3002/api/auth/oauth2/callback/oidc", + "http://localhost:3003/api/auth/oauth2/callback/oidc" + ], + "webOrigins": [ + "http://localhost:3000", + "http://localhost:3001", + "http://localhost:3002", + "http://localhost:3003" + ], + "standardFlowEnabled": true, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "authorizationServicesEnabled": false, + "fullScopeAllowed": true, + "attributes": { + "access.token.lifespan": "3600", + "client.secret.creation.time": "0" + }, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "users": [ + { + "username": "test", + "enabled": true, + "emailVerified": true, + "email": "test@example.com", + "firstName": "Test", + "lastName": "User", + "credentials": [ + { + "type": "password", + "value": "test", + "temporary": false + } + ], + "realmRoles": ["user"] + } + ], + "roles": { + "realm": [ + { + "name": "user", + "description": "User role" + } + ] + }, + "defaultRole": { + "name": "default-roles-toolhive", + "description": "Default role for new users", + "composite": true, + "composites": { + "realm": ["user"] + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..725760a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +services: + # Keycloak Identity Provider + keycloak: + image: quay.io/keycloak/keycloak:26.0 + command: start-dev --import-realm + environment: + # Admin credentials + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + # Database (dev mode uses H2) + KC_DB: dev-mem + # Hostname configuration + KC_HOSTNAME_STRICT: false + KC_HOSTNAME_STRICT_HTTPS: false + KC_HTTP_ENABLED: true + ports: + - "8080:8080" + volumes: + # Import pre-configured realm on startup + - ./dev-auth/keycloak-realm.json:/opt/keycloak/data/import/realm.json:ro + healthcheck: + test: ["CMD-SHELL", "timeout 1 bash -c '