OAuth connect flow

Start browser or programmatic authorization, process callbacks, and redeem short-lived exchange codes.

PrerequisitesOAuth Apps overview
sequenceDiagram
    participant Backend as Customer backend
    participant Browser as End-user browser
    participant Cliodot as Cliodot OAuth Apps
    participant IdP as Identity provider

    Backend->>Browser: Redirect to connect URL
    Browser->>Cliodot: GET/POST /oauth/apps/{appId}/connect
    alt authorization_code / OIDC / SAML redirect
        Cliodot->>Browser: 302 to IdP authorization URL
        Browser->>IdP: User consents
        IdP->>Cliodot: GET/POST /oauth/callback/{appId}/{provider}
        Cliodot->>Cliodot: Exchange code, store connection
        Cliodot->>Browser: 302 to customer redirect_uri ?code=ex_...&connection_id=conn_...
    else client_credentials / assertion grants
        Cliodot->>Cliodot: Server-side token exchange
        Cliodot->>Browser: 302 or JSON with exchange_code
    else device_code grant
        Cliodot->>Browser: JSON with device_code + poll_state
        loop Until authorized
            Backend->>Cliodot: POST /oauth/apps/{appId}/connect/poll
        end
        Cliodot->>Backend: exchange_code + connection_id
    end
    Browser->>Backend: Callback with code=ex_...&connection_id=conn_...
    Backend->>Cliodot: POST /oauth/token/exchange (app credentials)
    Cliodot->>Backend: Connection metadata (+ tokens per policy)
    Backend->>Cliodot: GET /oauth/connections/{connectionId}/token (as needed)

Summary of steps:

  1. Customer backend initiates connect via GET or POST /oauth/apps/:appId/connect.
  2. For browser grants, Cliodot redirects to the IdP; the IdP callbacks to /oauth/callback/:appId/:provider.
  3. For immediate grants (client_credentials, JWT bearer, token exchange), Cliodot returns an exchange_code in the response or redirect.
  4. For device flow, poll POST /oauth/apps/:appId/connect/poll until complete.
  5. Customer backend calls POST /oauth/token/exchange with app credentials to redeem the exchange code.


Provider configurations store oauth.grant_type. The connect response shape depends on the grant type.

Grant type Value Connect behavior
Authorization Code authorization_code (default) Browser redirect to authorization_url → IdP callback → exchange code
Client Credentials client_credentials Immediate server-side token exchange; returns exchange_code + redirect_url
Device Authorization urn:ietf:params:oauth:grant-type:device_code Returns device codes + poll_state; poll via POST /connect/poll
JWT Bearer urn:ietf:params:oauth:grant-type:jwt-bearer Requires assertion on connect; immediate token exchange
SAML2 Bearer urn:ietf:params:oauth:grant-type:saml2-bearer Requires assertion on connect; immediate token exchange
Token Exchange urn:ietf:params:oauth:grant-type:token-exchange Requires subject_token on connect; optional actor/audience fields

Connect response variants (POST)#

Authorization code (default):

{
  "ok": true,
  "protocol": "oauth2",
  "grant_type": "authorization_code",
  "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=..."
}

Client credentials (immediate):

{
  "ok": true,
  "protocol": "oauth2",
  "grant_type": "client_credentials",
  "exchange_code": "ex_a1b2c3d4e5f6789012345678901234567890ab",
  "connection_id": "conn_507f1f77bcf86cd799439014",
  "redirect_url": "https://friendsconnect.com/auth/callback?code=ex_a1b2c3d4...&connection_id=conn_507f1f77bcf86cd799439014&state=csrf-token-from-your-app"
}

Device authorization:

{
  "ok": true,
  "protocol": "oauth2",
  "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
  "device_code": "GmRhmhcxhwAz30EkNva2BFGrNP0TOKsUyIT0lHDg-pQ",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://example.com/device",
  "verification_uri_complete": "https://example.com/device?user_code=WDJB-MJHT",
  "poll_interval": 5,
  "expires_in": 600,
  "poll_state": "a1b2c3d4e5f6789012345678901234567890abcdef"
}

SAML (POST binding):

{
  "ok": true,
  "protocol": "saml",
  "authorization_url": "https://idp.example.com/sso",
  "sso_binding": "post",
  "saml_request": "PHNhbWxwOlJlcXVlc3Q...",
  "relay_state": "relay-123"
}

GET /connect returns 302 redirects for browser flows. POST-only grant types return 400 on GET:

{
  "ok": false,
  "message": "This grant type requires POST /connect",
  "grant_type": "urn:ietf:params:oauth:grant-type:device_code"
}


Start Connect (GET — browser redirect)

Method GET
Path /oauth/apps/:appId/connect
Auth None (rate limited)

Description: Start an OAuth flow via browser redirect to the identity provider.

Query parameters:

Param Required Description
provider Yes Provider alias (e.g. google)
redirect_uri Yes Customer callback URL (must be in app allowlist)
scope No Space-delimited OAuth scopes
state No CSRF token returned on customer callback
external_user_id No Your app's user key stored on the connection
connection_storage No persistent or ephemeral

Sample success response (302 — authorization code):

Location: https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=...

Sample success response (302 — immediate grant):

Location: https://friendsconnect.com/auth/callback?code=ex_a1b2c3d4...&connection_id=conn_507f1f77bcf86cd799439014&state=csrf-token-from-your-app

Common errors: OAUTH_APP_NOT_FOUND, OAUTH_APP_DISABLED, OAUTH_REDIRECT_URI_NOT_ALLOWED, OAUTH_PROVIDER_NOT_CONFIGURED, OAUTH_SCOPE_NOT_ALLOWED, POST-required grant (400)

cURL:

curl -v "https://api.cliodot.com/oauth/apps/oauth_app_507f1f77bcf86cd799439011/connect?provider=google&redirect_uri=https%3A%2F%2Ffriendsconnect.com%2Fauth%2Fcallback&scope=openid%20email%20profile&state=csrf-token-from-your-app&external_user_id=user_12345"

Start Connect (POST — programmatic)

Method POST
Path /oauth/apps/:appId/connect
Auth None (rate limited)

Description: Start an OAuth flow programmatically. Response shape depends on provider grant_type (see Grant Types and Connect Responses).

Request body:

{
  "provider": "google",
  "redirect_uri": "https://friendsconnect.com/auth/callback",
  "scopes": ["openid", "email", "profile"],
  "state": "csrf-token-from-your-app",
  "external_user_id": "user_12345"
}

Additional fields by grant type:

Grant type Extra fields
JWT bearer assertion
SAML2 bearer assertion
Token exchange subject_token, subject_token_type, audience, actor_token, actor_token_type, requested_token_type

Sample success response (200 — authorization code):

{
  "ok": true,
  "protocol": "oauth2",
  "grant_type": "authorization_code",
  "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=..."
}

Common errors: Same as GET connect

cURL:

curl -X POST "https://api.cliodot.com/oauth/apps/oauth_app_507f1f77bcf86cd799439011/connect" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "google",
    "redirect_uri": "https://friendsconnect.com/auth/callback",
    "scopes": ["openid", "email", "profile"],
    "state": "csrf-token-from-your-app",
    "external_user_id": "user_12345"
  }'

Connect Poll (Device Flow)#

Method POST
Path /oauth/apps/:appId/connect/poll
Auth None (rate limited)

Description: Poll device authorization after a device_code connect. Retry on 428 until an exchange_code is returned.

Request body:

{
  "provider": "cli-tv",
  "poll_state": "a1b2c3d4e5f6789012345678901234567890abcdef"
}

Sample success response (200):

{
  "ok": true,
  "protocol": "oauth2",
  "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
  "exchange_code": "ex_a1b2c3d4e5f6789012345678901234567890ab",
  "connection_id": "conn_507f1f77bcf86cd799439014",
  "redirect_url": "https://friendsconnect.com/auth/callback?code=ex_a1b2c3d4...&connection_id=conn_507f1f77bcf86cd799439014&state=csrf-token-from-your-app"
}

Pending response (428):

{
  "ok": false,
  "error": "authorization_pending",
  "code": "OAUTH_PROVIDER_ERROR"
}

Wait poll_interval seconds (from the connect response) and retry.

Common errors: OAUTH_STATE_INVALID, OAUTH_PROVIDER_ERROR

cURL:

curl -X POST "https://api.cliodot.com/oauth/apps/oauth_app_507f1f77bcf86cd799439011/connect/poll" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "cli-tv",
    "poll_state": "a1b2c3d4e5f6789012345678901234567890abcdef"
  }'

Callback#

Called by the identity provider after user authorization. Do not call these endpoints directly from your application.

OAuth2/OIDC Callback (GET)

Method GET
Path /oauth/callback/:appId/:provider
Auth None (rate limited)

Description: Cliodot receives the OAuth callback from the IdP, exchanges the authorization code, stores the connection, and redirects to the customer's redirect_uri.

Path parameters:

Param Example
appId oauth_app_507f1f77bcf86cd799439011
provider google

Query parameters (from IdP): code, state

Sample success response (302):

Location: https://friendsconnect.com/auth/callback?code=ex_a1b2c3d4...&connection_id=conn_507f1f77bcf86cd799439014&state=csrf-token-from-your-app

Sample error response (302):

Location: https://friendsconnect.com/auth/callback?error=access_denied&error_description=User+denied+consent&state=csrf-token-from-your-app

Common errors: OAUTH_STATE_INVALID, OAUTH_PROVIDER_ERROR, OAUTH_CREDENTIALS_MISSING


SAML Callback (POST)

Method POST
Path /oauth/callback/:appId/:provider
Auth None (rate limited)

Description: SAML Assertion Consumer Service (ACS) callback. The IdP POSTs SAMLResponse and RelayState.

Request body (form-urlencoded):

Field Description
SAMLResponse Base64-encoded SAML response
RelayState Relay state from connect flow

Example:

POST /oauth/callback/oauth_app_507f1f77bcf86cd799439011/okta-saml
Content-Type: application/x-www-form-urlencoded
SAMLResponse=PHNhbWxwOlJlc3BvbnNl...&RelayState=relay-state-token

Sample success response (302): Same redirect shape as OAuth2 callback with code=ex_...&connection_id=conn_...

Common errors: OAUTH_STATE_INVALID, OAUTH_PROVIDER_ERROR, OAUTH_CREDENTIALS_MISSING


Token Exchange#

Method POST
Path /oauth/token/exchange
Auth App API key or app ID + secret

Description: Redeem a one-time exchange code after the user completes OAuth. Returns connection metadata; tokens are included only per the app's token_exposure_policy. Exchange codes are single-use and expire within exchange_code_ttl_seconds (default 60).

Request body:

{
  "code": "ex_a1b2c3d4e5f6789012345678901234567890ab",
  "connection_id": "conn_507f1f77bcf86cd799439014"
}

Sample success response (200 — metadata only):

{
  "ok": true,
  "connection": {
    "id": "conn_507f1f77bcf86cd799439014",
    "oauth_app_id": "oauth_app_507f1f77bcf86cd799439011",
    "provider": "google",
    "status": "active",
    "external_user_key": "user_12345",
    "identity": {
      "provider": "google",
      "provider_user_id": "108123456789012345678",
      "email": "user@example.com",
      "email_verified": true,
      "name": "Jane Doe",
      "picture": "https://lh3.googleusercontent.com/a/..."
    },
    "scopes_granted": ["openid", "email", "profile"],
    "expires_at": "2026-07-06T11:00:00.000Z"
  }
}

Sample success response (200 — include access token):

When token_exposure_policy is include_access_token or include_refresh_token:

{
  "ok": true,
  "connection": { },
  "access_token": "PROVIDER_ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_at": "2026-07-06T11:00:00.000Z"
}

Common errors: OAUTH_UNAUTHORIZED_APP, OAUTH_EXCHANGE_CODE_INVALID, OAUTH_CONNECTION_EXPIRED

cURL:

curl -X POST "https://api.cliodot.com/oauth/token/exchange" \
  -H "Authorization: Bearer YOUR_APP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "ex_a1b2c3d4e5f6789012345678901234567890ab",
    "connection_id": "conn_507f1f77bcf86cd799439014"
  }'

Alternative authentication:

curl -X POST "https://api.cliodot.com/oauth/token/exchange" \
  -H "X-Cliodot-App-Id: oauth_app_507f1f77bcf86cd799439011" \
  -H "X-Cliodot-App-Secret: YOUR_APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "ex_a1b2c3d4e5f6789012345678901234567890ab",
    "connection_id": "conn_507f1f77bcf86cd799439014"
  }'


FriendsConnect — social login with Google:

Step Action
1 Create OAuth App in the Cliodot portal. Save app_id, app_secret, and app_api_key (oak_...). Add https://friendsconnect.com/auth/callback to the redirect URI allowlist.
2 Attach Google provider in the Cliodot portal with alias google and scopes openid, email, profile. Register the Cliodot callback URL at Google (see below).
3 Redirect user to connect
4 User completes Google consent; Cliodot redirects to your app
5 Exchange code server-side
6 Fetch token when calling Google APIs

Register this callback at Google:

https://api.cliodot.com/oauth/callback/oauth_app_507f1f77bcf86cd799439011/google

Step 3 cURL:

curl -v "https://api.cliodot.com/oauth/apps/oauth_app_507f1f77bcf86cd799439011/connect?provider=google&redirect_uri=https%3A%2F%2Ffriendsconnect.com%2Fauth%2Fcallback&scope=openid%20email%20profile&state=abc&external_user_id=user_12345"

Step 5 cURL:

curl -X POST "https://api.cliodot.com/oauth/token/exchange" \
  -H "Authorization: Bearer YOUR_APP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "ex_a1b2c3d4e5f6789012345678901234567890ab",
    "connection_id": "conn_507f1f77bcf86cd799439014"
  }'

Step 6 cURL:

curl "https://api.cliodot.com/oauth/connections/conn_507f1f77bcf86cd799439014/token" \
  -H "Authorization: Bearer YOUR_APP_API_KEY"