# OpenAI Workload Identity Federation


Let a VM call the OpenAI API without storing an OpenAI API key. The VM gets a
short-lived exe.dev token, and OpenAI exchanges it for a short-lived access
token for one of your project's service accounts.

Usage is billed to your OpenAI organization. To use exe.dev's built-in models
instead, see the [LLM integration](integrations-llm).

OpenAI's documentation says that OIDC issuers other than the providers in its
setup guides
[aren't supported yet](https://developers.openai.com/api/reference/workload-identity-federation#limitations),
and asks you to contact OpenAI support if your provider isn't listed. exe.dev
is a custom OIDC issuer, so your organization may need OpenAI to enable it.

Setup uses two browser tabs: the exe.dev [Integrations page](/integrations)
and OpenAI's
[Workload Identity Provider settings](https://platform.openai.com/settings/organization/security/workload-identity-provider).
You need permission to manage Workload Identity Providers in your OpenAI
organization.

## 1. Start the integration in exe.dev

On the [Integrations page](/integrations), add an **Identity Federation**
integration and choose **OpenAI**. Enter a name, such as `openai-wif`. To use
it through an LLM integration (step 4), you don't need to attach it to any VMs.

The dialog shows an **Issuer** and a **Subject**. You will paste both into
OpenAI in the next step. Keep the dialog open; the subject is reserved for
15 minutes.

The integration's exe.dev tokens last 15 minutes, the longest allowed. An
OpenAI access token never outlives the token it was exchanged for, so a
shorter lifetime only means more frequent exchanges.

## 2. Create the provider and mapping in OpenAI

In OpenAI, open
[Workload Identity Provider settings](https://platform.openai.com/settings/organization/security/workload-identity-provider)
and create a provider:

| OpenAI field | Value |
| --- | --- |
| Name | Any unique name, such as `exe-dev` |
| OIDC Issuer URL | The exe.dev **Issuer** |
| Audience | `https://api.openai.com/v1` |
| Custom URL for OIDC discovery, uploaded JWKS | Leave both off; OpenAI uses the issuer's OIDC discovery |

Then, on the provider's details page, add a service account mapping:

| OpenAI field | Value |
| --- | --- |
| Key | `sub` |
| Value | The exe.dev **Subject**, exactly. Do not use a wildcard. |
| Project | The project that receives the API usage |
| Service account | A service account in that project; the VM acts as it |
| Permissions | Optional. Leave empty to allow everything the service account can do, or narrow it, for example to `api.model.request` and `api.model.read` |

OpenAI shows the provider's ID on its details page and the service account's
ID in the project's settings. You need both in the next step.

## 3. Enter the IDs and save

Back in the exe.dev dialog, fill in:

| Field | Where it comes from |
| --- | --- |
| Identity provider ID | The Workload Identity Provider you just created |
| Service account ID | The mapping's service account |
| Project ID (optional) | The mapping's project, `proj_...`, from the project's settings |

Click **Run**. If you don't have the IDs yet, you can save with the fields
empty and edit the integration later to add them.

The access token is always bound to the mapping's project, so the project ID
is optional. When it's set, requests send it as the `OpenAI-Project` header.
OpenAI accepts that header only if it names the token's project and rejects
any other project with `401 mismatched_project`. The header doesn't switch
projects; it makes requests fail instead of using a project you didn't expect,
for example after someone points the mapping at a different project.

## 4. Use it from an LLM integration

On the [Integrations page](/integrations), add an
[LLM integration](integrations-llm), set its OpenAI provider to
**Workload identity**, and pick this integration. Attach the LLM integration
to your VMs; the workload identity integration doesn't need to be attached.
exe.dev exchanges and renews OpenAI tokens for you and, if you set a project
ID, sends it as `OpenAI-Project`. Both integrations must be in the same scope:
personal for a personal LLM integration, team for a team one.

Or from the CLI:

```
ssh exe.dev integrations add llm --name gpt \
  --openai=wif --openai-wif=openai-wif \
  --anthropic=disabled --fireworks=disabled --attach vm:example-vm
```

On the VM, OpenAI SDKs and tools use the LLM integration's `/v1` URL as their
base URL and need no API key:

```
curl https://gpt.int.exe.xyz/v1/responses \
  -H 'content-type: application/json' \
  -d '{"model":"gpt-5.5","input":"Hello from exe.dev"}'
```

For a team integration, use `https://gpt.team.exe.xyz`. To run Codex, see
[Use with Codex](integrations-llm#use-with-codex).

## Other uses: call OpenAI directly

To call OpenAI yourself instead of through an LLM integration, for example
from code that does its own token exchange, attach this integration to the VM.
Then run this on the VM. It reads the IDs from the integration, exchanges a
fresh exe.dev token for an OpenAI access token, and sends a request:

```
EXE_WIF_URL=https://openai-wif.int.exe.xyz
META="$(curl -fsS "$EXE_WIF_URL/metadata")"
PROJECT_ID="$(echo "$META" | jq -r '.project_id // empty')"
JWT="$(curl -fsS "$EXE_WIF_URL/token" | jq -er .token)"

ACCESS_TOKEN="$(
  jq -n --arg jwt "$JWT" --argjson meta "$META" '{
    grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
    subject_token_type: "urn:ietf:params:oauth:token-type:jwt",
    subject_token: $jwt,
    identity_provider_id: $meta.identity_provider_id,
    service_account_id: $meta.service_account_id
  }' |
  curl -fsS https://auth.openai.com/oauth/token \
    -H 'content-type: application/json' -d @- |
  jq -er .access_token
)"

curl -fsS https://api.openai.com/v1/responses \
  -H "authorization: Bearer $ACCESS_TOKEN" \
  ${PROJECT_ID:+-H "OpenAI-Project: $PROJECT_ID"} \
  -H 'content-type: application/json' \
  -d '{"model":"gpt-5.5","input":"Hello from exe.dev"}'

unset JWT ACCESS_TOKEN
```

Replace `openai-wif` with your integration's name. For a team integration,
use `https://<name>.team.exe.xyz`.

### Tokens

- Fetch a new exe.dev token from `/token` for every exchange.
- Exchange again before `expires_at` (or `expires_in` seconds after the
  exchange). OpenAI returns no refresh token.
- Both tokens are credentials. Don't print, log, or commit them.

OpenAI's SDKs can do the exchange and renewal for you. See
[OpenAI's workload identity federation guide](https://developers.openai.com/api/docs/guides/workload-identity-federation)
and its
[token exchange reference](https://developers.openai.com/api/reference/workload-identity-federation).

## Use the CLI instead

The CLI prints the issuer and subject only after it creates the integration,
so add the IDs with a second command:

```
ssh exe.dev integrations add wif --name openai-wif \
  --audience https://api.openai.com/v1 --consumer openai --ttl 15m \
  --attach vm:example-vm
```

The `--attach` is only needed to call OpenAI directly from the VM. Create the
OpenAI provider and mapping with the printed **Issuer** and **Subject**, then:

```
ssh exe.dev integrations edit openai-wif \
  --metadata=identity_provider_id=IDENTITY_PROVIDER_ID \
  --metadata=service_account_id=SERVICE_ACCOUNT_ID \
  --metadata=project_id=PROJECT_ID
```

The `project_id` line is optional. Add `--team` to `add` for a team
integration. `edit` replaces all metadata, so include every ID each time.

## Troubleshooting

- **The exchange is rejected.** Check that the provider's issuer and audience
  and the mapping's `sub` value exactly match the integration, that the
  mapping is enabled, and that the request names the right provider and
  service account. OpenAI's
  [error reference](https://developers.openai.com/api/reference/workload-identity-federation#token-exchange-errors)
  lists the causes.
- **The exchange works but API calls fail.** The access token has the
  service account's project access and the mapping's permissions. Check
  those, and the project's IP allowlist if it has one. `401
  mismatched_project` means the integration's project ID isn't the
  mapping's project.
- **`/token` or `/metadata` doesn't respond.** The integration isn't attached
  to this VM.
