Skip to content

Commit 370b476

Browse files
[recipes] OAuth MCP upgrade and remote auth hardening
1 parent f934a8b commit 370b476

48 files changed

Lines changed: 8400 additions & 473 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
node_modules/
22
.DS_Store
33
.planning/
4-
supabase/
4+
/supabase/
55

66
# Credentials & runtime state
77
.env
@@ -14,3 +14,6 @@ sync-log.json
1414

1515
# Build artifacts
1616
deno.lock
17+
.next/
18+
.svelte-kit/
19+
dist/

dashboards/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ https://github.com/user-attachments/assets/9454662f-2648-4928-8723-f7d52e94e9b8
44

55
Frontend templates you can host on Vercel or Netlify, pointed at your Supabase backend. Instant UI for your brain.
66

7-
*No community dashboards yet — be the first to contribute one.*
7+
| Dashboard | What It Does |
8+
| --------- | ------------ |
9+
| [Open Brain Dashboard](open-brain-dashboard/) | SvelteKit UI for browsing, searching, and capturing thoughts through MCP |
10+
| [Open Brain Dashboard Next](open-brain-dashboard-next/) | Full-featured Next.js dashboard backed by the REST gateway |
11+
| [Open Brain Auth Portal](open-brain-auth-portal/) | Minimal sign-in and consent app for MCP OAuth flows |
812

913
## Ideas
1014

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
2+
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Open Brain Auth Portal
2+
3+
Standalone OAuth consent portal for Open Brain MCP servers.
4+
5+
## What it does
6+
7+
This app is the hosted sign-in and consent surface for the new Open Brain OAuth-based MCP flow. It gives Supabase Auth a real login page, a consent screen, and approve/deny callbacks that redirect back to Claude, ChatGPT, or any other remote MCP client.
8+
9+
## Prerequisites
10+
11+
- Working Open Brain setup ([guide](../../docs/01-getting-started.md))
12+
- Supabase OAuth 2.1 enabled for your project
13+
- A Supabase Auth owner user created for Open Brain
14+
- Node.js 20.9+
15+
16+
## Quick Start
17+
18+
1. Install dependencies:
19+
20+
```bash
21+
cd dashboards/open-brain-auth-portal
22+
npm install
23+
```
24+
25+
2. Copy envs:
26+
27+
```bash
28+
cp .env.example .env.local
29+
```
30+
31+
3. Fill in:
32+
33+
- `NEXT_PUBLIC_SUPABASE_URL`
34+
- `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`
35+
36+
4. Run locally:
37+
38+
```bash
39+
npm run dev
40+
```
41+
42+
5. Deploy to Vercel and use the deployed `/oauth/consent` route as your Supabase OAuth authorization URL.
43+
44+
## Expected outcome
45+
46+
After deployment, a remote MCP client that hits your protected Open Brain server and receives `401 + WWW-Authenticate` should be redirected through Supabase Auth and land on this portal for sign-in and approval.
47+
48+
## Troubleshooting
49+
50+
**Issue: Consent screen says `Missing authorization_id`**
51+
Solution: You opened the consent route directly without a real Supabase OAuth request. Use the root page preview or trigger the flow from an MCP client.
52+
53+
**Issue: Sign-in works but consent page loops back to login**
54+
Solution: Verify the same `NEXT_PUBLIC_SUPABASE_URL` and publishable key are configured in both local env and deployed env. Mismatched projects cause session cookies to be ignored.
55+
56+
**Issue: Supabase returns an invalid authorization request**
57+
Solution: Confirm Supabase OAuth 2.1 is enabled and that the authorization URL path points to `/oauth/consent` on this deployed app.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { NextResponse } from "next/server";
2+
import { createSupabaseServerClient } from "@/lib/supabase/server";
3+
4+
export async function POST(request: Request) {
5+
const formData = await request.formData();
6+
const decision = formData.get("decision");
7+
const authorizationId = String(formData.get("authorization_id") || "");
8+
9+
if (!authorizationId) {
10+
return NextResponse.json({ error: "Missing authorization_id" }, { status: 400 });
11+
}
12+
13+
const supabase = await createSupabaseServerClient();
14+
15+
if (decision === "approve") {
16+
const { data, error } = await supabase.auth.oauth.approveAuthorization(authorizationId);
17+
18+
if (error) {
19+
return NextResponse.json({ error: error.message }, { status: 400 });
20+
}
21+
22+
return NextResponse.redirect(data.redirect_url);
23+
}
24+
25+
const { data, error } = await supabase.auth.oauth.denyAuthorization(authorizationId);
26+
27+
if (error) {
28+
return NextResponse.json({ error: error.message }, { status: 400 });
29+
}
30+
31+
return NextResponse.redirect(data.redirect_url);
32+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
:root {
2+
--bg: #f4efe4;
3+
--card: rgba(255, 251, 244, 0.88);
4+
--ink: #152033;
5+
--muted: #5e697b;
6+
--line: rgba(21, 32, 51, 0.12);
7+
--accent: #c65d2d;
8+
--accent-2: #157a8a;
9+
--shadow: 0 24px 60px rgba(21, 32, 51, 0.12);
10+
}
11+
12+
* {
13+
box-sizing: border-box;
14+
}
15+
16+
html {
17+
min-height: 100%;
18+
}
19+
20+
body {
21+
margin: 0;
22+
min-height: 100vh;
23+
font-family: var(--font-display), sans-serif;
24+
color: var(--ink);
25+
background:
26+
radial-gradient(circle at top left, rgba(198, 93, 45, 0.18), transparent 32%),
27+
radial-gradient(circle at top right, rgba(21, 122, 138, 0.2), transparent 28%),
28+
linear-gradient(180deg, #fffaf1 0%, var(--bg) 100%);
29+
}
30+
31+
a {
32+
color: inherit;
33+
}
34+
35+
.shell {
36+
width: min(960px, calc(100% - 2rem));
37+
margin: 0 auto;
38+
padding: 2rem 0 4rem;
39+
}
40+
41+
.hero {
42+
display: grid;
43+
gap: 1.5rem;
44+
align-items: start;
45+
}
46+
47+
.pill {
48+
display: inline-flex;
49+
align-items: center;
50+
gap: 0.5rem;
51+
padding: 0.45rem 0.8rem;
52+
border-radius: 999px;
53+
border: 1px solid var(--line);
54+
background: rgba(255, 255, 255, 0.52);
55+
font-family: var(--font-mono), monospace;
56+
font-size: 0.82rem;
57+
text-transform: uppercase;
58+
letter-spacing: 0.08em;
59+
}
60+
61+
.grid {
62+
display: grid;
63+
gap: 1rem;
64+
}
65+
66+
.cards {
67+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
68+
}
69+
70+
.card {
71+
background: var(--card);
72+
border: 1px solid var(--line);
73+
border-radius: 28px;
74+
padding: 1.25rem;
75+
box-shadow: var(--shadow);
76+
backdrop-filter: blur(14px);
77+
}
78+
79+
.card h1,
80+
.card h2,
81+
.card h3,
82+
.card p {
83+
margin-top: 0;
84+
}
85+
86+
.headline {
87+
font-size: clamp(2.2rem, 4vw, 4.5rem);
88+
line-height: 0.94;
89+
letter-spacing: -0.04em;
90+
margin-bottom: 1rem;
91+
}
92+
93+
.lede,
94+
.muted {
95+
color: var(--muted);
96+
}
97+
98+
.button-row {
99+
display: flex;
100+
flex-wrap: wrap;
101+
gap: 0.75rem;
102+
}
103+
104+
.button,
105+
.ghost-button,
106+
button {
107+
appearance: none;
108+
border: none;
109+
border-radius: 999px;
110+
padding: 0.9rem 1.2rem;
111+
font: inherit;
112+
cursor: pointer;
113+
transition: transform 160ms ease, box-shadow 160ms ease, opacity 160ms ease;
114+
}
115+
116+
.button,
117+
button[type="submit"] {
118+
background: linear-gradient(135deg, var(--accent), #ea8a47);
119+
color: white;
120+
box-shadow: 0 16px 30px rgba(198, 93, 45, 0.22);
121+
}
122+
123+
.ghost-button {
124+
background: transparent;
125+
color: var(--ink);
126+
border: 1px solid var(--line);
127+
}
128+
129+
.button:hover,
130+
.ghost-button:hover,
131+
button:hover {
132+
transform: translateY(-1px);
133+
}
134+
135+
.stack {
136+
display: grid;
137+
gap: 0.9rem;
138+
}
139+
140+
.field {
141+
display: grid;
142+
gap: 0.35rem;
143+
}
144+
145+
.field label {
146+
font-family: var(--font-mono), monospace;
147+
font-size: 0.82rem;
148+
text-transform: uppercase;
149+
letter-spacing: 0.08em;
150+
}
151+
152+
.field input {
153+
width: 100%;
154+
border-radius: 18px;
155+
border: 1px solid var(--line);
156+
background: rgba(255, 255, 255, 0.72);
157+
padding: 0.95rem 1rem;
158+
font: inherit;
159+
}
160+
161+
.list {
162+
display: grid;
163+
gap: 0.6rem;
164+
padding-left: 1.1rem;
165+
}
166+
167+
.error {
168+
border: 1px solid rgba(198, 93, 45, 0.26);
169+
background: rgba(198, 93, 45, 0.08);
170+
color: #8b3d1d;
171+
border-radius: 18px;
172+
padding: 0.9rem 1rem;
173+
}
174+
175+
.meta {
176+
display: grid;
177+
gap: 0.7rem;
178+
}
179+
180+
.meta-row {
181+
display: grid;
182+
gap: 0.2rem;
183+
padding-bottom: 0.7rem;
184+
border-bottom: 1px solid var(--line);
185+
}
186+
187+
.meta-row:last-child {
188+
border-bottom: none;
189+
padding-bottom: 0;
190+
}
191+
192+
.meta-row strong {
193+
font-family: var(--font-mono), monospace;
194+
font-size: 0.8rem;
195+
text-transform: uppercase;
196+
letter-spacing: 0.08em;
197+
}
198+
199+
@media (max-width: 720px) {
200+
.shell {
201+
width: min(100%, calc(100% - 1rem));
202+
padding-top: 1rem;
203+
}
204+
205+
.card {
206+
border-radius: 22px;
207+
padding: 1rem;
208+
}
209+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Metadata } from "next";
2+
import { IBM_Plex_Mono, Space_Grotesk } from "next/font/google";
3+
import "./globals.css";
4+
5+
const display = Space_Grotesk({
6+
variable: "--font-display",
7+
subsets: ["latin"],
8+
});
9+
10+
const mono = IBM_Plex_Mono({
11+
variable: "--font-mono",
12+
subsets: ["latin"],
13+
weight: ["400", "500"],
14+
});
15+
16+
export const metadata: Metadata = {
17+
title: "Open Brain Auth Portal",
18+
description: "Sign in and approve MCP connector access for your Open Brain.",
19+
};
20+
21+
export default function RootLayout({
22+
children,
23+
}: Readonly<{
24+
children: React.ReactNode;
25+
}>) {
26+
return (
27+
<html lang="en">
28+
<body className={`${display.variable} ${mono.variable}`}>{children}</body>
29+
</html>
30+
);
31+
}

0 commit comments

Comments
 (0)