Login with ChatGPT
SDK for "sign in with your ChatGPT account". Users authenticate through OpenAI's device flow; your handler keeps the tokens and proxies Responses-style calls to the ChatGPT-backed Codex endpoint. The browser only ever holds an HttpOnly session cookie.
Rules: read before writing code
- There is no API key. Auth comes from each user's ChatGPT session.
Never add
OPENAI_API_KEY, never callapi.openai.comdirectly for this flow. Requests go through the app's own/api/chatgpt/responsesproxy or a server route built onauth.proxyFetch(request). - Discover before selecting a model. Availability is per account and
plan. Call
await chatgpt.listModels()(browser) orawait auth.getModels(request)(server), then pick from that result. A hardcodedallowedModelsguardrail is fine; assuming one model exists for every signed-in account is not. - Tokens stay inside the handler by default. Don't build endpoints that
return tokens to the client. Normal app code should use
/responses,/models, orauth.proxyFetch(request). Raw token export requires the explicitdangerouslyAllowTokenExportescape hatch. - Consent cannot be removed. The widget always shows a consent step
before OpenAI's verification page. Custom UIs must render equivalent
consent before calling
login(). - Production needs a stable
secretand a sharedsessionStore. The defaults (ephemeral secret, in-memory store) log everyone out on restart and break across serverless instances.
Packages
| Package | Use for |
|---|---|
@opencoredev/loginwithchatgpt-server |
createChatGPTHandler() for login, session, logout, models, and the streaming proxy |
@opencoredev/loginwithchatgpt-react |
<LoginWithChatGPT /> button, useLoginWithChatGPT() hook |
@opencoredev/loginwithchatgpt-ai |
Vercel AI SDK providers (ai + @ai-sdk/openai are peer deps) |
@opencoredev/loginwithchatgpt-core |
Low-level OAuth/device flow, errors, and types; rarely imported directly |
bun add @opencoredev/loginwithchatgpt-server @opencoredev/loginwithchatgpt-react @opencoredev/loginwithchatgpt-ai ai @ai-sdk/openai
Server handler
One handler owns everything under basePath (default /api/chatgpt). It is
written against Web-standard Request, Response, fetch, and
crypto.subtle, so use it in runtimes that provide those APIs.
// Next.js: app/api/chatgpt/[...lwc]/route.ts
import { createChatGPTHandler } from "@opencoredev/loginwithchatgpt-server";
const auth = createChatGPTHandler({
secret: process.env.LWC_SECRET, // openssl rand -hex 32
responsesProxy: {
allowedModels: ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"],
},
});
export const GET = (request: Request) => auth.handler(request);
export const POST = (request: Request) => auth.handler(request);
// Bun
Bun.serve({
routes: {
"/": index,
"/api/chatgpt/*": (req) => auth.handler(req),
},
});
Key options: basePath, secret, sessionStore (any
get/set/delete key-value store), cookieName, cookie,
sessionTtlMs (30 days), defaultModel ("gpt-5.5"),
enableResponsesProxy (false also disables /models), responsesProxy
(allowedModels, maxRequestBytes 40 MiB, rateLimit default
30/min/session), allowedOrigins (cross-origin CSRF allowlist).
Server helpers on the returned handler (all read the session cookie):
auth.getSession(request)→{ status, user? }; no upstream call.auth.proxyFetch(request)→ request-scoped fetch for custom server AI routes without exposing raw bearer tokens.auth.getModels(request)→ account's model slugs orundefined.auth.dangerouslyGetTokens(request)→ raw-token escape hatch; requiresdangerouslyAllowTokenExport.
React sign-in
"use client";
import { LoginWithChatGPT } from "@opencoredev/loginwithchatgpt-react";
<LoginWithChatGPT
consent={{ appName: "Acme" }}
=> console.log("connected", user?.email)}
/>;
The widget handles the full flow: consent popup → OpenAI verification →
code copy → polling → signed-in chip with Disconnect. Restyle via the
injected .lwc-* classes, or pass a children render function for a fully
custom UI (then render your own consent and use
openLoginWithChatGPTConsentPopup()).
For custom UIs, useLoginWithChatGPT({ basePath?, pollIntervalMs?, ... })
returns { status, user, userCode, verificationUrl, login, logout, copyCode, reopen, isAuthenticated, isPending }. status is one of
loading | unauthenticated | connecting | pending | authenticated | expired | error.
Streaming with the AI SDK
Browser proxy provider, with credentials injected server-side from the cookie:
import { createChatGPTProxyProvider } from "@opencoredev/loginwithchatgpt-ai";
import { streamText } from "ai";
const chatgpt = createChatGPTProxyProvider(); // { basePath } if not /api/chatgpt
const models = await chatgpt.listModels(); // throws ChatGPTProxyError; status 401 = signed out
const model = models.includes("gpt-5.5") ? "gpt-5.5" : models[0];
const result = streamText({ model: chatgpt(model), prompt });
Server proxy provider for your own AI route:
import { createChatGPTProxyProvider } from "@opencoredev/loginwithchatgpt-ai";
import { streamText } from "ai";
export async function POST(request: Request) {
const chatgpt = createChatGPTProxyProvider({
fetch: auth.proxyFetch(request),
});
const { prompt } = await request.json();
return streamText({ model: chatgpt(), prompt }).toUIMessageStreamResponse();
}
Models support streamText, generateText, tool calling, structured
output, and file attachments via standard AI SDK messages. Images are a
first-class provider API: use chatgpt.images.generate() for prompt-to-image
and chatgpt.images.edit() for edits, multiple references, masks, input
fidelity, custom size, quality, format, compression, background, multiple
outputs, and partial-image callbacks. Both use the signed-in user's ChatGPT
plan through /responses; do not add an API key. Embeddings and audio are not
provided.
Per-request tuning headers on POST /responses:
x-login-with-chatgpt-reasoning-effort (none|low|medium|high|xhigh) and
x-login-with-chatgpt-service-tier (auto|default|flex|priority|fast).
HTTP routes (relative to basePath)
| Route | Purpose |
|---|---|
POST /login |
Start device login → { status: "pending", userCode, verificationUrl, interval, expiresAt } |
GET /status |
Advance login by one poll, return state |
GET /session |
Cheap state read, never polls upstream |
POST /logout |
Delete session, clear cookie |
GET /models |
Account's model slugs (401 when signed out) |
POST /responses |
Streaming Responses-style proxy |
Non-GET routes enforce Origin-based CSRF: same-origin or allowedOrigins
only. Split frontend/backend deployments also need SameSite=None cookies,
credentialed fetches, and CORS headers (see the cross-origin guide).
Production checklist
- Set
LWC_SECRET(stable across deploys; rotation logs everyone out). - Use a shared
sessionStore(Redis/DB).MemoryStoreis dev-only. - Set
responsesProxy.allowedModels; pass a sharedrateLimit.storewhen running multiple instances. - HTTPS with
X-Forwarded-Protoforwarded so the cookie getsSecure. - Log
/responsesmetadata (session id, model, status, duration), never prompts or attachments.
Errors
ChatGPTAuthError (from core) has .code, .status, .body. Notable
codes: refresh_token_invalid (session dead; handler deletes it and
reports expired; detect with isRefreshTokenInvalid(error)),
not_authenticated, token_refresh_failed (retryable),
models_request_failed. The browser provider's listModels() throws
ChatGPTProxyError with .status. HTTP errors from /responses:
401 not_authenticated, 403 model_not_allowed / origin_not_allowed,
413 responses_request_too_large, 429 rate_limited (+ retry-after).
Docs
Full docs live in the repo under docs/content/docs/ (quickstart,
concepts/security, guides/production, reference/*). The docs site also
serves /llms.txt and per-page markdown at
/llms.mdx/docs/<path>/content.md.