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 (
-
-
-
-
-
- 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 '