Auth & Security
cPod uses OAuth 2.0 with RS256 JWTs throughout. The CoreSDK sidecar — a Rust security process running on loopback — is the trust boundary for all authentication and authorization decisions.
Three OAuth Flows
| Flow | When to use | User involved? |
|---|---|---|
client_credentials | Service-to-service, background jobs, daemons | No |
authorization_code + PKCE | User-facing web and mobile apps | Yes |
refresh_token | Extending access without re-authentication | N/A (rotation) |
client_credentials
Used by server-side services that act on their own behalf, not on behalf of a user.
Your Service
│
│ POST /v1/oauth/token
│ grant_type=client_credentials
│ &client_id=app-myservice
│ &client_secret=cs_xxxx
│ &scope=edm.read storage.private.write
▼
cpod-backend → CoreSDK Control Plane (internal)
│
│ Argon2-verify client_secret
│ Check requested scopes ⊆ declared scopes
│ Issue RS256 JWT (exp = now + 3600)
▼
Service receives access_token
Caches it, refreshes at 80% TTL (48 min)
Uses: Authorization: Bearer <jwt>authorization_code + PKCE
Used by user-facing applications where the token should be bound to a specific user’s identity and consent.
User opens your app
│
│ 1. App generates PKCE pair:
│ code_verifier = 32 random bytes (base64url)
│ code_challenge = base64url(SHA-256(code_verifier))
│ state = CSRF token (store in session)
│
│ 2. Redirect user to:
│ GET /oauth/authorize
│ ?client_id=app-myapp
│ &response_type=code
│ &redirect_uri=https://myapp.example.com/callback
│ &scope=edm.read storage.user.write
│ &code_challenge=<challenge>
│ &code_challenge_method=S256
│ &state=<csrf-token>
▼
cpod-backend renders consent screen
User authenticates + approves
│
│ 3. Redirect to callback:
│ GET https://myapp.example.com/callback
│ ?code=AUTH_CODE
│ &state=<csrf-token>
▼
Your App callback
│
│ 4. Verify state matches session (CSRF check)
│
│ 5. POST /v1/oauth/token
│ grant_type=authorization_code
│ &client_id=app-myapp
│ &code=AUTH_CODE
│ &redirect_uri=https://myapp.example.com/callback
│ &code_verifier=<original verifier>
▼
CoreSDK Control Plane
│ SHA-256(code_verifier) == stored code_challenge ← PKCE proof
│ Auth code expires in 60 s
│ Issue: access_token (RS256, 1 h) + refresh_token (opaque, 30 d)
▼
App stores tokens in HttpOnly + Secure + SameSite=Lax cookiesNever expose tokens to browser JavaScript. Store them in HttpOnly cookies only. The SameSite=Lax attribute protects against CSRF on token-bearing requests.
JWT Structure
Every token issued by cPod contains these claims:
| Claim | Description |
|---|---|
sub | Subject — appId for client_credentials, userId for user flows |
iss | Issuer — your platform URL (e.g. https://api.yourdomain.com) |
aud | Audience — must match the target service |
exp | Expiry — Unix timestamp |
iat | Issued at — Unix timestamp |
jti | Unique token ID — used for revocation |
tenant_id | Tenant identifier — extracted server-side, used for namespace isolation |
app_id | Application identifier — from client_id at registration |
user_id | User identifier — present only in user flows |
scope | Space-separated list of granted scopes |
The tenant_id, app_id, and user_id claims are set by the CoreSDK Control Plane at issuance. They cannot be forged by the client.
Scope System
Scopes are declared at app registration. Tokens can only grant scopes that were declared — you cannot request undeclared scopes even with valid credentials.
| Scope | What it grants |
|---|---|
storage.private.read | Read files, docs, and KV scoped to this app |
storage.private.write | Write files, docs, and KV scoped to this app |
storage.user.read | Read files and docs scoped to the authenticated user |
storage.user.write | Write files and docs scoped to the authenticated user |
storage.shared.read | Read tenant-wide shared storage |
storage.shared.write | Write tenant-wide shared storage |
edm.read | Read all EDM domain entities (people, assets, risk, etc.) |
edm.write | Create, update, and delete EDM entities |
platform.skills | Execute platform skills |
platform.workflows | Create and trigger workflows |
admin | Organization settings and user management |
Declare only the scopes your app genuinely needs. Tokens issued with fewer scopes than the maximum declared are valid — the SDK’s scope parameter at token request time lets you downscope for specific operations.
Token Validation Pipeline
On every request, cpod-backend middleware calls the CoreSDK sidecar (gRPC, loopback) before any handler runs:
cpod-backend receives: Authorization: Bearer eyJ...
│
▼
┌─ AuthService.ValidateToken ─────────────────┐
│ 1. Parse JWT header → get kid │
│ 2. Fetch matching JWK from in-memory cache │
│ 3. Verify RS256 signature │
│ 4. Check exp > now │
│ 5. Check iss == configured issuer │
│ 6. Check aud contains this service │
│ 7. Check jti not in revocation list │
└──────────────────┬──────────────────────────┘
valid │ invalid → 401
▼
┌─ AuthService.Authorize ─────────────────────┐
│ Input: { claims, action, resource, │
│ tenant_id, timestamp } │
│ Rego policy evaluation │
│ default allow := false │
└──────────────────┬──────────────────────────┘
allow │ deny → 403
▼
┌─ RateLimitService.CheckRateLimit ───────────┐
│ Token-bucket per tenant_id + client_id │
└──────────────────┬──────────────────────────┘
ok │ exceeded → 429
▼
Handler executesRefresh Token Rotation
Refresh tokens rotate on every use. The previous token is revoked immediately.
access_token expires (or 80% TTL = 48 min reached)
│
│ POST /v1/oauth/token
│ grant_type=refresh_token
│ &refresh_token=rt_old_xxxx
▼
CoreSDK Control Plane:
Verify rt_old_xxxx is valid + not revoked
Issue new access_token (RS256, 1 h)
Issue new refresh_token (rt_new_yyyy)
Revoke rt_old_xxxx ← immediately, entire family if replay detected
│
▼
Client stores rt_new_yyyy, discards rt_old_xxxxTheft detection: if rt_old_xxxx is presented again after rotation, the Control Plane detects token replay and revokes the entire refresh token family. The legitimate user is logged out and must re-authenticate.
Monitor token-events for refresh_replay events. They indicate that a refresh token was used by two parties — likely credential theft.
CoreSDK Sidecar Security Properties
| Property | Detail |
|---|---|
| Language | Rust — memory-safe, no buffer overflows |
| Binds to | [::1]:50051 — loopback only, not routable |
| State | Stateless at rest — in-memory caches only (JWKS, Rego bundle, JTI deny-list) |
| Algorithm | RS256 only — alg: none and symmetric algorithms rejected |
| JWKS refresh | Every 30 seconds from the Control Plane |
| Policy updates | Streaming (WatchPolicyUpdates) — Rego bundle updated without restart |
| Restart safety | Rehydrates from Control Plane within one 30 s sync cycle — no data loss |
| mTLS | Optional via CORESDK_TLS_* env vars — recommended for production |