SpyBara
Go Premium

guides/workload-identity-federation/oracle-cloud.md 2026-09-17 10:04 UTC to 2026-09-18 22:59 UTC

This page contains 0 additions and 2 deletions.

2026
Fri 4 23:59 Fri 11 20:00 Fri 18 22:59

Configuring workload identity federation for Oracle Cloud Infrastructure

For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.

Use Oracle Cloud Infrastructure (OCI) as a Workload Identity Provider by exchanging an Oracle Identity Cloud Service (IDCS) access token for a short-lived OpenAI access token. An OCI instance principal signs a token exchange request to an identity domain in the same tenancy. OpenAI validates the resulting token and authorizes the OCI workload to act as a mapped OpenAI service account.

This setup does not require an OpenAI API key, a custom Oracle OAuth resource application, or dynamic group grants to a custom application.

Set up the OCI workload

Run your workload on an OCI Compute instance with an instance principal. For Oracle Kubernetes Engine (OKE), confirm which identity signs the request: the standard instance principal signer typically identifies the worker node, not an individual Kubernetes pod.

The signer obtains credentials from the OCI instance metadata service. Verify the workload can reach the link-local metadata endpoint:

curl --fail --silent \
  --header "Authorization: Bearer Oracle" \
  http://169.254.169.254/opc/v2/instance/id

The workload must also be able to make outbound HTTPS requests to the identity domain in its tenancy. The metadata endpoint itself does not require a NAT gateway or an internet connection.

Request an Oracle identity token

Use InstancePrincipalsSecurityTokenSigner from the OCI Python SDK to sign an OAuth token exchange request to your identity domain:

POST https://<identity-domain>/oauth2/v1/token
Content-Type: application/x-www-form-urlencoded;charset=utf-8

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
scope=urn:opc:idm:__myscopes__
requested_token_type=urn:ietf:params:oauth:token-type:access_token

The urn:opc:idm:__myscopes__ scope uses the instance principal's existing authorization. Use the returned IDCS access token as the subject token for OpenAI workload identity federation. Do not replace the Oracle token audience with https://api.openai.com/v1; configure the OpenAI provider with an audience that appears in the actual Oracle token.

Verify the token

Set TOKEN to an access token generated by the actual OCI workload, then use the existing local JWT decoder to inspect its claims:

const parts = process.env.TOKEN?.split(".") ?? [];
if (parts.length !== 3) {
  throw new Error("Expected a compact JWT with three segments");
}
if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {
  throw new Error("JWT payload is not valid Base64URL");
}

const bytes = Buffer.from(parts[1], "base64url");
if (bytes.toString("base64url") !== parts[1]) {
  throw new Error("JWT payload is not valid Base64URL");
}
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const claims = JSON.parse(decoded);
if (claims === null || Array.isArray(claims) || typeof claims !== "object") {
  throw new Error("JWT payload is not a JSON object");
}
console.log(decoded);
import base64
import json
import os
import re


def reject_non_json_constant(value):
    raise ValueError(f"JWT payload contains non-JSON constant: {value}")


parts = os.environ.get("TOKEN", "").split(".")
if len(parts) != 3:
    raise ValueError("Expected a compact JWT with three segments")

payload = parts[1]
if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:
    raise ValueError("JWT payload is not valid Base64URL")
padded_payload = payload + "=" * (-len(payload) % 4)
decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)
if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:
    raise ValueError("JWT payload is not valid Base64URL")
decoded_text = decoded.decode("utf-8")
claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)
if not isinstance(claims, dict):
    raise ValueError("JWT payload is not a JSON object")
print(decoded_text)
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"unicode/utf8"
)

func decodeSegment(segment string) (json.RawMessage, error) {
	if !isBase64URLSegment(segment) {
		return nil, fmt.Errorf("JWT segment is not valid Base64URL")
	}
	decoded, err := base64.RawURLEncoding.DecodeString(segment)
	if err != nil {
		return nil, err
	}
	if base64.RawURLEncoding.EncodeToString(decoded) != segment {
		return nil, fmt.Errorf("JWT segment is not valid Base64URL")
	}
	if !utf8.Valid(decoded) {
		return nil, fmt.Errorf("JWT segment is not valid UTF-8")
	}

	var value json.RawMessage
	if err := json.Unmarshal(decoded, &value); err != nil {
		return nil, err
	}
	if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {
		return nil, fmt.Errorf("JWT segment is not a JSON object")
	}
	return value, nil
}

func isBase64URLSegment(segment string) bool {
	if segment == "" || len(segment)%4 == 1 {
		return false
	}
	for _, character := range segment {
		if !('A' <= character && character <= 'Z') &&
			!('a' <= character && character <= 'z') &&
			!('0' <= character && character <= '9') &&
			character != '-' &&
			character != '_' {
			return false
		}
	}
	return true
}

func main() {
	parts := strings.Split(os.Getenv("TOKEN"), ".")
	if len(parts) != 3 {
		panic("Expected a compact JWT with three segments")
	}

	payload, err := decodeSegment(parts[1])
	if err != nil {
		panic(err)
	}
	formatted, err := json.MarshalIndent(payload, "", "  ")
	if err != nil {
		panic(err)
	}
	fmt.Println(string(formatted))
}
// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public final class DecodeJwtPayloadExample {
  private static final ObjectMapper JSON =
      new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);

  private DecodeJwtPayloadExample() {}

  static String decodeUtf8(byte[] bytes) throws IOException {
    try {
      return StandardCharsets.UTF_8
          .newDecoder()
          .onMalformedInput(CodingErrorAction.REPORT)
          .onUnmappableCharacter(CodingErrorAction.REPORT)
          .decode(ByteBuffer.wrap(bytes))
          .toString();
    } catch (CharacterCodingException exception) {
      throw new IOException("JWT segment is not valid UTF-8", exception);
    }
  }

  static String decodeSegment(String segment) throws IOException {
    if (!isBase64UrlSegment(segment)) {
      throw new IllegalArgumentException("JWT segment is not valid Base64URL");
    }
    byte[] bytes = Base64.getUrlDecoder().decode(segment);
    if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {
      throw new IllegalArgumentException("JWT segment is not valid Base64URL");
    }
    String decoded = decodeUtf8(bytes);
    JsonNode value = JSON.readTree(decoded);
    if (value == null || value.isMissingNode() || !value.isObject()) {
      throw new IOException("JWT segment is not a JSON object");
    }
    return decoded;
  }

  static boolean isBase64UrlSegment(String segment) {
    if (segment.isEmpty() || segment.length() % 4 == 1) {
      return false;
    }
    return segment
        .chars()
        .allMatch(
            character ->
                character >= 'A' && character <= 'Z'
                    || character >= 'a' && character <= 'z'
                    || character >= '0' && character <= '9'
                    || character == '-'
                    || character == '_');
  }

  static String[] requireCompactJwt(String token) {
    if (token == null) {
      throw new IllegalArgumentException("Expected a compact JWT with three segments");
    }
    String[] parts = token.split("\\.", -1);
    if (parts.length != 3) {
      throw new IllegalArgumentException("Expected a compact JWT with three segments");
    }
    return parts;
  }

  public static void main(String[] args) throws IOException {
    String[] parts = requireCompactJwt(System.getenv("TOKEN"));
    System.out.println(decodeSegment(parts[1]));
  }
}
using System.Text;
using System.Text.Json;

static string DecodeSegment(string segment)
{
    if (
        segment.Length % 4 == 1 ||
        segment.Any(
            character =>
                !(
                    character is >= 'A' and <= 'Z' ||
                    character is >= 'a' and <= 'z' ||
                    character is >= '0' and <= '9' ||
                    character is '-' or '_'
                )
        )
    )
    {
        throw new FormatException("JWT segment is not valid Base64URL");
    }

    byte[] decoded = Convert.FromBase64String(
        segment.Replace('-', '+').Replace('_', '/') +
        new string('=', (4 - segment.Length % 4) % 4)
    );
    string canonicalSegment = Convert
        .ToBase64String(decoded)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');
    if (canonicalSegment != segment)
    {
        throw new FormatException("JWT segment is not valid Base64URL");
    }
    string decodedJson = new UTF8Encoding(false, true).GetString(decoded);
    using JsonDocument document = JsonDocument.Parse(decodedJson);
    if (document.RootElement.ValueKind is not JsonValueKind.Object)
    {
        throw new FormatException("JWT segment is not a JSON object");
    }
    return decodedJson;
}

string? token = Environment.GetEnvironmentVariable("TOKEN");
if (token is null)
{
    throw new InvalidOperationException(
        "Expected a compact JWT with three segments"
    );
}
string[] parts = token.Split('.');
if (parts.Length != 3)
{
    throw new InvalidOperationException(
        "Expected a compact JWT with three segments"
    );
}

Console.WriteLine(DecodeSegment(parts[1]));
require "base64"
require "json"

parts = ENV.fetch("TOKEN", "").split(".", -1)
raise "Expected a compact JWT with three segments" unless parts.length == 3

unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1
  raise "JWT payload is not valid Base64URL"
end

begin
  payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))
rescue ArgumentError
  raise "JWT payload is not valid Base64URL"
end
unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]
  raise "JWT payload is not valid Base64URL"
end

payload.force_encoding(Encoding::UTF_8)
raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?

claims = JSON.parse(payload)
raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)

puts(payload)

The decoder inspects the token without verifying its signature. Treat raw tokens as sensitive, do not log them, and do not paste production tokens into third-party JWT decoders.

A decoded Oracle access token can contain the following claims:

{
  "iss": "https://identity.oraclecloud.com/",
  "aud": [
    "https://idcs-example.us-phoenix-1.identity.oraclecloud.com",
    "https://idcs-example.identity.oraclecloud.com"
  ],
  "sub_type": "instance",
  "ipst_instance": "ocid1.instance.oc1.phx.<instance-id>",
  "ipst_compartment": "ocid1.compartment.oc1..<compartment-id>",
  "domain_id": "ocid1.domain.oc1..<domain-id>",
  "ca_ocid": "ocid1.tenancy.oc1..<tenancy-id>",
  "tenant": "idcs-example",
  "exp": 1782369434,
  "iat": 1782365834
}

Use the token issued by your own identity domain as the source of truth. Configure the exact iss value and one of the token's aud values. Prefer the immutable ipst_instance, ipst_compartment, domain_id, and ca_ocid claims when authorizing a workload.

Set up workload identity federation

Create a Workload Identity Provider for your Oracle identity domain, then add a mapping for the OCI instance or compartment that can use the target OpenAI service account.

Set up the Workload Identity Provider

  1. Create the Workload Identity Provider. Set Name to a unique value, such as oracle-cloud-prod. Use Description, such as Production OCI instance principal, to identify the trusted workload.

  2. Set the issuer and audience. Set OIDC Issuer URL to the token's iss claim, such as https://identity.oraclecloud.com/. Set Audience to one of the aud values in the same token.

  3. Configure tenant-specific OIDC discovery when available. If Use custom URL for OIDC discovery appears under Advanced, enable it. Set Custom OIDC discovery URL to your tenant-specific identity domain, such as https://idcs-example.identity.oraclecloud.com. OpenAI retrieves https://idcs-example.identity.oraclecloud.com/.well-known/openid-configuration, then uses the discovery document's jwks_uri to retrieve the tenant's public signing keys. If the custom discovery option does not appear, enable Use uploaded JWKS for token verification and upload the public JWKS from https://<identity-domain>/admin/v1/SigningCert/jwk instead.

  4. Add attribute transformations only when you need derived attributes. You can use raw Oracle claims such as ipst_instance, ipst_compartment, domain_id, and ca_ocid directly in service account mapping assertions. For an explicitly derived instance attribute, enter instance with the expression assertion.ipst_instance to create openai.instance.

Oracle's OpenID Connect discovery reference shows why custom discovery is important: the discovery document can declare the global issuer https://identity.oraclecloud.com/ while publishing the token endpoint and jwks_uri on the tenant-specific identity domain. Keep the global issuer in OIDC Issuer URL and use the tenant domain for Custom OIDC discovery URL.

If your identity domain publishes discovery metadata at the token issuer, leave custom discovery disabled and use standard OIDC discovery. If OpenAI cannot reach the tenant discovery document or signing-key endpoint, disable custom discovery, enable Use uploaded JWKS for token verification, and upload the tenant's public JWKS from https://<identity-domain>/admin/v1/SigningCert/jwk. Custom discovery and uploaded JWKS cannot be enabled at the same time. Update uploaded keys when Oracle rotates its signing certificates.

Set up the service account mapping

  1. Create a service account mapping. Set Name to a unique value, such as oracle-instance-prod, and add a description that identifies the trusted OCI workload.

  2. Match the narrowest stable OCI identity. To grant access to one instance, set Key to ipst_instance and Value to the exact instance OCID from the verified token. To grant access to instances across one compartment, set Key to ipst_compartment and Value to the exact compartment OCID.

  3. Add domain and tenancy boundaries when needed. Add further mapping rows for domain_id or ca_ocid to limit the workload to a particular Oracle identity domain or tenancy. Add sub_type with the value instance when the token includes that claim and you want to require an instance principal. All mapping rows must match.

  4. Choose the OpenAI target. Set Project to the project that owns the service account, then select the Service account that the trusted OCI workload can use.

  5. Narrow API permissions if needed. Select only the Permissions needed by the workload. Mapping permissions can restrict the selected service account but cannot grant permissions the service account does not already have.

An OKE workload that uses the standard instance principal signer inherits the worker node's identity. An instance-level mapping authorizes that node, not just one pod. Use a more specific, supported OCI workload identity when you need isolation between pods sharing a worker node.

Use the token in code

Install the OpenAI, OCI, and Requests Python packages:

pip install openai oci requests

For Ruby, install the OpenAI and OCI gems:

gem install openai oci

Set OCI_IDENTITY_DOMAIN_URL to the base URL of the identity domain in the same tenancy as the workload. Set OPENAI_IDENTITY_PROVIDER_ID and OPENAI_SERVICE_ACCOUNT_ID to the IDs from your OpenAI provider and service account mapping.

The following example signs an Oracle token exchange request with the OCI instance principal, returns the IDCS access token to the OpenAI SDK, and lets the SDK exchange it for a short-lived OpenAI access token when needed:

Authenticate with an OCI instance principal

import os

import oci
import requests
from openai import OpenAI
from openai.auth import SubjectTokenProvider


def oracle_instance_principal_token_provider(
    identity_domain_url: str,
) -> SubjectTokenProvider:
    def get_token() -> str:
        signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner()
        response = requests.post(
            f"{identity_domain_url.rstrip('/')}/oauth2/v1/token",
            data={
                "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
                "scope": "urn:opc:idm:__myscopes__",
                "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
            },
            headers={
                "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
            },
            auth=signer,
            timeout=30,
        )
        response.raise_for_status()

        token = response.json().get("access_token")
        if not isinstance(token, str) or not token:
            raise RuntimeError("Oracle IDCS did not return an access token.")

        return token

    return {"token_type": "jwt", "get_token": get_token}


client = OpenAI(
    workload_identity={
        "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],
        "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],
        "provider": oracle_instance_principal_token_provider(
            os.environ["OCI_IDENTITY_DOMAIN_URL"]
        ),
    },
)

response = client.responses.create(
    model="gpt-5.6-terra",
    input="Say hello from Oracle Cloud Infrastructure workload identity federation.",
)

print(response.output_text)
require "json"
require "net/http"
require "oci"
require "openai"
require "uri"

class OracleInstancePrincipalTokenProvider
  include OpenAI::Auth::SubjectTokenProvider

  def initialize(identity_domain_url:)
    @identity_domain_url = identity_domain_url.sub(%r{/+\z}, "")
  end

  def token_type
    OpenAI::Auth::TokenType::JWT
  end

  def get_token
    uri = URI("#{@identity_domain_url}/oauth2/v1/token")
    unless uri.is_a?(URI::HTTPS)
      raise OpenAI::Errors::SubjectTokenProviderError.new(
        message: "Oracle identity domain URL must use HTTPS",
        provider: "oracle-instance-principal"
      )
    end

    body = URI.encode_www_form(
      grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
      scope: "urn:opc:idm:__myscopes__",
      requested_token_type: "urn:ietf:params:oauth:token-type:access_token"
    )
    headers = {
      "content-type": "application/x-www-form-urlencoded;charset=utf-8"
    }

    signer = OCI::Auth::Signers::InstancePrincipalsSecurityTokenSigner.new
    signer.sign(:post, uri.to_s, headers, body)

    request = Net::HTTP::Post.new(uri)
    headers.each { |name, value| request[name.to_s] = value }
    request.body = body

    response = Net::HTTP.start(
      uri.hostname,
      uri.port,
      use_ssl: true,
      open_timeout: 10,
      read_timeout: 30
    ) do |http|
      http.request(request)
    end

    unless response.is_a?(Net::HTTPSuccess)
      raise OpenAI::Errors::SubjectTokenProviderError.new(
        message: "Oracle identity token request failed with status #{response.code}",
        provider: "oracle-instance-principal"
      )
    end

    token = JSON.parse(response.body).fetch("access_token")
    unless token.is_a?(String) && !token.empty?
      raise OpenAI::Errors::SubjectTokenProviderError.new(
        message: "Oracle identity domain did not return an access token",
        provider: "oracle-instance-principal"
      )
    end

    token
  rescue JSON::ParserError
    raise OpenAI::Errors::SubjectTokenProviderError.new(
      message: "Oracle identity token response was not valid JSON",
      provider: "oracle-instance-principal"
    ), cause: nil
  rescue KeyError
    raise OpenAI::Errors::SubjectTokenProviderError.new(
      message: "Oracle identity domain did not return an access token",
      provider: "oracle-instance-principal"
    ), cause: nil
  rescue SystemCallError, Timeout::Error => error
    raise OpenAI::Errors::SubjectTokenProviderError.new(
      message: "Failed to request Oracle identity token: #{error.message}",
      provider: "oracle-instance-principal",
      cause: error
    )
  end
end

provider = OracleInstancePrincipalTokenProvider.new(
  identity_domain_url: ENV.fetch("OCI_IDENTITY_DOMAIN_URL")
)

workload_identity = OpenAI::Auth::WorkloadIdentity.new(
  identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),
  service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),
  provider: provider
)

client = OpenAI::Client.new(workload_identity: workload_identity)

response = client.responses.create(
  model: "gpt-5.6-terra",
  input: "Say hello from Oracle Cloud Infrastructure workload identity federation."
)

puts(response.output_text)

The subject token provider requests a fresh Oracle token when the OpenAI SDK needs to renew the workload identity credential. Never print or persist the Oracle subject token or the resulting OpenAI access token.

OCI security recommendations

  • Map one instance with ipst_instance when only one workload should have access.
  • Use ipst_compartment only when every eligible instance in that compartment should share the mapping.
  • Add domain_id or ca_ocid to enforce identity domain and tenancy boundaries.
  • Use a separate OpenAI service account for each application and environment.
  • Verify whether an OKE token represents a worker node before relying on pod-level isolation.
  • Use the audience present in the issued Oracle token rather than assuming an OpenAI-specific audience.
  • Rotate uploaded public keys when Oracle rotates its signing keys if your identity domain cannot use OIDC discovery.