OAuth 2.1 for ChatGPT, Claude & custom connectors
Onboard implements the standard flow that ChatGPT's Custom Connectors and Claude.ai's Custom Connectors run automatically — Discovery, Dynamic Client Registration, PKCE-S256.
Consumer apps like ChatGPT and Claude.ai let users add their own MCP servers as a Custom Connector. For the connection dialog to work without manual setup, the server has to implement a small stack of standards — RFC 8414, RFC 9728, RFC 7591, PKCE-S256. Onboard ships exactly that stack.
RFC 7591 — the relying-party client registers itself with the authorization server without anyone provisioning it manually beforehand. The prerequisite for ChatGPT/Claude to connect on the fly.
Endpoints at a glance
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| /.well-known/oauth-authorization-server | GET | — | — | Authorization server metadata (RFC 8414). Points at the registration, authorization and token endpoints; declares PKCE-S256 as the only code-challenge method. |
| /.well-known/oauth-protected-resource | GET | — | — | Protected-resource metadata (RFC 9728). Returned by the MCP server in a |
| /api/oauth/register | POST | — | — | Dynamic Client Registration. Public endpoint, mints a client_id per relying party — no client_secret (public clients). |
| /oauth/authorize | GET | — | — | User-facing consent page. Approve / Deny. NOT under /api/ because it is UI. |
| /oauth/authorize/grant | POST | — | — | Approve submission. Mints an authorization code (10-min TTL, PKCE-S256) and 303-redirects back to the redirect_uri. |
| /api/oauth/token | POST | — | — | Token exchange (RFC 6749 §4.1.3). Validates PKCE, burns the code (single-use) and stores the bearer in ai_api_keys. |
The full flow
- 1
Connector discovery (no code needed)
The user adds a connector URL inside ChatGPT or Claude.ai (typicallyhttps://app.onboardos.org/api/mcp). The client hits that endpoint, gets a 401 with aWWW-Authenticate: Bearer resource_metadata=…header, follows the resource metadata and discovers the authorization server from there. - 2
Dynamic client registration
The client POSTs to/api/oauth/registerwith redirect_uri, client_name and (optionally) a logo. The response carries aclient_id, persisted in oauth_clients withregistered_via='dcr'. - 3
User consent
The client opens/oauth/authorizein the user's browser (with PKCE challenge + state). The consent page shows: connector name, requested permissions, optional contact-email field (volunteered by the user for anonymous bookings). After Approve, Onboard 303-redirects to the redirect_uri with code + state. - 4
Token exchange with PKCE
The client POSTs code + code_verifier + redirect_uri to/api/oauth/token. Onboard recomputes the S256 hash from the verifier, burns the code in a conditional UPDATE (race-safe) and responds with aaccess_token(Bearer). The token lands as an API key in ai_api_keys with the oauth_client_id linked —verifyAiApiKey()validates it transparently, no special code path in the MCP handler. - 5
Calling tools
The connector now attachesAuthorization: Bearer <token>to every MCP call. Rate-limiting, audit logging and source attribution behave identically to directly-issued keys.
ai_api_keys table, with the same rate-limit window, the same revocation flow and the same CEO dashboard. Revoking an oauth_clients row revokes the issued tokens.No refresh tokens
Onboard does not issue refresh tokens — the discovery doc does not advertise any. Bearer tokens are long-lived until revoked. On revocation, clients re-run the flow — which ChatGPT and Claude do automatically.
PKCE-S256 only, no plain codes
The authorization server accepts only code_challenge_method=S256. Plain PKCE and non-PKCE flows are rejected with 400 — so even public clients without a secret cannot slip into a code-interception gap.
Consent-page behavior
The consent page only shows Approve / Deny and — optionally — a contact-email field for anonymous bookings. There is deliberately no login form. Most connector flows come through ChatGPT/Claude and identify the guest per booking via the create_reservation arguments.
/.well-known/ responses aggressively. If endpoints move, existing connectors need a full reconnect — so only change them in a major release.