Skip to content

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

FeatureSupported
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

ScopeWhat it grants
execution:runTrigger AI executions on the user's behalf
wallet:readRead 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.

javascript
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=S256

Query Parameters

ParameterRequiredDescription
client_idYesYour OAuth client UUID, issued by Chainabit
redirect_uriYesMust exactly match a registered redirect URI
response_typeYesAlways code
scopeYesSpace-separated list of scopes
stateRecommendedRandom value you generate; returned unchanged in callback
code_challengeYesBASE64URL(SHA-256(code_verifier))
code_challenge_methodYesAlways 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

bash
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:

bash
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

FieldRequiredDescription
grant_typeYesauthorization_code
codeYesThe authorization code from the callback
redirect_uriYesMust exactly match the value used in Step 2
code_verifierYesThe original verifier you generated in Step 1
client_idConditionalRequired if not using HTTP Basic auth

Token Response

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "v1_base64url_opaque_token...",
  "scope": "execution:run wallet:read"
}
FieldTypeDescription
access_tokenstringJWT — send as Authorization: Bearer <token>
token_typestringAlways Bearer
expires_innumberSeconds until access token expires (3600)
refresh_tokenstringOpaque long-lived token — store securely server-side
scopestringSpace-separated list of granted scopes

Step 5 — Use the Access Token

Include the access token as a bearer token on Chainabit API requests:

bash
# 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.

bash
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:

bash
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:

bash
curl https://api.chainabit.com/api/v1/oauth/userinfo \
  -H "Authorization: Bearer <access-token>"

Response

The payload is deterministic for the granted scopes:

  • sub is always present
  • email and email_verified require email:read
  • name and preferred_username require profile:read
  • wallet data, internal account IDs, admin metadata, and security metadata are never exposed here
json
{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "email": "alice@example.com",
  "email_verified": true,
  "name": "Alice Smith",
  "preferred_username": "alice"
}
FieldTypeNotes
substringStable Chainabit user UUID — use as your user identifier
emailstringPresent only when email:read was granted
email_verifiedbooleanPresent only when email:read was granted
namestring | nullPresent only when profile:read was granted
preferred_usernamestring | nullPresent only when profile:read was granted

Error Reference

Token endpoint errors

HTTPErrorMeaning
400unsupported_grant_typegrant_type is not authorization_code or refresh_token
400invalid_grantCode expired, already used, redirect_uri mismatch, or PKCE verification failed
400invalid_requestMissing required parameter or malformed code_verifier
401invalid_clientClient authentication failed (wrong secret)

Authorization errors (redirect)

ErrorWhen
access_deniedUser 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 the state parameter 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:

bash
chainabit partner oauth-client create \
  --name "My App" \
  --type confidential \
  --redirect-uri https://yourapp.com/oauth/callback \
  --scope execution:run \
  --scope wallet:read

Contact 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-configuration

An identical document is also available at the RFC 8414 canonical path:

GET https://api.chainabit.com/.well-known/oauth-authorization-server

Both 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:

  1. Select Configuration Method: Auto-discovery.
  2. Set Issuer URL to https://api.chainabit.com (no path, no trailing slash).
  3. Supabase fetches https://api.chainabit.com/.well-known/openid-configuration automatically.
  4. 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_endpoint to verify the user's identity after the authorization code exchange. Register your OAuth client with profile:read and email:read scopes.

Discovery Document Fields

FieldValue
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_supportedemail:read, profile:read, execution:run, wallet:read
subject_types_supported["public"]
claims_supportedsub, email, email_verified, name, preferred_username

jwks_uri and id_token_signing_alg_values_supported are intentionally absent — Chainabit uses symmetric signing and does not issue ID tokens.

Built with purpose.