OAuth 2.0 Provider Guide
Chainabit acts as an OAuth 2.0 Authorization Server (AS). Your application can request delegated access to Chainabit resources on behalf of a user — for example, triggering AI runs or reading wallet balances — without ever handling the user's Chainabit credentials.
Supported Capabilities
| Feature | Supported |
|---|---|
| Authorization Code + PKCE | ✅ |
| Refresh token rotation | ✅ |
Token revocation (/oauth/revoke) | ✅ |
OpenID Connect UserInfo (/oauth/userinfo) | ✅ |
OIDC Auto-Discovery (/.well-known/openid-configuration) | ✅ |
| Implicit flow | ❌ |
| Client credentials | ❌ |
| Device code | ❌ |
plain PKCE | ❌ |
PKCE with S256 is mandatory for all clients, including confidential ones.
Scopes
| Scope | What it grants |
|---|---|
execution:run | Trigger AI executions on the user's behalf |
wallet:read | Read the user's Chainabit wallet balance |
Request only the scopes your application actually needs.
Full Authorization Flow
Step 1 — Generate PKCE Parameters
Generate a code_verifier and derive the code_challenge before redirecting the user.
import { createHash, randomBytes } from 'crypto';
function generatePKCE() {
const codeVerifier = randomBytes(48).toString('base64url'); // 64 chars
const codeChallenge = createHash('sha256')
.update(codeVerifier)
.digest('base64url');
return { codeVerifier, codeChallenge };
}
const { codeVerifier, codeChallenge } = generatePKCE();
const state = randomBytes(16).toString('hex'); // CSRF state
// Store codeVerifier and state securely — session, cookie, etc.Step 2 — Redirect to Authorization Endpoint
GET https://api.chainabit.com/api/v1/oauth/authorize
?client_id=<your-client-id>
&redirect_uri=https://yourapp.com/oauth/callback
&response_type=code
&scope=execution:run%20wallet:read
&state=<random-state>
&code_challenge=<base64url-sha256-of-verifier>
&code_challenge_method=S256Query Parameters
| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your OAuth client UUID, issued by Chainabit |
redirect_uri | Yes | Must exactly match a registered redirect URI |
response_type | Yes | Always code |
scope | Yes | Space-separated list of scopes |
state | Recommended | Random value you generate; returned unchanged in callback |
code_challenge | Yes | BASE64URL(SHA-256(code_verifier)) |
code_challenge_method | Yes | Always S256 |
Chainabit displays a login page to the user. The user authenticates (email+password or magic link) and then reviews the consent page showing which scopes your application is requesting.
Step 3 — Handle the Callback
After the user approves, Chainabit redirects to your redirect_uri:
https://yourapp.com/oauth/callback?code=<authorization-code>&state=<your-state>Always verify state matches what you sent to prevent CSRF.
If the user denies:
https://yourapp.com/oauth/callback?error=access_denied&state=<your-state>Step 4 — Exchange Code for Tokens
curl -X POST https://api.chainabit.com/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "<client-id>:<client-secret>" \
-d "grant_type=authorization_code" \
-d "code=<authorization-code>" \
-d "redirect_uri=https://yourapp.com/oauth/callback" \
-d "code_verifier=<your-code-verifier>"For public clients (no secret), omit the -u flag and pass client_id in the body:
curl -X POST https://api.chainabit.com/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=<client-id>" \
-d "code=<authorization-code>" \
-d "redirect_uri=https://yourapp.com/oauth/callback" \
-d "code_verifier=<your-code-verifier>"Request Body
| Field | Required | Description |
|---|---|---|
grant_type | Yes | authorization_code |
code | Yes | The authorization code from the callback |
redirect_uri | Yes | Must exactly match the value used in Step 2 |
code_verifier | Yes | The original verifier you generated in Step 1 |
client_id | Conditional | Required if not using HTTP Basic auth |
Token Response
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "v1_base64url_opaque_token...",
"scope": "execution:run wallet:read"
}| Field | Type | Description |
|---|---|---|
access_token | string | JWT — send as Authorization: Bearer <token> |
token_type | string | Always Bearer |
expires_in | number | Seconds until access token expires (3600) |
refresh_token | string | Opaque long-lived token — store securely server-side |
scope | string | Space-separated list of granted scopes |
Step 5 — Use the Access Token
Include the access token as a bearer token on Chainabit API requests:
# Read wallet balance (requires wallet:read scope)
curl https://api.chainabit.com/api/v1/wallet/me \
-H "Authorization: Bearer <access-token>"
# Trigger an AI run (requires execution:run scope)
curl -X POST https://api.chainabit.com/api/v1/features/chat/runs \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "Hello!"}]}'Step 6 — Refresh the Access Token
Access tokens expire after 3600 seconds. Use the refresh token to obtain a new one.
curl -X POST https://api.chainabit.com/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "<client-id>:<client-secret>" \
-d "grant_type=refresh_token" \
-d "refresh_token=<current-refresh-token>"Response
Same shape as the initial token response — a new refresh_token is always returned. Discard the old refresh token immediately and store the new one.
Refresh Token Rotation
Chainabit rotates refresh tokens on every use. If you attempt to reuse a consumed refresh token, Chainabit revokes the entire token family, invalidating all tokens for that grant. You must then restart the authorization flow.
Revoke a Token
Revoke an access or refresh token when the user disconnects your application:
curl -X POST https://api.chainabit.com/api/v1/oauth/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "<client-id>:<client-secret>" \
-d "token=<token-to-revoke>" \
-d "token_type_hint=refresh_token"Revoking a refresh token also revokes its entire rotation family. The endpoint always returns 200 OK, even for unknown tokens (per RFC 7009).
UserInfo Endpoint
Retrieve basic profile data for the authenticated user:
curl https://api.chainabit.com/api/v1/oauth/userinfo \
-H "Authorization: Bearer <access-token>"Response
The payload is deterministic for the granted scopes:
subis always presentemailandemail_verifiedrequireemail:readnameandpreferred_usernamerequireprofile:read- wallet data, internal account IDs, admin metadata, and security metadata are never exposed here
{
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "[email protected]",
"email_verified": true,
"name": "Alice Smith",
"preferred_username": "alice"
}| Field | Type | Notes |
|---|---|---|
sub | string | Stable Chainabit user UUID — use as your user identifier |
email | string | Present only when email:read was granted |
email_verified | boolean | Present only when email:read was granted |
name | string | null | Present only when profile:read was granted |
preferred_username | string | null | Present only when profile:read was granted |
Error Reference
Token endpoint errors
| HTTP | Error | Meaning |
|---|---|---|
400 | unsupported_grant_type | grant_type is not authorization_code or refresh_token |
400 | invalid_grant | Code expired, already used, redirect_uri mismatch, or PKCE verification failed |
400 | invalid_request | Missing required parameter or malformed code_verifier |
401 | invalid_client | Client authentication failed (wrong secret) |
Authorization errors (redirect)
| Error | When |
|---|---|
access_denied | User explicitly denied the authorization request |
Security Best Practices
- HTTPS only — Use HTTPS for all redirect URIs and token exchanges in production.
- Never log tokens — Do not log authorization codes, access tokens, refresh tokens, PKCE verifiers, or client secrets.
- Store refresh tokens server-side — Treat them as long-lived secrets; never expose them to browser JavaScript.
- Verify
state— Always validate thestateparameter in your callback handler. - Minimal scope — Request only the scopes you actually use.
- Rotate immediately — Store the new refresh token as soon as you receive it; never attempt to reuse a consumed token.
Client Registration
OAuth clients are provisioned by Chainabit partner admins using the CLI:
chainabit partner oauth-client create \
--name "My App" \
--type confidential \
--redirect-uri https://yourapp.com/oauth/callback \
--scope execution:run \
--scope wallet:readContact your Chainabit partner account manager to get partner API key access for client management.
Auto-Discovery (OIDC / RFC 8414)
Chainabit exposes a standard Authorization Server Metadata document. Clients that support auto-discovery can point at the issuer URL and retrieve all endpoint addresses automatically — no manual endpoint configuration needed.
Discovery Document URLs
GET https://api.chainabit.com/.well-known/openid-configurationAn identical document is also available at the RFC 8414 canonical path:
GET https://api.chainabit.com/.well-known/oauth-authorization-serverBoth endpoints are public, cacheable (Cache-Control: public, max-age=3600), and return Access-Control-Allow-Origin: *.
Supabase Custom Auth — Auto-Discovery Mode
When configuring Chainabit as a Custom Auth Provider in Supabase:
- Select Configuration Method: Auto-discovery.
- Set Issuer URL to
https://api.chainabit.com(no path, no trailing slash). - Supabase fetches
https://api.chainabit.com/.well-known/openid-configurationautomatically. - All endpoint URLs (authorize, token, userinfo, revoke) are populated from the discovery document.
Chainabit issues OAuth 2.0 access tokens, not OIDC ID tokens. Supabase uses the
userinfo_endpointto verify the user's identity after the authorization code exchange. Register your OAuth client withprofile:readandemail:readscopes.
Discovery Document Fields
| Field | Value |
|---|---|
response_types_supported | ["code"] |
grant_types_supported | ["authorization_code", "refresh_token"] |
token_endpoint_auth_methods_supported | ["client_secret_basic", "none"] |
code_challenge_methods_supported | ["S256"] |
scopes_supported | email:read, profile:read, execution:run, wallet:read |
subject_types_supported | ["public"] |
claims_supported | sub, email, email_verified, name, preferred_username |
jwks_uriandid_token_signing_alg_values_supportedare intentionally absent — Chainabit uses symmetric signing and does not issue ID tokens.