Docs

Docs

Authentication

cPod apps hold no credentials of their own — no API key, no client secret. Identity is centralized in your IdP (Keycloak). A user authenticates once against Keycloak; the resulting access token flows in with each request, and your app forwards it to the SDK. Every call then runs as that user, with their tenant and RBAC — exactly what the IdP issued.

code
Browser ──login──▶ Keycloak ──┐
                              │  (control plane mints a CoreSDK token)
Your App  ──fromToken(token)──▶ cpod-backend ──▶ CoreSDK sidecar (validates)
   ▲                                                   │
   └──────── the token that arrived on the request ────┘

The app stores nothing. There is no CPOD_API_KEY, no client_secret in your app config. The only thing it handles is the per-request Bearer token the IdP already issued — and it just passes it through.


The pattern: forward the request's token#

In a request handler, take the inbound Authorization: Bearer … token and build a client from it with fromToken. The SDK attaches it and (given a refresh token) refreshes transparently on 401.

typescript
import { CpodClient } from '@cpod/sdk'
 
app.get('/people', async (req, res) => {
  const token = (req.headers.authorization ?? '').replace(/^Bearer\s+/i, '')
  const cpod = CpodClient.fromToken(token)      // no secrets — acts as the user
  const people = await cpod.people.persons.list()
  res.json(people.data)
})

Pass a refresh token to get transparent re-auth: CpodClient.fromToken(accessToken, { refreshToken }).


Where the token comes from — the login#

The user logs in through the centralized Keycloak flow. You don't implement OAuth. The SDK builds the login URL; after the user authenticates, the backend hands your app a one-time code which the SDK swaps for the token, server-to-server. The token never travels in a browser URL — only the opaque code does.

1. Start login — send the browser to the login URL (return to your callback):

typescript
const { loginUrl } = CpodClient.beginLogin({
  backendUrl: 'https://api.cyberpod.app',          // the SDK's only point of contact
  returnTo: 'https://app.example.com/auth/callback',
})
// → redirect the browser to loginUrl

2. Exchange the code — your /auth/callback receives ?code=…; swap it for the token (server-side) and build a client:

typescript
const { accessToken, refreshToken } = await CpodClient.exchangeCode({
  backendUrl: 'https://api.cyberpod.app',
  code,  // from req.query.code
})
const client = CpodClient.fromToken(accessToken, { refreshToken })

The full redirect chain — the browser only ever carries an opaque code:

code
app → cpod-sdk.beginLogin() → {backend}/api/v1/auth/oidc/login?return_to={app}/auth/callback
    → backend → control plane (delivery=code) → Keycloak → control-plane callback
    → backend /oidc/callback  (exchanges code→token SERVER-SIDE, holds the app URL)
    → {app}/auth/callback?code=<opaque>     ← only an opaque code, never the token
app → cpod-sdk.exchangeCode(code) → {token} ← server-to-server swap, then fromToken

backendUrl is the cPod backend base URL — the SDK's only point of contact (login, code exchange, data, refresh). The app never talks to the control plane directly, and the token never appears in a browser URL.


No registration, no secret to manage#

You do notBecause
store a CPOD_API_KEYthe app holds no credentials
register an app_id / rotate a client_secretidentity is the user's, from Keycloak
call a token endpoint at startupthe token arrives with the request
implement OAuththe control plane + Keycloak own it

Configuration left in your app is non-secret only — app name, port, and the API base URL — in cpod.config.yaml. (For local development you may set CPOD_API_URL to point at a different backend; it is not a secret.)


Advanced: service-to-service (no user present)#

Some workloads have no logged-in user — cron jobs, queue workers, CI/CD, MCP tool registration. For these, the platform supports an OAuth 2.0 client_credentials service identity (RFC 6749 §4.4). This is not the default app model — reach for it only when there is genuinely no user token to forward.

Prefer forwarding the user's token (fromToken) whenever a user is in the loop. Service-account credentials act as the app, not a user, and bypass per-user RBAC — treat them as privileged.

A service account is registered once and issued a client_secret:

bash
# register (admin token), then rotate to get a secret (shown once)
curl -X POST https://api.cyberpod.app/api/v1/apps/register \
  -H "Authorization: Bearer $CPOD_ADMIN_TOKEN" \
  -d '{"name": "nightly-risk-sync"}'
curl -X POST https://api.cyberpod.app/api/v1/apps/<app_id>/credentials/rotate \
  -H "Authorization: Bearer $CPOD_ADMIN_TOKEN"

Exchange the credentials for a token, then use fromToken with it like any other:

bash
curl -X POST https://api.cyberpod.app/api/v1/auth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=$CPOD_CLIENT_ID&client_secret=$CPOD_CLIENT_SECRET"

from_env() / fromEnv() also reads CPOD_CLIENT_ID + CPOD_CLIENT_SECRET (or a raw CPOD_API_KEY) for these headless cases — convenient for local dev and automation, but never the model for a user-facing app.


Validate / revoke a token#

bash
curl  https://api.cyberpod.app/api/v1/auth/validate?token=<access_token>
curl -X POST https://api.cyberpod.app/api/v1/auth/revoke \
  -H "Authorization: Bearer <access_token>"

Revoke returns 200 OK even for already-expired tokens (RFC 7009).


Reference#

MethodEndpointDescription
GET/api/v1/auth/oidc/loginBackend — start login (the SDK's entry point; forwards to the control plane)
GET/auth/oidc/callback(control plane) IdP returns here; the control plane mints the token
POST/api/v1/auth/refreshBackend — swap a refresh token for a fresh access token (the SDK does this silently)
POST/api/v1/auth/validateValidate a token
POST/api/v1/auth/revokeRevoke a token
POST/api/v1/auth/token(advanced) client_credentials for service accounts
POST/api/v1/apps/register(advanced) register a service account
POST/api/v1/apps/{app_id}/credentials/rotate(advanced) rotate a service secret

SDK entry points#

Primary (user)Login URLAdvanced (env)
TypeScriptCpodClient.fromToken(token)CpodClient.beginLogin({…})CpodClient.fromEnv()
PythonCpodClient.from_token(token)CpodClient.begin_login(…)CpodClient.from_env()
Gocpod.NewFromToken(token)cpod.BeginLogin(backendUrl, returnTo)cpod.NewFromEnv()
.NETCpodClient.FromToken(token)CpodClient.BeginLogin(backendUrl, returnTo)CpodClient.FromEnv()