|
| 1 | +<div align="center"> |
| 2 | + |
1 | 3 | # `<CodexAuth />` |
2 | 4 |
|
3 | | -Drop a ChatGPT sign-in into any React app. The device-code OAuth flow, the |
4 | | -polling, the session handling: all headless, all yours to style. Your users |
5 | | -bring their own ChatGPT plan (Free, Plus, or Pro); you ship the button. |
| 5 | +### Login with ChatGPT, as a React component. |
6 | 6 |
|
7 | | -An open-source, headless-first React component + npm package. It uses the |
8 | | -official Codex CLI **device-code** flow, proxied through a small backend you |
9 | | -host. **OAuth tokens never touch the browser**: they stay in your backend's |
10 | | -session, the same way the Codex CLI keeps them in `~/.codex/auth.json`. |
| 7 | +Your users sign in with their own ChatGPT account and run prompts on it. |
| 8 | +**You never pay OpenAI** — they use their own plan. |
11 | 9 |
|
12 | | -``` |
13 | | -npm install codex-auth |
14 | | -``` |
| 10 | +[](https://github.com/AsyncFuncAI/CodexAuth/actions/workflows/ci.yml) |
| 11 | +[](https://www.npmjs.com/package/codex-auth) |
| 12 | +[](./LICENSE) |
| 13 | +[](https://react.dev) |
| 14 | + |
| 15 | +[Quick start](#quick-start) · [Two runners](#pick-a-runner) · [Headless](#headless-usage) · [Deploy](./DEPLOYMENT.md) · [Security](./SECURITY.md) |
| 16 | + |
| 17 | +> ⚠️ **For research & education only.** This uses OpenAI's Codex client off-label |
| 18 | +> and likely violates the ChatGPT Terms of Use. **You are solely responsible for |
| 19 | +> your use.** [Read the disclaimer ↓](#%EF%B8%8F-disclaimer--read-before-you-use-this) |
| 20 | +
|
| 21 | +</div> |
15 | 22 |
|
16 | | -> Requires React 17+. The reference backend requires Node and the official |
17 | | -> [`codex`](https://developers.openai.com/codex) CLI installed on the server. |
| 23 | +<p align="center"> |
| 24 | + <img src="./docs/screenshots/login.png" alt="CodexAuth login screen" width="900" /> |
| 25 | +</p> |
18 | 26 |
|
19 | 27 | --- |
20 | 28 |
|
21 | | -## Why this needs a backend (important) |
| 29 | +## What it is |
22 | 30 |
|
23 | | -`<CodexAuth>` is a **drop-in component for an app that has a backend** — not a |
24 | | -browser-only widget. Here's why, in one paragraph: |
| 31 | +A headless React component that adds a **"Login with ChatGPT"** button to your app. |
| 32 | +Click it, your user approves on `auth.openai.com`, and you can run prompts on their |
| 33 | +ChatGPT plan. The OAuth flow, the popup, the polling, the sessions — all handled. |
25 | 34 |
|
26 | | -The whole point is that your users authenticate with their **own ChatGPT |
27 | | -account** and you **never pay OpenAI**. That means real OAuth tokens for their |
28 | | -account exist somewhere. If those tokens lived in the browser, any XSS could |
29 | | -steal them and hijack the user's ChatGPT account. So the security model is |
30 | | -**tokens never touch the browser** — they live on a server you control, which |
31 | | -talks to OpenAI and runs the prompts. The browser only ever sees an `HttpOnly` |
32 | | -cookie and the streamed output. |
| 35 | +```bash |
| 36 | +npm install codex-auth |
| 37 | +``` |
| 38 | + |
| 39 | +**OAuth tokens never touch the browser.** They live on a backend you control; the |
| 40 | +browser only ever sees an `HttpOnly` cookie and the streamed output. |
| 41 | + |
| 42 | +<p align="center"> |
| 43 | + <img src="./docs/screenshots/console.png" alt="CodexAuth console running a prompt" width="900" /> |
| 44 | +</p> |
33 | 45 |
|
34 | | -This is exactly how the original login-with-chatgpt works (it runs a server-side |
35 | | -Codex process), and it's the same reason Clerk/Auth0/your-own-DB all need a |
36 | | -backend. **The component gives you the hard 80%** — the popup flow, device-code |
37 | | -UX, polling, sessions, and a security-hardened HTTP contract. **You bring a small |
38 | | -backend endpoint** (one of the drop-in adapters below). |
| 46 | +--- |
| 47 | + |
| 48 | +## Quick start |
39 | 49 |
|
40 | | -### Two runners — pick how the backend talks to OpenAI |
| 50 | +**1. Frontend** — drop in the component: |
41 | 51 |
|
42 | | -The component is the same; the **runner** decides how your backend reaches OpenAI: |
| 52 | +```tsx |
| 53 | +import { CodexAuth } from "codex-auth"; |
43 | 54 |
|
44 | | -| Runner | How it works | Deploys on | |
45 | | -|---|---|---| |
46 | | -| **`directRunner`** (`codex-auth/backend/direct`) | Pure `fetch` — the device-code grant + the Codex `responses` backend directly | **Anywhere, incl. Vercel serverless / edge.** No binary, no persistent process | |
47 | | -| **`defaultCliRunner`** (`codex-auth/backend`) | Shells out to the official `codex` CLI | A **persistent** Node host (Railway, Render, Docker, a VPS) — not Vercel functions | |
| 55 | +<CodexAuth onAuthenticated={({ account }) => console.log("hi", account)} /> |
| 56 | +``` |
48 | 57 |
|
49 | | -> `directRunner` is the easy default for most apps: it has no native dependency, so |
50 | | -> the whole thing (frontend + backend) can live in one Vercel/Next.js deploy. |
51 | | -> `defaultCliRunner` exists for when you'd rather lean on the installed CLI's auth. |
52 | | -> Both implement the same `CodexRunner` interface and hold tokens server-side. |
| 58 | +**2. Backend** — mount one route (Express shown; Next.js below): |
53 | 59 |
|
54 | 60 | ```ts |
55 | | -// serverless-friendly: no codex binary needed |
56 | 61 | import { createCodexRouter } from "codex-auth/backend"; |
57 | 62 | import { directRunner } from "codex-auth/backend/direct"; |
58 | 63 |
|
59 | | -createCodexRouter({ runner: directRunner(), cookieSecret: process.env.COOKIE_SECRET! }); |
| 64 | +app.use("/api/codex", createCodexRouter({ |
| 65 | + runner: directRunner(), |
| 66 | + cookieSecret: process.env.COOKIE_SECRET!, |
| 67 | +})); |
60 | 68 | ``` |
61 | 69 |
|
62 | | -See [`DEPLOYMENT.md`](./DEPLOYMENT.md) for Vercel, Railway (one-click), Docker, and |
63 | | -Next.js, and [`SECURITY.md`](./SECURITY.md) for the off-label-API caveats `directRunner` |
64 | | -inherits. |
65 | | - |
66 | | -Backend adapters (all share one hardened core, work with either runner): |
| 70 | +That's it. The component talks to `/api/codex/*` on the same origin by default. |
67 | 71 |
|
68 | | -| Import | For | |
69 | | -|---|---| |
70 | | -| `codex-auth/backend` | Express (`createCodexRouter`) — the generic reference | |
71 | | -| `codex-auth/backend/next` | Next.js App Router route handler | |
72 | | -| `codex-auth/backend/direct` | the serverless-friendly `directRunner` | |
73 | | -| `codex-auth/backend/worker` | Cloudflare Worker — proxy in front of a Node backend | |
| 72 | +> **One-time, per user:** they enable *device code authorization* in |
| 73 | +> **ChatGPT → Settings → Security & Login**. The component shows this hint |
| 74 | +> automatically when needed. |
74 | 75 |
|
75 | 76 | --- |
76 | 77 |
|
77 | | -## Quick start |
| 78 | +## Pick a runner |
78 | 79 |
|
79 | | -### 1. Frontend — drop in the component |
| 80 | +The component is always the same. The **runner** decides how your backend reaches OpenAI: |
80 | 81 |
|
81 | | -```tsx |
82 | | -import { CodexAuth } from "codex-auth"; |
| 82 | +| Runner | How | Deploys on | |
| 83 | +| --- | --- | --- | |
| 84 | +| **`directRunner`** | Pure `fetch` to OpenAI. No binary. | **Anywhere — incl. Vercel serverless / edge** | |
| 85 | +| **`defaultCliRunner`** | Shells out to the `codex` CLI. | A persistent Node host (Railway, Docker, VPS) | |
83 | 86 |
|
84 | | -export function SignIn() { |
85 | | - return <CodexAuth onAuthenticated={({ account }) => console.log("hi", account)} />; |
86 | | -} |
87 | | -``` |
| 87 | +Both implement the same interface and keep tokens server-side. **`directRunner` is |
| 88 | +the recommended default** — frontend + backend can live in one Vercel/Next.js deploy. |
88 | 89 |
|
89 | | -That renders the default "Login with ChatGPT" button, the device-code card, the |
90 | | -popup-blocked fallback, and an account console after login. |
| 90 | +### Backend adapters |
| 91 | + |
| 92 | +| Import | For | |
| 93 | +| --- | --- | |
| 94 | +| `codex-auth/backend` | Express — `createCodexRouter` | |
| 95 | +| `codex-auth/backend/next` | Next.js App Router route handler | |
| 96 | +| `codex-auth/backend/direct` | the serverless-friendly `directRunner` | |
| 97 | +| `codex-auth/backend/worker` | Cloudflare Worker proxy | |
91 | 98 |
|
92 | | -### 2. Backend — mount the reference router |
| 99 | +<details> |
| 100 | +<summary><b>Next.js — one file, zero config</b></summary> |
93 | 101 |
|
94 | 102 | ```ts |
95 | | -import express from "express"; |
96 | | -import { createCodexRouter, defaultCliRunner } from "codex-auth/backend"; |
97 | | - |
98 | | -const app = express(); |
99 | | -app.use( |
100 | | - "/api/codex", |
101 | | - createCodexRouter({ |
102 | | - runner: defaultCliRunner(), // shells out to the `codex` CLI |
103 | | - cookieSecret: process.env.COOKIE_SECRET!, // 32-byte random, from env |
104 | | - }), |
105 | | -); |
106 | | -app.listen(8787); |
107 | | -``` |
108 | | - |
109 | | -The component talks to `/api/codex/*` on the **same origin** by default. That's it. |
| 103 | +// app/api/codex/[...codex]/route.ts |
| 104 | +import { createNextCodexHandler } from "codex-auth/backend/next"; |
| 105 | +import { directRunner } from "codex-auth/backend/direct"; |
110 | 106 |
|
111 | | -### 3. Your users must enable device auth (one time) |
| 107 | +export const { GET, POST } = createNextCodexHandler({ |
| 108 | + runner: directRunner(), |
| 109 | + cookieSecret: process.env.COOKIE_SECRET!, |
| 110 | +}); |
| 111 | +``` |
112 | 112 |
|
113 | | -The first time, a user opens **ChatGPT → Settings → Security & Login** and enables |
114 | | -**device code authorization**. The component shows this hint automatically if the |
115 | | -backend reports `DEVICE_AUTH_NOT_ENABLED`. |
| 113 | +</details> |
116 | 114 |
|
117 | 115 | --- |
118 | 116 |
|
119 | | -## Headless usage (full control of the UI) |
| 117 | +## Headless usage |
120 | 118 |
|
121 | | -Pass a function as `children` to get the full auth state and render whatever you |
122 | | -want. No default UI is rendered. |
| 119 | +Pass a function as `children` for full control — no default UI is rendered: |
123 | 120 |
|
124 | 121 | ```tsx |
125 | 122 | <CodexAuth> |
126 | 123 | {(auth) => |
127 | | - auth.isAuthenticated ? ( |
128 | | - <button onClick={auth.logout}>Sign out {auth.account}</button> |
129 | | - ) : auth.isWaiting ? ( |
130 | | - <p>Enter code {auth.userCode} in the popup…</p> |
131 | | - ) : ( |
132 | | - <button onClick={auth.login}>Login with ChatGPT</button> |
133 | | - ) |
| 124 | + auth.isAuthenticated |
| 125 | + ? <button onClick={auth.logout}>Sign out {auth.account}</button> |
| 126 | + : <button onClick={auth.login}>Login with ChatGPT</button> |
134 | 127 | } |
135 | 128 | </CodexAuth> |
136 | 129 | ``` |
137 | 130 |
|
138 | | -`useCodexAuth()` exposes the same result if you prefer a hook: |
| 131 | +Run a prompt against the user's account once authenticated: |
139 | 132 |
|
140 | 133 | ```tsx |
141 | | -const auth = useCodexAuth({ pollIntervalMs: 2500 }); |
| 134 | +auth.run("Write a haiku about the ocean.", { |
| 135 | + onText: (text) => append(text), |
| 136 | + onDone: ({ text }) => console.log(text), |
| 137 | +}); |
142 | 138 | ``` |
143 | 139 |
|
144 | | -### Running prompts |
| 140 | +Prefer a hook? `useCodexAuth()` returns the same `auth` object. |
145 | 141 |
|
146 | | -Once authenticated, stream a prompt against the user's account: |
| 142 | +--- |
147 | 143 |
|
148 | | -```tsx |
149 | | -auth.run("Write a haiku about the ocean.", { |
150 | | - onText: (text, mode) => append(text, mode), |
151 | | - onDone: ({ text }) => console.log("final:", text), |
152 | | - onError: (e) => console.error(e), |
153 | | -}); |
| 144 | +## How it works |
| 145 | + |
| 146 | +``` |
| 147 | +Browser <CodexAuth/> Your backend OpenAI |
| 148 | + │ click login │ │ |
| 149 | + ├── open popup (sync) ─────────┤ │ |
| 150 | + ├── POST /session ────────────▶│ │ |
| 151 | + ├── POST /login/start ────────▶│── device code ───────▶ │ |
| 152 | + │ show code, poll status │◀── loginUrl + code ────│ |
| 153 | + │ user approves in popup ───────────────────────────▶ │ |
| 154 | + ├── GET /status (poll) ───────▶│◀── tokens (stay here) ─│ |
| 155 | + │ authenticated ✓ │ │ |
| 156 | + ├── POST /run/stream ─────────▶│── run prompt ────────▶ │ |
| 157 | + │ stream output ◀─────────────│◀── tokens never leave ─│ |
154 | 158 | ``` |
155 | 159 |
|
156 | | -Need just the run client without the auth UI? Import from `codex-auth/run`. |
| 160 | +The popup is opened **synchronously** on click (so the blocker allows it), then |
| 161 | +redirected to the validated `auth.openai.com` URL once the backend returns it. |
| 162 | +Full HTTP spec in [`CONTRACT.md`](./CONTRACT.md). |
157 | 163 |
|
158 | 164 | --- |
159 | 165 |
|
160 | | -## Configuration (`CodexClientConfig`) |
| 166 | +## Run the demo |
| 167 | + |
| 168 | +```bash |
| 169 | +git clone https://github.com/AsyncFuncAI/CodexAuth |
| 170 | +cd CodexAuth && npm install |
| 171 | +cp demo/.env.example demo/.env # set COOKIE_SECRET |
| 172 | +npm run demo # app → :5173, backend → :8787 |
| 173 | +``` |
| 174 | + |
| 175 | +--- |
| 176 | + |
| 177 | +## Reference |
| 178 | + |
| 179 | +<details> |
| 180 | +<summary><b><code>CodexClientConfig</code></b> (component / hook options)</summary> |
161 | 181 |
|
162 | 182 | | Option | Default | Purpose | |
163 | | -|---|---|---| |
| 183 | +| --- | --- | --- | |
164 | 184 | | `basePath` | `/api/codex` | Base path for the contract | |
165 | | -| `endpoints` | derived | Per-route overrides (absolute URL or relative path; replaces verbatim) | |
166 | | -| `fetch` | `globalThis.fetch` | Injectable fetch (SSR/tests) | |
167 | 185 | | `pollIntervalMs` | `3000` | Status poll cadence | |
168 | 186 | | `resumeMaxAgeMs` | `86_400_000` | Resume a persisted session within 24h | |
169 | | -| `storage` | `localStorage` | Persistence backend; `null` disables (SSR-safe) | |
170 | | -| `credentials` | `same-origin` | fetch credentials mode (see Cross-origin below) | |
171 | | -| `allowedLoginHosts` | `["auth.openai.com"]` | Hosts the login popup may navigate to | |
172 | | -| `enableGravatar` | `false` | Opt-in Gravatar avatar (leaks an email MD5 to gravatar.com) | |
| 187 | +| `storage` | `localStorage` | Persistence; `null` disables (SSR-safe) | |
| 188 | +| `credentials` | `same-origin` | fetch credentials mode | |
| 189 | +| `allowedLoginHosts` | `["auth.openai.com"]` | Hosts the login popup may open | |
| 190 | +| `enableGravatar` | `false` | Opt-in Gravatar avatar | |
173 | 191 |
|
174 | | -### `useCodexAuth()` result |
| 192 | +`useCodexAuth()` returns: `status`, `account`, `userCode`, `loginUrl`, `error`, |
| 193 | +`popupBlocked`, `isAuthenticated` / `isConnecting` / `isWaiting`, `avatarUrl`, and |
| 194 | +the actions `login()`, `logout()`, `cancelLogin()`, `run()`, `copyUserCode()`. |
175 | 195 |
|
176 | | -`status` (`idle` · `resuming` · `connecting` · `waitingForLogin` · `authenticated` · |
177 | | -`error` · `loggedOut`), `account`, `userCode`, `loginUrl`, `expiresAt`, `error`, |
178 | | -`popupBlocked`, derived `isAuthenticated`/`isConnecting`/`isWaiting`, `avatarUrl`, |
179 | | -and actions `login()`, `logout()`, `cancelLogin()`, `run()`, `copyUserCode()`, |
180 | | -`openLoginPage()`. |
| 196 | +</details> |
181 | 197 |
|
182 | | ---- |
183 | | - |
184 | | -## How it works (the popup trick) |
| 198 | +<details> |
| 199 | +<summary><b>Cross-origin backends</b></summary> |
185 | 200 |
|
186 | | -1. On click, the component opens a **blank popup synchronously** — before any |
187 | | - `await` — so the browser's popup blocker permits it, and shows a "Connecting…" |
188 | | - holding screen. |
189 | | -2. It calls your backend (`POST /session`, `POST /login/start`), which runs the |
190 | | - Codex CLI device-code flow and returns a `loginUrl` + `userCode`. |
191 | | -3. The popup is pointed at the (validated) `loginUrl` on `auth.openai.com`. The |
192 | | - user approves; the component polls `GET /status` every few seconds. |
193 | | -4. On success it shows the account. **Tokens stay on your backend the whole time.** |
| 201 | +When the app and backend are on different origins, set `credentials: 'include'` |
| 202 | +on the client and pass `allowedOrigins: ['https://your-app.example']` to the |
| 203 | +router (it emits credentialed CORS for that specific origin, never `*`). |
194 | 204 |
|
195 | | -See [`CONTRACT.md`](./CONTRACT.md) for the exact HTTP contract (so you can write a |
196 | | -backend in any language) and [`SECURITY.md`](./SECURITY.md) for the trust model. |
| 205 | +</details> |
197 | 206 |
|
198 | 207 | --- |
199 | 208 |
|
200 | | -## Run the demo |
| 209 | +## ⚠️ Disclaimer — read before you use this |
201 | 210 |
|
202 | | -``` |
203 | | -cp demo/.env.example demo/.env # set COOKIE_SECRET |
204 | | -npm install |
205 | | -npm run demo # backend on :8787, app on :5173 |
206 | | -``` |
| 211 | +> **This project is for education and research. You alone are responsible for how you use it.** |
207 | 212 |
|
208 | | -You need the `codex` CLI installed and `codex login --device-auth` available. |
| 213 | +`<CodexAuth>` reuses OpenAI's **first-party Codex CLI** OAuth client and talks to |
| 214 | +undocumented Codex endpoints off-label. **This very likely violates OpenAI's Terms |
| 215 | +of Use.** OpenAI's ChatGPT plans explicitly prohibit, among other things: |
209 | 216 |
|
210 | | ---- |
| 217 | +> - *Abusive usage, such as automatically or programmatically extracting data.* |
| 218 | +> - *Sharing your account credentials or making your account available to anyone else.* |
| 219 | +> - **Reselling access or using ChatGPT to power third-party services.** |
| 220 | +> |
| 221 | +> — [OpenAI Help Center, "Access and usage allowances"](https://help.openai.com/en/articles/8983719-what-is-chatgpt-pro) |
211 | 222 |
|
212 | | -## Cross-origin backends |
| 223 | +Using this to power an app for other users falls squarely under that last point. |
213 | 224 |
|
214 | | -Default `credentials` is `same-origin` — keep it that way when the app and backend |
215 | | -share an origin (recommended). For a backend on a **different** origin: |
| 225 | +**By using this software, you acknowledge that:** |
216 | 226 |
|
217 | | -- set `credentials: 'include'` on the client, **and** |
218 | | -- pass `allowedOrigins: ['https://your-app.example']` to `createCodexRouter` so it |
219 | | - emits credentialed CORS for that **specific** origin (never `*`). |
| 227 | +- You are **solely responsible** for ensuring your use complies with OpenAI's |
| 228 | + [Terms of Use](https://openai.com/policies/row-terms-of-use/) and Usage Policies. |
| 229 | +- It may get **your account, your users' accounts, or the shared OAuth client |
| 230 | + rate-limited, suspended, or banned**, at OpenAI's discretion. |
| 231 | +- The undocumented endpoints can **change or break at any time** with no notice. |
| 232 | +- The authors provide this **as-is, with no warranty**, and accept **no liability** |
| 233 | + for any consequence of its use (see the [MIT License](./LICENSE)). |
220 | 234 |
|
221 | | -Never reflect an arbitrary `Origin` with credentials — see `SECURITY.md`. |
| 235 | +**Do not build a product or business on this.** For anything real, use the |
| 236 | +[official OpenAI API](https://platform.openai.com) with your own key, or have each |
| 237 | +user bring their own key. See [`SECURITY.md`](./SECURITY.md) for the full trust model. |
222 | 238 |
|
223 | 239 | --- |
224 | 240 |
|
225 | | -## License |
| 241 | +<div align="center"> |
| 242 | + |
| 243 | +**MIT** · Not affiliated with or endorsed by OpenAI · For research & education only |
226 | 244 |
|
227 | | -MIT. Not affiliated with OpenAI. |
| 245 | +</div> |
0 commit comments