Skip to content

Commit 2a910dd

Browse files
authored
feat: set up better-auth (#23)
1 parent 17bfd02 commit 2a910dd

File tree

11 files changed

+1120
-60
lines changed

11 files changed

+1120
-60
lines changed

__tests__/page.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { render, screen } from "@testing-library/react";
22
import { expect, test } from "vitest";
33
import Home from "../src/app/page";
44

5-
test("Home page", () => {
5+
test("Home page renders welcome heading", () => {
66
render(<Home />);
77
expect(
88
screen.getByRole("heading", {
99
level: 1,
10-
name: /To get started, edit the page.tsx file./i,
10+
name: /Welcome to ToolHive Cloud UI/i,
1111
}),
1212
).toBeDefined();
1313
});

dev-auth/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Local Development OIDC Provider
2+
3+
This directory contains a simple OIDC provider for local development and testing.
4+
5+
## What is it?
6+
7+
A minimal OIDC-compliant identity provider built with `oidc-provider` that:
8+
- Automatically logs in a test user (`[email protected]`)
9+
- Auto-approves all consent requests
10+
- Supports standard OAuth 2.0 / OIDC flows
11+
12+
## How to use
13+
14+
Start the provider:
15+
```bash
16+
pnpm oidc
17+
```
18+
19+
Or run it alongside the Next.js app:
20+
```bash
21+
pnpm dev
22+
```
23+
24+
The provider runs on `http://localhost:4000` and is already configured in `.env.local`.
25+
26+
## Configuration
27+
28+
The provider is pre-configured with:
29+
- **Client ID**: `better-auth-dev`
30+
- **Client Secret**: `dev-secret-change-in-production`
31+
- **Test User**: `[email protected]` (Test User)
32+
- **Supported Scopes**: openid, email, profile
33+
- **Redirect URIs**: Ports 3000-3003 supported
34+
35+
## For Production
36+
37+
Replace this with a real OIDC provider (Okta, Keycloak, Auth0, etc.) by updating the environment variables in `.env.local`:
38+
- `OIDC_ISSUER_URL`
39+
- `OIDC_CLIENT_ID`
40+
- `OIDC_CLIENT_SECRET`

dev-auth/oidc-provider.mjs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import Provider from "oidc-provider";
2+
3+
const ISSUER = "http://localhost:4000";
4+
const PORT = 4000;
5+
6+
// Simple in-memory account storage
7+
const accounts = {
8+
"test-user": {
9+
accountId: "test-user",
10+
11+
email_verified: true,
12+
name: "Test User",
13+
},
14+
};
15+
16+
// Configuration
17+
const configuration = {
18+
clients: [
19+
{
20+
client_id: "better-auth-dev",
21+
client_secret: "dev-secret-change-in-production",
22+
redirect_uris: [
23+
// Better Auth genericOAuth uses /oauth2/callback/:providerId
24+
"http://localhost:3000/api/auth/oauth2/callback/oidc",
25+
"http://localhost:3001/api/auth/oauth2/callback/oidc",
26+
"http://localhost:3002/api/auth/oauth2/callback/oidc",
27+
"http://localhost:3003/api/auth/oauth2/callback/oidc",
28+
],
29+
response_types: ["code"],
30+
grant_types: ["authorization_code", "refresh_token"],
31+
token_endpoint_auth_method: "client_secret_post",
32+
},
33+
],
34+
cookies: {
35+
keys: ["some-secret-key-for-dev"],
36+
},
37+
findAccount: async (_ctx, id) => {
38+
const account = accounts[id];
39+
if (!account) return undefined;
40+
41+
return {
42+
accountId: id,
43+
async claims() {
44+
return {
45+
sub: id,
46+
email: account.email,
47+
email_verified: account.email_verified,
48+
name: account.name,
49+
};
50+
},
51+
};
52+
},
53+
// Simple interaction - auto-login for dev
54+
interactions: {
55+
url(_ctx, interaction) {
56+
return `/interaction/${interaction.uid}`;
57+
},
58+
},
59+
features: {
60+
devInteractions: { enabled: true }, // Enable dev interactions for easy testing
61+
},
62+
claims: {
63+
email: ["email", "email_verified"],
64+
profile: ["name"],
65+
},
66+
ttl: {
67+
AccessToken: 3600, // 1 hour
68+
RefreshToken: 86400 * 30, // 30 days
69+
},
70+
};
71+
72+
const oidc = new Provider(ISSUER, configuration);
73+
74+
// Simple interaction endpoint for dev - auto-login as test-user
75+
oidc.use(async (ctx, next) => {
76+
if (ctx.path.startsWith("/interaction/")) {
77+
const _uid = ctx.path.split("/")[2];
78+
const interaction = await oidc.interactionDetails(ctx.req, ctx.res);
79+
80+
if (interaction.prompt.name === "login") {
81+
// Auto-login as test-user for dev
82+
await oidc.interactionFinished(
83+
ctx.req,
84+
ctx.res,
85+
{
86+
login: {
87+
accountId: "test-user",
88+
},
89+
},
90+
{ mergeWithLastSubmission: false },
91+
);
92+
return;
93+
}
94+
95+
if (interaction.prompt.name === "consent") {
96+
// Auto-consent for dev
97+
const grant = new oidc.Grant({
98+
accountId: interaction.session.accountId,
99+
clientId: interaction.params.client_id,
100+
});
101+
102+
grant.addOIDCScope(
103+
interaction.params.scope
104+
?.split(" ")
105+
.filter((scope) => ["openid", "email", "profile"].includes(scope))
106+
.join(" ") || "openid email profile",
107+
);
108+
109+
await grant.save();
110+
111+
await oidc.interactionFinished(
112+
ctx.req,
113+
ctx.res,
114+
{
115+
consent: {
116+
grantId: grant.jti,
117+
},
118+
},
119+
{ mergeWithLastSubmission: true },
120+
);
121+
return;
122+
}
123+
}
124+
await next();
125+
});
126+
127+
oidc.listen(PORT, () => {
128+
console.log(`🔐 OIDC Provider running at ${ISSUER}`);
129+
console.log(`📝 Client ID: better-auth-dev`);
130+
console.log(`🔑 Client Secret: dev-secret-change-in-production`);
131+
console.log(`👤 Test user: [email protected]`);
132+
console.log(
133+
`\n⚙️ Update your .env.local with:\nOIDC_CLIENT_ID=better-auth-dev\nOIDC_CLIENT_SECRET=dev-secret-change-in-production\nOIDC_ISSUER_URL=${ISSUER}`,
134+
);
135+
});

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44
"private": true,
55
"packageManager": "[email protected]",
66
"scripts": {
7-
"dev": "next dev",
7+
"dev": "concurrently -n \"OIDC,Next\" -c \"blue,green\" \"pnpm oidc\" \"pnpm dev:next\"",
8+
"dev:next": "next dev",
89
"build": "next build",
910
"start": "next start",
1011
"lint": "biome check",
1112
"format": "biome format --write",
1213
"test": "vitest",
1314
"type-check": "tsc --noEmit",
14-
"prepare": "husky"
15+
"prepare": "husky",
16+
"oidc": "node dev-auth/oidc-provider.mjs"
1517
},
1618
"dependencies": {
19+
"better-auth": "1.4.0-beta.20",
1720
"next": "16.0.3",
1821
"react": "19.2.0",
1922
"react-dom": "19.2.0"
@@ -28,9 +31,11 @@
2831
"@types/react-dom": "^19",
2932
"@vitejs/plugin-react": "^5.1.1",
3033
"babel-plugin-react-compiler": "1.0.0",
34+
"concurrently": "^9.2.1",
3135
"husky": "^9.1.7",
3236
"jsdom": "^27.2.0",
3337
"lint-staged": "^16.0.0",
38+
"oidc-provider": "^9.5.2",
3439
"tailwindcss": "^4",
3540
"typescript": "^5",
3641
"vite-tsconfig-paths": "^5.1.4",

0 commit comments

Comments
 (0)