Federated Principal Auth In development
Federated principal auth lets a workload prove its identity using an external identity provider (GitHub Actions OIDC, AWS IAM, Kubernetes service accounts, or any standard OIDC issuer), then exchange that proof for a short-lived SaaSLayer Permissions session.
Use it when you want to:
- Remove long-lived SaaSLayer API keys from CI/CD, Lambda, or Kubernetes deployments.
- Bind workload identity to a specific repository, branch, AWS role, or Kubernetes service account.
- Cap session lifetime by the upstream identity's own expiry.
- Audit every session back to the external proof that authorized it.
This is not a secrets manager. SaaSLayer Permissions does not store customer secrets. It stores trust policies that decide which external identities are allowed to act as which RBAC principals.
Who can federate
Three principal types can hold auth methods:
| Type | Use case |
|---|---|
serviceAccount | CI pipelines, deploy bots, scheduled workloads. |
agent | AI agents, automation actors that act on behalf of humans. |
user | Reserved for future human IdP federation. Not yet enabled. |
The first shipping slice supports serviceAccount. agent storage works the same way; only the exchange routes are gated.
Create the service account first; its id is server-generated and returned in data.id:
POST /principals/serviceAccount
Content-Type: application/json
{ "name": "deploy-prod", "ownerType": "org", "ownerId": "org_acme" }The examples below use the returned id sa_01j9zq3m4k5n6p7r8s9t0v1w2x.
Auth methods
An auth method attaches a trust policy to a principal. Trust policies say which external identities the principal trusts.
Create an OIDC auth method
POST /principals/serviceAccount/sa_01j9zq3m4k5n6p7r8s9t0v1w2x/auth-methods
Content-Type: application/json
{
"method": "oidc",
"name": "github-main-branch",
"enabled": true,
"trustPolicy": {
"issuer": "https://token.actions.githubusercontent.com",
"jwksUri": "https://token.actions.githubusercontent.com/.well-known/jwks",
"audiences": ["saaslayer"],
"subjects": ["repo:acme/api:ref:refs/heads/main"],
"claims": {
"repository": { "eq": "acme/api" },
"ref": { "eq": "refs/heads/main" },
"workflow": { "glob": ".github/workflows/deploy*.yml" }
}
},
"maxSessionDuration": 3600
}The response includes a methodId of the form pam_....
Trust policy operators
| Operator | Meaning |
|---|---|
eq | Exact match. |
neq | Must not equal. |
in | Membership in a small list. |
glob | Glob match using the same matchesPattern() semantics as policies. |
Trust policies always evaluate as conjunctions: every field must pass. Subject patterns and claim conditions both have to match.
List, update, delete
GET /principals/serviceAccount/sa_01j9zq3m4k5n6p7r8s9t0v1w2x/auth-methods
GET /principals/serviceAccount/sa_01j9zq3m4k5n6p7r8s9t0v1w2x/auth-methods/pam_01J...
PUT /principals/serviceAccount/sa_01j9zq3m4k5n6p7r8s9t0v1w2x/auth-methods/pam_01J...
DELETE /principals/serviceAccount/sa_01j9zq3m4k5n6p7r8s9t0v1w2x/auth-methods/pam_01J...Disabling a method (enabled: false) blocks new exchanges immediately. It does not revoke active sessions; revoke sessions explicitly for that.
OIDC exchange
After an OIDC auth method is configured, the workload exchanges its OIDC token for a SaaSLayer session.
POST /principals/serviceAccount/sa_01j9zq3m4k5n6p7r8s9t0v1w2x/auth/oidc
Content-Type: application/json
{
"token": "<oidc-jwt-from-provider>",
"durationSeconds": 900
}The route principal in the URL path is authoritative. SaaSLayer never derives the SaaSLayer principal from token claims. Trust policies only prove that the configured external identity is allowed to authenticate as that specific path principal. This is the same anti-confused-deputy rule used by AWS AssumeRoleWithWebIdentity.
Successful response:
{
"data": {
"type": "session",
"id": "sess_01J...",
"attributes": {
"token": "sess_<jwt>",
"sessionKind": "principal",
"subject": { "type": "SERVICE_ACCOUNT", "id": "sa_01j9zq3m4k5n6p7r8s9t0v1w2x" },
"authMethod": "oidc",
"authMethodId": "pam_01J...",
"externalIssuer": "https://token.actions.githubusercontent.com",
"externalSubject": "repo:acme/api:ref:refs/heads/main",
"scope": "tn_prod",
"expiresAt": "2026-05-17T18:15:00.000Z"
}
}
}Use the returned sess_<jwt> as a bearer token against any SaaSLayer-protected API:
GET /some/protected/resource
Authorization: Bearer sess_<jwt>Session duration capping
Session duration is capped by the minimum of:
min(
requestedDuration,
authMethod.maxSessionDuration,
externalProofRemainingLifetime,
serviceDefaultMax
)If the OIDC token has only five minutes of life left, the minted session is capped at five minutes. Long-running workloads should re-exchange a fresh OIDC token rather than rely on session renewals that outlive the upstream proof.
GitHub Actions setup
Inside a GitHub Actions job:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Get OIDC token
id: oidc
uses: actions/github-script@v7
with:
script: |
const token = await core.getIDToken('saaslayer')
core.setSecret(token)
core.setOutput('token', token)
- name: Exchange for SaaSLayer session
run: |
SESSION=$(curl -fsS -X POST \
https://rbac.example.com/principals/serviceAccount/sa_01j9zq3m4k5n6p7r8s9t0v1w2x/auth/oidc \
-H 'Content-Type: application/json' \
-d "{\"token\":\"${{ steps.oidc.outputs.token }}\",\"durationSeconds\":900}" \
| jq -r '.data.attributes.token')
echo "SAASLAYER_SESSION=$SESSION" >> "$GITHUB_ENV"
- name: Call SaaSLayer API
run: |
curl -fsS -H "Authorization: Bearer $SAASLAYER_SESSION" \
https://rbac.example.com/permissions/check \
-d '{"action":"deploy","resource":"service/api","scope":"tn_prod"}'Important: audiences in the trust policy must match the aud value requested in getIDToken('saaslayer'). The trust policy's subjects and claims should pin the workflow to exactly the repository, branch, and workflow file you intend to authorize.
How the session is validated
Each request that uses a sess_<jwt> token is authorized in this order:
- JWT signature verified locally against the active signing key ring. No DDB call.
- JWT
expchecked locally. No DDB call. - Single primary-key
GetItemagainstSESSION#<sessionId>to readstatus,expiresAt,permissionDocId, andpermissionHash. - Row
statusmust beactiveandexpiresAtmust be in the future. DDB TTL is cleanup-only; an absent row is treated as expired/invalid, never as authorization. - JWT
permissionHashmust equal the rowpermissionHash. Mismatch fails closed — this protects against stale or substituted permission references. - The referenced permission document is loaded from a module-scope LRU cache, or from DDB on cache miss.
- The requested action and resource are checked against the materialized
permissionsmap on that document.
This is the same one-row-lookup model that already authorizes API keys. JWT carries tamper-proof identity; the row carries authoritative status. Both must agree.
Cache and revocation lag
The authorizer keeps two module-scope LRU caches on warm Lambda containers:
| Cache | Default TTL | Default size |
|---|---|---|
| Session row | 15 seconds | 1000 entries |
| Permission document | 30 seconds | 1000 entries |
Revocation lag for a session is bounded by the session cache TTL, not by the JWT exp. If you need stricter revocation guarantees, either shorten the TTL via configuration or rely on signing-key removal (see below).
Cold starts pay the full DDB lookup. Production deployments should size Lambda warm pools accordingly.
Signing-key rotation
SaaSLayer signs session JWTs with a configured signing key ring. The ring has exactly one active key (used for signing) and any number of previous keys (still accepted for verification).
SESSION_SIGNING_KEY_RING = [
{ kid: "sig_2026_a", secret: "...", active: false },
{ kid: "sig_2026_b", secret: "...", active: true }
]To retire a compromised key:
- Add a new key with
active: true. Set the prior active key toactive: false. - Deploy. New sessions are signed with the new key; existing sessions still verify against the old key during the overlap window.
- After the overlap window (recommended ≥ 1 maximum-session-duration), remove the old key from the ring. Any session still signed by the removed key fails verification.
Removing a key is the only mechanism that immediately invalidates every session signed by it. Use it when the signing key itself is compromised. For per-session revocation, mutate the SESSION row instead.
Operators: the routine and emergency rotation procedures are documented in docs/architecture/session-key-rotation.md.
JWKS caching
The OIDC verifier caches issuer JWKS in a bounded module-scope cache:
- Cache key includes both issuer and JWKS URI to isolate identical-issuer-different-key-source situations.
- Default TTL: 5 minutes, capped shorter when the JWKS endpoint sets a more restrictive
Cache-Control. - Default size: 100 issuer entries per warm container, LRU-evicted.
Cold containers fetch JWKS on the first request. Plan for the IdP being reachable; an unreachable JWKS endpoint means exchange fails closed.
Long-running workloads
Sessions are intentionally short. For workloads that run longer than maxSessionDuration:
- Re-exchange a fresh OIDC token before the current session expires.
- Cache the SaaSLayer session for the duration the upstream proof and trust policy allow.
- Do not store SaaSLayer sessions to disk. They are bearer tokens; treat them like one.
Operational guidance
- Pin trust policies to the smallest viable scope (specific repo and branch, not whole org).
- Set
maxSessionDurationon the auth method to bound blast radius if a workload is compromised. - Audit auth method changes and exchange attempts (success and failure).
- Never log the raw OIDC token. The exchange route, audit log, and outbox events redact tokens before storage.
- Rotate trust policies when the upstream workload identity changes (e.g., new repo, new branch convention).
Related guides
- Agent Delegation — agents acting on behalf of users via delegated principal sessions.
- Event Delivery — webhook and EventBridge notifications for approvals and session lifecycle events.
- Delegated Sessions — temporary role assumption (the original session model, still supported).