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:

TypeUse case
serviceAccountCI pipelines, deploy bots, scheduled workloads.
agentAI agents, automation actors that act on behalf of humans.
userReserved 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

OperatorMeaning
eqExact match.
neqMust not equal.
inMembership in a small list.
globGlob 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:

  1. JWT signature verified locally against the active signing key ring. No DDB call.
  2. JWT exp checked locally. No DDB call.
  3. Single primary-key GetItem against SESSION#<sessionId> to read status, expiresAt, permissionDocId, and permissionHash.
  4. Row status must be active and expiresAt must be in the future. DDB TTL is cleanup-only; an absent row is treated as expired/invalid, never as authorization.
  5. JWT permissionHash must equal the row permissionHash. Mismatch fails closed — this protects against stale or substituted permission references.
  6. The referenced permission document is loaded from a module-scope LRU cache, or from DDB on cache miss.
  7. The requested action and resource are checked against the materialized permissions map 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:

CacheDefault TTLDefault size
Session row15 seconds1000 entries
Permission document30 seconds1000 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:

  1. Add a new key with active: true. Set the prior active key to active: false.
  2. Deploy. New sessions are signed with the new key; existing sessions still verify against the old key during the overlap window.
  3. 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 maxSessionDuration on 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).
  • 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).