> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# OIDC

> Trust external OIDC JWTs for Formal API auth and short-lived connector federation tokens

export const G = ({term, anchor, children}) => {
  const href = anchor ? `/docs/glossary/index#${anchor}` : `/docs/glossary/index`;
  return <a href={href} className="glossary-link" style={{
    textDecoration: "underline",
    textDecorationLine: "underline",
    textDecorationColor: "#6b7280",
    textDecorationThickness: "1px",
    textUnderlineOffset: "2px",
    color: "inherit",
    transition: "text-decoration-color 0.2s ease",
    borderBottom: "none"
  }} onMouseEnter={e => e.target.style.textDecorationColor = "#fff"} onMouseLeave={e => e.target.style.textDecorationColor = "#6b7280"}>
  {children || term}
</a>;
};

> **Outcome:** Workloads can call Formal APIs with IdP JWTs and mint connector access tokens.\
> **Prerequisites:** A Formal [machine user](/docs/guides/core-concepts/identities#users), and an IdP that can mint OIDC JWTs.

## Overview

OIDC integrations let external workloads authenticate to the Formal
<G anchor="control-plane">control plane</G> 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 <G anchor="connector">Connector</G> — without
long-lived Formal API keys or machine access tokens.

Use this for remote workloads such as GitHub Actions, GitLab, or Terraform Cloud.

<Note>
  OIDC integrations are separate from [SSO](/docs/guides/integrations/sso). SSO
  covers human console login. OIDC integrations cover machine authentication to
  core APIs and optional federated connector access.
</Note>

## 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.

```mermaid theme={null}
sequenceDiagram
  participant Workload
  participant IdP as Identity Provider
  participant Formal as Formal Control Plane
  participant Connector as Formal Connector
  participant Resource

  Workload->>IdP: Request OIDC JWT
  IdP-->>Workload: JWT with aud=oidc.formal.ai/...
  Workload->>Formal: CreateFederationToken with Bearer JWT
  Formal->>Formal: Verify JWT and claim condition
  Formal-->>Workload: Federation token + username
  Workload->>Connector: Connect with username + federation token
  Connector->>Resource: Proxied access
```

## Setup in the Formal Console

1. Go to [OIDC Integrations](https://app.formal.ai/oidc-integrations).
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`.

Examples:

```cel theme={null}
true
```

```cel theme={null}
claims.repo.startsWith('org/my-repo')
```

```cel theme={null}
claims.sub == 'repo:formalco/monorepo:ref:refs/heads/main'
```

Failed, non-boolean, or invalid expressions deny authentication.

## 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

```bash theme={null}
curl -X POST \
  "https://api.joinformal.com/core.v1.ConnectorService/CreateFederationToken" \
  -H "Authorization: Bearer <OIDC_JWT>" \
  -H "Content-Type: application/json" \
  -d '{}'
```

Example response:

```json theme={null}
{
  "token": "<base64-encoded Formal JWT>",
  "username": "idp:formal:machine:ci-bot",
  "expires_at": "2026-08-06T03:00:00Z",
  "id": "fedtoken_...",
  "end_user_id": "user_..."
}
```

| Field         | Description                                    |
| ------------- | ---------------------------------------------- |
| `token`       | Connector password (base64-encoded Formal JWT) |
| `username`    | Machine user's Formal DB username              |
| `expires_at`  | Token expiry                                   |
| `id`          | Federated token ID (`fedtoken_…`)              |
| `end_user_id` | Optional Formal human resolved from claims     |

### Connect through a Connector

Use the response like any Formal username and access token:

```bash theme={null}
psql "host=<CONNECTOR_HOST> port=5432 dbname=<DB> \
  user=<USERNAME> password=<FEDERATION_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.

### Token lifetime and claims

Federation token expiry is:

```text theme={null}
min(upstream OIDC JWT exp, now + 1 hour)
```

Formal signs the federated JWT with:

| Claim                 | Value                             |
| --------------------- | --------------------------------- |
| `aud`                 | `connectors.formal.ai`            |
| `token_use`           | `federated`                       |
| `sub`                 | Machine user ID                   |
| `integration_oidc_id` | OIDC integration ID               |
| `end_user_id`         | Optional Formal human user ID     |
| `jti`                 | Federated token ID (`fedtoken_…`) |

Upstream OIDC claims are not copied into the federated JWT or synced to
Connectors.

### 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
<G anchor="end-user-identity">end-user identity</G> for policy and audit.

Examples:

```cel theme={null}
claims.owner_email
```

```cel theme={null}
claims.sub.split('/').last()
```

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:

```hcl theme={null}
resource "formal_user" "ci" {
  type = "machine"
  name = "github-actions"
}

resource "formal_integration_oidc" "github_actions" {
  name            = "GitHub Actions"
  issuer          = "https://token.actions.githubusercontent.com"
  machine_user_id = formal_user.ci.id
  claim_condition = "claims.repository.startsWith(\"formalco/\")"

  # Optional: attribute federated connector sessions to a Formal human.
  # end_user_email_expression = "claims.owner_email"

  status = "active"
}

output "oidc_audience" {
  value = formal_integration_oidc.github_actions.audience
}
```

**Verify:**

```bash theme={null}
terraform apply
terraform output oidc_audience
# Expected: oidc.formal.ai/integrationoidc_...
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="Requests return 401 Unauthorized">
    **Possible causes:**

    * Integration status is `draft` or deleted
    * Token `aud` does not exactly match `oidc.formal.ai/<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.
  </Accordion>

  <Accordion title="Discovery or JWKS fails">
    **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.
  </Accordion>

  <Accordion title="CreateFederationToken returns failed precondition">
    **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.
  </Accordion>

  <Accordion title="Connector rejects the federation token">
    **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.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Permissions" icon="key" href="/docs/guides/core-concepts/permissions">
    Authorize federated callers with claim-aware rules
  </Card>

  <Card title="Machine Users" icon="user" href="/docs/guides/core-concepts/identities#users">
    Create the machine principal OIDC tokens map to
  </Card>

  <Card title="Connectors" icon="plug" href="/docs/guides/core-concepts/connectors/introduction">
    Deploy Connectors that accept federation tokens
  </Card>

  <Card title="SSO" icon="right-to-bracket" href="/docs/guides/integrations/sso">
    Configure human console login separately
  </Card>
</CardGroup>
