OpenAI Oracle Cloud 身份联合配置

2026-11-09 15:42 更新

OCI 实例主体向同租户身份域签名提交交换请求,取得 Oracle Identity Cloud Service(IDCS)访问令牌,再由 OpenAI 验证并换为映射服务账户的短期令牌。此配置无需 OpenAI API 密钥、自定义 Oracle OAuth 资源应用,或对自定义应用授予动态组权限。

设置 OCI 工作负载

在具有实例主体的 Compute 实例运行。OKE 中须确认签名身份:标准实例主体签名器通常代表工作节点,不是单个 Pod。签名器从 OCI 元数据服务取凭证,确认可达链路本地地址:

来源示例 1(bash):

## [中文] 只读访问OCI实例的链路本地元数据端点,返回实例ID;这里的Bearer Oracle是元数据请求头,不是OpenAI令牌。
curl --fail --silent \
  --header "Authorization: Bearer Oracle" \
  http://169.254.169.254/opc/v2/instance/id

元数据本身不需要 NAT 网关或互联网,但工作负载仍须能向同租户身份域发出 HTTPS 请求。

请求 Oracle 身份令牌

用 OCI Python SDK 的 InstancePrincipalsSecurityTokenSigner 签名 OAuth 交换请求:

来源示例 2(text):

中文说明:这是OCI身份域OAuth令牌交换的HTTP请求示意:展示POST地址、Content-Type及表单字段,不是Python程序,也不是独立JSON。实际请求须由实例主体签名;返回的IDCS访问令牌才作为OpenAI交换的外部主体令牌。

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

urn:opc:idm:__myscopes__ 使用实例主体现有授权。返回的 IDCS 令牌作为 OpenAI 主体令牌。不要把 Oracle audience 换为 https://api.openai.com/v1,应在 OpenAI 配置实际 Oracle 令牌包含的受众。

检查令牌

将真实 OCI 工作负载产生的令牌放入 TOKEN,本地解码:

来源示例 3(javascript):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
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);

来源示例 4(python):

## [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
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)

来源示例 5(go):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
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))
}

来源示例 6(java):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
// 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]));
  }
}

来源示例 7(csharp):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
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]));

来源示例 8(ruby):

## [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
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)

只检查不验签,令牌视为敏感信息,不写日志、不提交第三方解码器。声明示例:

来源示例 9(json):

中文说明:来源的JSON数据或配置/输出片段,保留字段和值;令牌声明中的iss/aud/sub等须对照真实工作负载,解码本身不等于验签。

{
  "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
}

以自己的身份域签发结果为准,配置精确 iss 与某个 aud。优先不可变 ipst_instance、ipst_compartment、domain_id、ca_ocid 授权。

设置身份联合

为 Oracle 身份域创建提供商,再映射可用目标服务账户的实例或 compartment。

设置工作负载身份提供商

  1. 唯一 Name 如 oracle-cloud-prod,Description 标明可信生产实例主体。
  2. OIDC Issuer URL 填实际 iss,例如 https://identity.oraclecloud.com/;Audience 取同一令牌的一个 aud。
  3. Advanced 若有 Use custom URL for OIDC discovery,则启用并填租户身份域,如 https://idcs-example.identity.oraclecloud.com,OpenAI 获取其 /.well-known/openid-configuration 和 jwks_uri。没有该选项时启用上传 JWKS,上传 https://<identity-domain>/admin/v1/SigningCert/jwk 的公开对象。
  4. ipst_instance、ipst_compartment、domain_id、ca_ocid 可直接匹配;仅按需派生,例如名称 instance 配 assertion.ipst_instance 产生 openai.instance。

Oracle 发现参考中,文档可能声明全球 issuer、但端点和公钥在租户域。Issuer 保留全球值,发现 URL 使用租户域。若 issuer 自身提供可用发现,则关闭自定义发现使用标准模式;无法访问租户文档/公钥时关闭自定义发现并上传公有 JWKS。两种模式互斥,Oracle 轮换签名时更新上传密钥。

设置服务账户映射

  1. 创建唯一名称如 oracle-instance-prod,描述工作负载。
  2. 最窄稳定身份:单实例用 ipst_instance 精确 OCID;只有 compartment 内所有合格实例都应共享访问时才用 ipst_compartment。
  3. 按需加 domain_id、ca_ocid 限定身份域/租户;真实令牌有此声明且需实例主体时,加 sub_type = instance。所有行必须匹配。
  4. 选择拥有账户的 Project 和目标 Service account。
  5. Permissions 仅选必要值,可收窄但不能扩大服务账户权限。

OKE 标准签名器继承节点身份。实例映射授权的是节点,不是独立 Pod;共享节点下需要 Pod 隔离时,应选择更具体且受支持的 OCI 工作负载身份。

在代码中使用令牌

Python 安装 OpenAI、OCI 和 Requests:

来源示例 10(bash):

## [中文] 使用pip安装openai、oci和requests三个Python依赖;本命令不获取身份或交换令牌。
pip install openai oci requests

Ruby 安装 OpenAI 与 OCI gems:

来源示例 11(bash):

## [中文] 使用gem安装openai和oci两个Ruby依赖;本命令不获取身份或交换令牌。
gem install openai oci

OCI_IDENTITY_DOMAIN_URL 为同租户身份域基址,OPENAI_IDENTITY_PROVIDER_ID、OPENAI_SERVICE_ACCOUNT_ID 为配置的 OpenAI ID。示例由实例主体签名 Oracle 请求,向 SDK 返回 IDCS 令牌,再按需交换短期 OpenAI 令牌:

来源示例 12(python):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
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)

来源示例 13(ruby):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
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)

SDK 续期身份凭证时,主体提供函数重新请求新 Oracle 令牌,不打印或持久化两种令牌。

OCI 安全建议

单工作负载优先精确 ipst_instance;仅有意共享时用 compartment;用 domain_id/ca_ocid 限边界;各应用/环境独立 OpenAI 服务账户;先确认 OKE 是否节点身份再依赖 Pod 隔离;受众取真实 Oracle 令牌;不能发现时,随 Oracle 签名轮换更新上传公钥。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号