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.
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.
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)
})from cpod import CpodClient
@app.get("/people")
async def people(request: Request):
token = request.headers["authorization"].removeprefix("Bearer ")
async with CpodClient.from_token(token) as cpod: # acts as the user
result = await cpod.people.list()
return result.datatoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
client := cpod.NewFromToken(token) // acts as the user
people, _ := client.People.List(r.Context(), nil)var token = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
var client = CpodClient.FromToken(token); // acts as the user
var people = await client.People.ListAsync();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):
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 loginUrllogin_url = CpodClient.begin_login(
backend_url="https://api.cyberpod.app",
return_to="https://app.example.com/auth/callback",
)loginURL := cpod.BeginLogin("https://api.cyberpod.app", "https://app.example.com/auth/callback")var loginUrl = CpodClient.BeginLogin("https://api.cyberpod.app", "https://app.example.com/auth/callback");2. Exchange the code — your /auth/callback receives ?code=…; swap it for the token (server-side) and build a client:
const { accessToken, refreshToken } = await CpodClient.exchangeCode({
backendUrl: 'https://api.cyberpod.app',
code, // from req.query.code
})
const client = CpodClient.fromToken(accessToken, { refreshToken })bundle = await CpodClient.exchange_code(backend_url="https://api.cyberpod.app", code=code)
async with CpodClient.from_token(
bundle["access_token"], refresh_token=bundle["refresh_token"]
) as client:
...access, refresh, _ := cpod.ExchangeCode(ctx, "https://api.cyberpod.app", code)
client := cpod.NewFromToken(access, cpod.WithRefreshToken(refresh))var (access, refresh) = await CpodClient.ExchangeCodeAsync("https://api.cyberpod.app", code);
var client = CpodClient.FromToken(access);The full redirect chain — the browser only ever carries an opaque 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 not | Because |
|---|---|
store a CPOD_API_KEY | the app holds no credentials |
register an app_id / rotate a client_secret | identity is the user's, from Keycloak |
| call a token endpoint at startup | the token arrives with the request |
| implement OAuth | the 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:
# 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:
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#
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#
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/auth/oidc/login | Backend — 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/refresh | Backend — swap a refresh token for a fresh access token (the SDK does this silently) |
POST | /api/v1/auth/validate | Validate a token |
POST | /api/v1/auth/revoke | Revoke 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 URL | Advanced (env) | |
|---|---|---|---|
| TypeScript | CpodClient.fromToken(token) | CpodClient.beginLogin({…}) | CpodClient.fromEnv() |
| Python | CpodClient.from_token(token) | CpodClient.begin_login(…) | CpodClient.from_env() |
| Go | cpod.NewFromToken(token) | cpod.BeginLogin(backendUrl, returnTo) | cpod.NewFromEnv() |
| .NET | CpodClient.FromToken(token) | CpodClient.BeginLogin(backendUrl, returnTo) | CpodClient.FromEnv() |