Docs

Docs

Auth & Security

cPod uses OAuth 2.0 client_credentials (RFC 6749 §4.4) for all service-to-service authentication. cpod-backend owns the auth surface — it validates tokens and proxies to the CoreSDK sidecar for policy enforcement. The sidecar binds to loopback only and is never reachable from the network.

code
Your App  →  POST /api/v1/auth/token  →  cpod-backend  →  gRPC [::]50051  →  CoreSDK Sidecar
                                              ↑
                                   Registers apps, issues tokens
                                   Sidecar port never exposed

client_credentials Flow#

Used by all backend services, CI, and MCP-registered apps.

code
Your Service
    │
    │  POST /api/v1/auth/token
    │  Content-Type: application/x-www-form-urlencoded
    │  grant_type=client_credentials
    │  &client_id=app_a1b2c3d4
    │  &client_secret=sk-xxxx
    │  &scope=mcp:call mcp:register
    ▼
cpod-backend
    │
    │  bcrypt-verify client_secret
    │  Lookup tenant_id from app_service_accounts
    │  Issue HS256 JWT (exp = now + 3600)
    ▼
Service receives access_token
Caches it, refreshes at 80% TTL (48 min)
Uses: Authorization: Bearer <jwt>

See Authentication for SDK helpers and the token cache pattern.


JWT Structure#

ClaimValue
subapp_id — the registered app identifier
tenant_idTenant owning the app — extracted server-side, never client-supplied
roles["app_service_account"]
scopeRequested scopes (e.g. mcp:call mcp:register)
iatIssued at (Unix timestamp)
expExpiry (Unix timestamp, 1 hour after iat)

tenant_id is embedded at issuance. It is re-derived on every request from the validated JWT — the client cannot override it via request args or headers.


Scopes#

ScopeWhat it grants
mcp:callInvoke tools through the MCP proxy
mcp:registerRegister and manage MCP tools
admin:*Full administrative access — use sparingly

Tokens issued via client_credentials carry the app_service_account role. This role cannot elevate to tenant_admin or global_admin.


Token Validation Pipeline#

On every request, cpod-backend middleware validates the Bearer token before any handler runs:

code
cpod-backend receives: Authorization: Bearer eyJ...
                              │
                              ▼
        ┌─ LocalFallbackSDK.authorize ────────────────┐
        │  1. Decode JWT header                        │
        │  2. Verify HS256 signature                   │
        │  3. Check exp > now                          │
        │  4. Extract tenant_id, roles, scope          │
        └──────────────────┬──────────────────────────┘
                     valid │  invalid → 401
                           ▼
                    Handler executes
                    (tenantId stamped from claims)

If the CoreSDK sidecar is available, token validation is done via gRPC ValidateToken instead — the sidecar uses RS256 with rotating JWKS.


CoreSDK Sidecar Security Properties#

PropertyDetail
LanguageRust — memory-safe
Binds to[::1]:50051 — loopback only, not routable
AlgorithmRS256 (production) / HS256 fallback (development without sidecar)
PolicyRego evaluation via AuthService.Authorize
JWKS refreshEvery 30 seconds from the Control Plane
StateStateless at rest — in-memory caches only

In development without a sidecar, cpod-backend falls back to local HS256 validation using BOOTSTRAP_ADMIN_SECRET. Always configure the CoreSDK sidecar (CORESDK_SIDECAR_ADDR) in staging and production.


Tenant Isolation#

tenant_id is always derived from the validated JWT — never from request arguments. A token issued for tenant A can only read data scoped to tenant A, regardless of what the client sends in the request body or query string.

code
client sends: POST /api/v1/mcp/call { "appId": "...", "tenantId": "tenant-B" }
                              │
cpod-backend ignores "tenantId" in body
Extracts tenant_id from JWT claims only
Scopes lookup to JWT tenant_id