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": "YOUR_ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "YOUR_REFRESH_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:


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 a partner API key with the partner:oauth-client-admin scope. The current CLI has no partner OAuth-client command:

bash
curl --fail-with-body -X POST "$BASE_URL/partner/oauth-clients" \
  -H "Authorization: Bearer $PARTNER_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "clientName": "My App",
    "clientType": "confidential",
    "redirectUris": ["https://yourapp.com/oauth/callback"],
    "grantTypes": ["authorization_code", "refresh_token"],
    "allowedScopes": ["execution:run", "wallet:read"]
  }'

Contact your Chainabit partner account manager to obtain an API key for client management. Keep that key out of shell history and source control.


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.