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

FlowWhen to useUser involved?
client_credentialsService-to-service, background jobs, daemonsNo
authorization_code + PKCEUser-facing web and mobile appsYes
refresh_tokenExtending access without re-authenticationN/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 cookies
⚠️

Never 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:

ClaimDescription
subSubject — appId for client_credentials, userId for user flows
issIssuer — your platform URL (e.g. https://api.yourdomain.com)
audAudience — must match the target service
expExpiry — Unix timestamp
iatIssued at — Unix timestamp
jtiUnique token ID — used for revocation
tenant_idTenant identifier — extracted server-side, used for namespace isolation
app_idApplication identifier — from client_id at registration
user_idUser identifier — present only in user flows
scopeSpace-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.

ScopeWhat it grants
storage.private.readRead files, docs, and KV scoped to this app
storage.private.writeWrite files, docs, and KV scoped to this app
storage.user.readRead files and docs scoped to the authenticated user
storage.user.writeWrite files and docs scoped to the authenticated user
storage.shared.readRead tenant-wide shared storage
storage.shared.writeWrite tenant-wide shared storage
edm.readRead all EDM domain entities (people, assets, risk, etc.)
edm.writeCreate, update, and delete EDM entities
platform.skillsExecute platform skills
platform.workflowsCreate and trigger workflows
adminOrganization 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 executes

Refresh 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_xxxx

Theft 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

PropertyDetail
LanguageRust — memory-safe, no buffer overflows
Binds to[::1]:50051 — loopback only, not routable
StateStateless at rest — in-memory caches only (JWKS, Rego bundle, JTI deny-list)
AlgorithmRS256 only — alg: none and symmetric algorithms rejected
JWKS refreshEvery 30 seconds from the Control Plane
Policy updatesStreaming (WatchPolicyUpdates) — Rego bundle updated without restart
Restart safetyRehydrates from Control Plane within one 30 s sync cycle — no data loss
mTLSOptional via CORESDK_TLS_* env vars — recommended for production