Skip to main content
Outcome: Workloads can call Formal APIs with IdP JWTs and mint connector access tokens.
Prerequisites: A Formal machine user, and an IdP that can mint OIDC JWTs.

Overview

OIDC integrations let external workloads authenticate to the Formal with short-lived identity-provider JWTs. Formal verifies the token, evaluates a claim condition, and maps the caller to a configured machine user. The same trust can mint a short-lived federation token. Use that credential to access data through a Formal — without long-lived Formal API keys or machine access tokens. Use this for remote workloads such as GitHub Actions, GitLab, Terraform, SPIFFE workloads, E2B sandboxes, Modal Functions, or the Kubernetes Operator.
OIDC integrations are separate from SSO. SSO covers human Control Plane login. OIDC integrations cover machine authentication to core APIs and optional federated connector access.

How It Works

  1. You create an OIDC integration with an issuer, machine user, and claim condition.
  2. Formal returns an audience value: oidc.formal.ai/<integration_id>.
  3. Your identity provider issues a JWT with that exact aud claim.
  4. The caller presents the JWT as a Bearer token to Formal core APIs.
  5. Formal verifies signature, issuer, audience, time claims, and the claim condition.
  6. The request runs as the integration’s machine user.
  7. Optionally, the caller mints a federation token and connects through a Connector.

Fixed IdP Audiences

Some identity providers do not let callers customize the JWT aud claim. These callers must select the integration with a request header:
Formal then verifies the issuer, signature, expiry, and claim condition. The claim condition must verify the provider’s audience and stable workload identity claims.
Do not use claim_condition = "true" with header selection. Any valid token from the configured issuer could otherwise authenticate, even when issued for another service or workload.
For Spacelift, the audience is your account hostname. Prefer stable custom claims instead of comparing the complete sub, which varies by run type and scope:
Get these values from the Spacelift stack and space settings. See Spacelift’s OIDC claim reference for claim formats. Send the fixed-audience token and integration ID together:
Verify: The request succeeds only for the configured account, space, and stack. Changing any checked claim returns 401 Unauthorized. The Formal Terraform provider can read Spacelift’s token and send the header:
If a token contains a Formal audience and the header, both integration IDs must match. Formal performs this check and rejects conflicting values.

Setup in the Control Plane

  1. Go to OIDC.
  2. Click Create Integration.
  3. Fill in:
    • Name — unique friendly name in your org
    • Issuer — absolute HTTPS issuer URL
    • JWKS URI — optional; leave empty to use OIDC Discovery
    • Machine User — Formal principal that authenticated tokens map to
    • Claim Condition — CEL expression over claims (default true)
    • End-User Email Expression — optional CEL that returns an email string
    • Status — active or draft (draft disables auth)
  4. Copy the Audience value shown after create.
  5. Configure your identity provider to mint tokens with that audience.
Verify: Create succeeds and the audience appears as oidc.formal.ai/integrationoidc_….

Claim Conditions

Claim conditions are CEL expressions that must return a boolean. Verified JWT claims are available under claims.
If your OIDC issuer lets you set aud, request a dedicated audience for Formal and validate that exact value in claim_condition.
Examples:
Failed, non-boolean, or invalid expressions deny authentication.

SPIFFE and SPIRE

SPIFFE workloads authenticate with a JWT-SVID. Formal verifies it like any other OIDC JWT, so the SPIFFE ID arrives as the sub claim. Formal fetches signing keys from the SPIRE OIDC Discovery Provider. Formal must reach it over public HTTPS on port 443.

1. Set the JWT-SVID issuer

Formal requires the token iss claim to match the integration issuer exactly. SPIRE omits iss unless you set jwt_issuer on the SPIRE server:
Use the same URL as a domains entry in the OIDC Discovery Provider config. Verify:

2. Create the integration

In OIDC, click Create Integration and choose the SPIFFE / SPIRE preset. Set Issuer to the jwt_issuer URL. Match the full SPIFFE ID in the claim condition:
To trust a group of workloads, match a path prefix that ends in /:
A prefix without a trailing / also matches sibling paths. For example, spiffe://example.org/ns/prod matches spiffe://example.org/ns/production.

3. Fetch a JWT-SVID for Formal

Request a JWT-SVID whose audience is the integration audience. SPIRE lets you set the audience, so you do not need X-Formal-OIDC-Integration-Id.
Verify: The output includes token(spiffe://example.org/...) followed by the JWT. Use it as the Bearer token for Formal APIs and CreateFederationToken. SPIRE issues JWT-SVIDs that expire after five minutes by default. Federation tokens minted from them expire at the same time.

4. Write policies on the SPIFFE ID

Connector policies read the SPIFFE ID from input.user.oidc.claims.sub:

E2B Sandboxes

E2B Workload Identity gives each sandbox execution a short-lived JWT-SVID. On E2B Cloud, tokens use issuer https://id.e2b.dev and a sub in this format:

1. Create the integration

In OIDC, click Create Integration and choose the E2B preset. Restrict the claim condition to your E2B project, with a trailing / so other projects cannot match:
For Bring Your Own Cloud (BYOC) deployments, set Issuer to your deployment’s issuer URL and use its SPIFFE trust domain in the claim condition.

2. Inject the token into Formal requests

Register a workload token with the integration audience, then add a network rule that injects it into HTTPS requests to Formal. E2B’s egress proxy adds the header, so sandbox code never handles the token:
If your HTTP client uses its own CA bundle, configure it to trust the E2B egress proxy. See E2B’s TLS certificate guide. Verify: The response contains a federation token and username. Connector policies can read input.user.oidc.claims.project_id, input.user.oidc.claims.sandbox_id, and input.user.oidc.claims.template_id. Modal injects an identity token into every Function container as MODAL_IDENTITY_TOKEN. Sandboxes and Sidecars receive one only when created with include_oidc_identity_token=True. Modal tokens always use the fixed audience oidc.modal.com, so select the integration with the X-Formal-OIDC-Integration-Id header (see Fixed IdP Audiences).

1. Create the integration

In OIDC, click Create Integration and choose the Modal preset. Restrict the claim condition to your Modal workspace, and optionally an environment, app, or function:
Find your workspace ID in Modal workspace settings or with modal token info.

2. Mint a federation token from a Function

Verify: The response contains a federation token and username. Connector policies can read Modal claims such as input.user.oidc.claims.app_name and input.user.oidc.claims.function_name.

Federated Connector Access

After an OIDC JWT authenticates to the control plane, call CreateFederationToken to mint a short-lived connector credential. Only OIDC-authenticated machine users can call this RPC. API keys and human sessions cannot mint federation tokens.

Mint a federation token

Example response:

Connect through a Connector

Use the response like any Formal username and access token:
Replace <USERNAME> and <FEDERATION_TOKEN> with username and token from the mint response. Protocols that present the Formal JWT can authenticate immediately. Protocols that match stored credentials (for example some MySQL and S3 paths) need the token to sync to the Connector first. Retry briefly if auth fails right after mint.

Connect over SSH

Add the resource name after a + in the SSH username. Enter the password when prompted:
OpenSSH truncates passwords longer than 1,023 bytes. Federation tokens carry the source claims, so they often exceed that limit. Use the hashed form of the token as the password instead. For example, with sshpass in automation:
The hashed form matches tokens synced to the Connector, so retry briefly right after mint. Verify: The session opens, and SSH session policies can read input.user.oidc.claims.

Token lifetime and claims

Federation token expiry is:
Formal signs the federated JWT with:

Evaluate connector policies with source claims

Connector policy evaluation over source OIDC claims requires Connector 2.16.1 or later.
The Connector exposes verified source claims under input.user.oidc:
  • input.user.oidc.integration_id
  • input.user.oidc.claims.*
Claims belong to the authenticated machine user. They do not appear under input.end_user. For example, this session policy allows only a specific GitHub repository and branch:
Missing or mismatched claims leave the default block action in effect.

End-user attribution

Set End-User Email Expression when the workload acts for a human. The CEL expression runs over verified IdP claims and must return an email string. At mint time, Formal resolves that email to a Formal human user (case-insensitive). Sessions then use the machine user for credentials and the human user as the for policy and audit. On resources with linked accounts, the Connector sends the human user’s linked account. The workload then acts upstream as that person, so build the expression only from claims the workload can’t choose itself. Examples:
Leave the expression empty for machine-only trusts. If the expression is set and resolution fails, mint fails closed — no token is issued.

Permissions and Audit

Verified OIDC callers expose claim context to Permissions as:
  • input.user.oidc.integration_id
  • input.user.oidc.issuer
  • input.user.oidc.claims.*
Control-plane audit events also record integration metadata and bounded claim JSON. They never store the raw JWT.

Terraform

You can manage the same resource with Terraform:
Verify:

Troubleshooting

Possible causes:
  • Integration status is draft or deleted
  • Token aud does not exactly match oidc.formal.ai/<id>
  • Fixed-audience token omitted X-Formal-OIDC-Integration-Id
  • Issuer or JWKS does not match the integration
  • Claim condition evaluates to false
  • Machine user is inactive or deleted
Fix: Confirm the audience, set status to active, and validate issuer/JWKS/claims.
Possible causes:
  • Issuer is not HTTPS
  • Discovery endpoint is unreachable from Formal
  • JWKS URI is missing when discovery is unavailable
Fix: Set an explicit HTTPS JWKS URI, or fix the issuer discovery document.
Possible causes:
  • SPIRE server jwt_issuer is unset, so the JWT-SVID has no iss claim
  • jwt_issuer differs from the integration issuer, including a trailing /
  • The JWT-SVID audience is not oidc.formal.ai/<INTEGRATION_ID>
Fix: Set jwt_issuer to the integration issuer and restart the SPIRE server. Then fetch a new JWT-SVID with the integration audience.
Possible causes:
  • Caller used an API key instead of an OIDC Bearer JWT
  • Caller is not a machine user
  • Upstream OIDC JWT is expired
  • End-user email expression failed or matched no Formal human
Fix: Authenticate with a valid OIDC JWT for an active integration. If you set an end-user expression, confirm the claim value and that the human exists in Formal.
Possible causes:
  • Token expired (min(OIDC exp, 1 hour))
  • Wrong Formal username (must be the mint response username)
  • Credential-match protocol used before the token synced
Fix: Mint a fresh token, use the returned username, and retry after a short sync delay for MySQL/S3-style auth paths.
Possible causes:
  • OpenSSH truncated the federation token, which is longer than 1,023 bytes
  • The hashed token was used before it synced to the Connector
  • The token expired
Fix: Use the formalhash: form of the token as the password, and retry briefly after mint. Mint a new token if it expired.

Next Steps

Permissions

Authorize federated callers with claim-aware rules

Machine Users

Create the machine principal OIDC tokens map to

Connectors

Deploy Connectors that accept federation tokens

SSO

Configure human Control Plane login separately