Configure OpenAI
Set up credential brokering so workloads call the OpenAI API with Keycard-issued tokens instead of static API keys
Configure workload identity federation between Keycard and OpenAI. Your applications get Keycard tokens that OpenAI exchanges for short-lived access tokens, so no OpenAI API key exists anywhere.
Prerequisites
Section titled “Prerequisites”Before starting:
- A Keycard account
- The Owner role in your OpenAI organization. Workload identity is configured at the organization level, so a project owner can’t set it up.
- An OpenAI project to hold the service account your workloads act as
Configure Keycard
Section titled “Configure Keycard”-
Create the OpenAI Resource
In Keycard Console, navigate to Resources → Add Resource → Add Manually.
Field Value Identifier https://api.openai.com/v1Credential Provider Zone Provider Credential Lifetime 1h(under Advanced options)The Identifier becomes the
audclaim of the tokens Keycard issues for this Resource, and OpenAI checks it against the audience you set on its side. -
Create an Application
Navigate to Applications → Add Application.
Field Value Name for example openai-workloadIdentifier for example openai-workloadNote the Application Identifier. Keycard puts it in the
subandkeycard_app_idclaims of the tokens this Application requests for itself, and OpenAI maps thatsubto a service account. It is not the ID in the browser URL. -
Link the Resource
Open the Application → Dependencies → Add dependency → select
https://api.openai.com/v1. -
Create application credentials (local development only)
Open the Application → Application Credentials → Add credential → Client ID & Secret.
Note the Client ID and Client Secret. The secret is shown once.
-
Note your Issuer URL
Copy your Issuer URL from Keycard Console under Settings → Connection. OpenAI needs this as the issuer URL.
Configure OpenAI
Section titled “Configure OpenAI”-
Create a workload identity provider
In OpenAI Platform, go to Organization Settings → Security → Workload Identity Provider → Create workload identity provider.
Field Value Name for example keycard-prodProvider type OIDC OIDC Issuer URL your Keycard Issuer URL, for example <keycard-issuer-url>Audience https://api.openai.com/v1, the Resource Identifier from Keycard. The field suggestshttps://api.openai.com; replace it.Leave everything under Advanced off. Keycard publishes standard OIDC discovery metadata and signing keys, so OpenAI needs no custom discovery URL, uploaded JWKS or attribute transformations.
Note the provider ID (
idp_...). -
Map the Application to a service account
Open the provider → Create mapping.
Field Value Name for example keycard-workloadKey subValue your Keycard Application Identifier Project the OpenAI project your workload uses Service account select Create a new service account in this project and name it, for example keycard-workloadPermissions All, or Restricted to limit it to the endpoints your workload calls Note the service account ID (
user-...).
Use from code
Section titled “Use from code”Every OpenAI SDK takes a workload identity option with a callback that returns a subject token. Point that callback at Keycard and the SDK exchanges the Keycard token for an OpenAI token on its own.
from keycardai.oauth import BasicAuth, Clientfrom openai import OpenAI
# 1. A Keycard client for this Application.keycard = Client( "<keycard-issuer-url>", # from Settings → Connection auth=BasicAuth("<your-client-id>", "<your-client-secret>"),)
# 2. The OpenAI SDK calls this for a fresh Keycard token whenever it# exchanges or renews its OpenAI token.def keycard_token() -> str: return keycard.client_credentials_grant(resource="https://api.openai.com/v1").access_token
client = OpenAI( workload_identity={ "identity_provider_id": "<idp_...>", "service_account_id": "<user-...>", "provider": {"token_type": "jwt", "get_token": keycard_token}, },)
response = client.responses.create( model="gpt-5-mini", input="Hello from a Keycard workload",)print(response.output_text)import OpenAI from "openai";import { ClientCredentialsClient } from "@keycardai/oauth";
// 1. A Keycard client for this Application.const keycard = new ClientCredentialsClient( "<keycard-issuer-url>", // from Settings → Connection { clientId: "<your-client-id>", clientSecret: "<your-client-secret>" },);
// 2. The OpenAI SDK calls getToken for a fresh Keycard token whenever it// exchanges or renews its OpenAI token.const client = new OpenAI({ workloadIdentity: { identityProviderId: "<idp_...>", serviceAccountId: "<user-...>", provider: { tokenType: "jwt", getToken: async () => (await keycard.requestToken({ resource: "https://api.openai.com/v1" })).accessToken, }, },});
const response = await client.responses.create({ model: "gpt-5-mini", input: "Hello from a Keycard workload",});console.log(response.output_text);package main
import ( "context" "fmt" "log"
"github.com/keycardai/go-sdk/oauth" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/auth" "github.com/openai/openai-go/v3/option" "github.com/openai/openai-go/v3/responses")
// keycardTokens hands the OpenAI SDK a fresh Keycard token whenever it// exchanges or renews its OpenAI token.type keycardTokens struct { keycard *oauth.ClientCredentialsClient}
func (keycardTokens) TokenType() auth.SubjectTokenType { return auth.SubjectTokenTypeJWT }
func (k keycardTokens) GetToken(ctx context.Context, _ auth.HTTPDoer) (string, error) { token, err := k.keycard.RequestToken(ctx, oauth.ClientCredentialsRequest{ Resource: "https://api.openai.com/v1", }) if err != nil { return "", err } return token.AccessToken, nil}
func main() { ctx := context.Background()
// 1. A Keycard client for this Application. keycard := oauth.NewClientCredentialsClient( "<keycard-issuer-url>", // from Settings → Connection oauth.WithCCBasicAuth("<your-client-id>", "<your-client-secret>"), )
// 2. The OpenAI SDK exchanges the Keycard token for an OpenAI token. client := openai.NewClient( option.WithWorkloadIdentity(auth.WorkloadIdentity{ IdentityProviderID: "<idp_...>", ServiceAccountID: "<user-...>", Provider: keycardTokens{keycard: keycard}, }), )
response, err := client.Responses.New(ctx, responses.ResponseNewParams{ Model: "gpt-5-mini", Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("Hello from a Keycard workload")}, }) if err != nil { log.Fatal(err) } fmt.Println(response.OutputText())}require "openai"require "keycardai/oauth"
# Hands the OpenAI SDK a fresh Keycard token whenever it exchanges or renews# its OpenAI token.class KeycardTokens include OpenAI::Auth::SubjectTokenProvider
def initialize(keycard) @keycard = keycard end
def token_type OpenAI::Auth::TokenType::JWT end
def get_token @keycard.request_token(resource: "https://api.openai.com/v1").access_token endend
# 1. A Keycard client for this Application.keycard = Keycardai::OAuth::ClientCredentialsClient.new( issuer: "<keycard-issuer-url>", # from Settings → Connection client_id: "<your-client-id>", client_secret: "<your-client-secret>",)
# 2. The OpenAI SDK exchanges the Keycard token for an OpenAI token.client = OpenAI::Client.new( workload_identity: OpenAI::Auth::WorkloadIdentity.new( provider: KeycardTokens.new(keycard), identity_provider_id: "<idp_...>", service_account_id: "<user-...>", ),)
response = client.responses.create(model: "gpt-5-mini", input: "Hello from a Keycard workload")puts response.output_text# 1. Get a Keycard token scoped to the OpenAI API.# Replace <keycard-issuer-url> with your Issuer URL from Settings → Connection.KC_TOKEN=$(curl -sS "<keycard-issuer-url>/oauth/2/token" \ -u "<your-client-id>:<your-client-secret>" \ -d "grant_type=client_credentials" \ -d "resource=https://api.openai.com/v1" \ | jq -r .access_token)
# 2. Exchange the Keycard token for an OpenAI token.OPENAI_TOKEN=$(curl -sS https://auth.openai.com/oauth/token \ -H "content-type: application/json" \ -d "{ \"grant_type\": \"urn:ietf:params:oauth:grant-type:token-exchange\", \"subject_token\": \"${KC_TOKEN}\", \"subject_token_type\": \"urn:ietf:params:oauth:token-type:jwt\", \"identity_provider_id\": \"<idp_...>\", \"service_account_id\": \"<user-...>\" }" | jq -r .access_token)
# 3. Call the API.curl -sS https://api.openai.com/v1/responses \ -H "authorization: Bearer ${OPENAI_TOKEN}" \ -H "content-type: application/json" \ -d '{"model": "gpt-5-mini", "input": "Hello from a Keycard workload"}' \ | jq -r '[.output[] | select(.type == "message") | .content[] | select(.type == "output_text") | .text] | join("")'Verify
Section titled “Verify”In Keycard Console, open Audit Log. Look for:
| Event | Description |
|---|---|
credentials:issue |
Token issued for https://api.openai.com/v1 |
In your terminal, the code sample prints the model’s reply. A reply means OpenAI accepted the exchange.
Troubleshooting
Section titled “Troubleshooting”apiKey and workloadIdentity are mutually exclusive
The TypeScript and Ruby SDKs read OPENAI_API_KEY from the environment and refuse to start when it’s set alongside workload identity. Unset OPENAI_API_KEY.
Keycard returns access_denied: the Application “is not allowed to access” the Resource
Add https://api.openai.com/v1 as a dependency of the Application. If your Zone runs its own policy set, it also needs a permit for this Application and Resource.
OpenAI rejects the exchange
Check that the provider’s Audience is https://api.openai.com/v1, exactly the Keycard Resource Identifier, and that the OIDC Issuer URL matches your Keycard Issuer URL. The mapping’s Value must be the Application Identifier, not the ID in the browser URL. Only one enabled mapping can match an exchange.
No Workload Identity Provider settings in OpenAI Platform Workload identity is configured at the organization level. Ask an OpenAI organization owner to set it up, or to give you the Owner role.