SPB Git

Repository Public

github-actions.md 31.6 KB 897 lines markdown

1# Configuring workload identity federation for GitHub Actions23> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.45Use GitHub Actions as a Workload Identity Provider by exchanging a GitHub-issued OIDC token for a short-lived OpenAI access token. This lets workflows authenticate to the OpenAI API without storing a long-lived API key in GitHub secrets.67GitHub can mint a signed OIDC JWT for a workflow job that has `id-token: write` permission and requests an identity token. OpenAI validates the token issuer, audience, signature, and mapping attributes before issuing an OpenAI access token.89## Setting up GitHub Actions1011Grant the workflow or job permission to request a GitHub OIDC token:1213```yaml14permissions:15  id-token: write16  contents: read17```1819The `id-token: write` permission lets the job request an OIDC JWT. It does not grant write access to repository contents. The `contents: read` permission is needed by `actions/checkout`.2021Request the token with the exact audience configured in your OpenAI Workload Identity Provider. Custom JavaScript actions can call `core.getIDToken("your-wif-audience")`; shell steps can call GitHub's OIDC request URL directly. Audience values containing reserved URL characters, such as `https://api.openai.com/v1`, should be URL encoded before being appended to the request URL:2223```bash24AUDIENCE="https://api.openai.com/v1"25ENCODED_AUDIENCE=$(jq -rn --arg audience "$AUDIENCE" '$audience | @uri')2627TOKEN=$(curl -sSf -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \28  "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${ENCODED_AUDIENCE}" | jq -r .value)29export TOKEN30```3132Important GitHub OIDC claims include:3334- `iss`: The token issuer. For GitHub Actions, this is `https://token.actions.githubusercontent.com`.35- `aud`: The audience value requested by the workflow. Configure OpenAI to require the exact value you request, such as `your-wif-audience` or `https://api.openai.com/v1`.36- `sub`: The main subject string. GitHub builds it from workflow metadata such as repository, branch, tag, pull request, or environment.37- `repository`: The repository running the workflow, such as `my-org/my-repo`.38- `repository_owner`: The organization or user that owns the repository, such as `my-org`.39- `ref`: The Git ref that triggered the workflow, such as `refs/heads/main` or `refs/tags/v1.0.0`.40- `workflow`: The workflow claim. Use the actual claim value emitted by GitHub, such as `deploy` if that is the workflow claim in your job.41- `workflow_ref`: The workflow file path and ref, such as `my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main`.42- `environment`: The GitHub environment name, such as `production`, when the job uses an environment.43- `run_id`, `run_number`, `run_attempt`, and `job_workflow_ref`: Run and job identifiers that can help with auditing or more advanced trust rules.4445For the full claim list and subject formats, see GitHub's [OpenID Connect reference](https://docs.github.com/en/actions/reference/security/oidc).4647## Verify the token4849Before configuring workload identity federation, export the GitHub OIDC token as `TOKEN`, then run this script in the workflow runner to inspect its claims:5051```javascript52const parts = process.env.TOKEN?.split(".") ?? [];53if (parts.length !== 3) {54  throw new Error("Expected a compact JWT with three segments");55}56if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {57  throw new Error("JWT payload is not valid Base64URL");58}5960const bytes = Buffer.from(parts[1], "base64url");61if (bytes.toString("base64url") !== parts[1]) {62  throw new Error("JWT payload is not valid Base64URL");63}64const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);65const claims = JSON.parse(decoded);66if (claims === null || Array.isArray(claims) || typeof claims !== "object") {67  throw new Error("JWT payload is not a JSON object");68}69console.log(decoded);70```7172```python73import base6474import json75import os76import re777879def reject_non_json_constant(value):80    raise ValueError(f"JWT payload contains non-JSON constant: {value}")818283parts = os.environ.get("TOKEN", "").split(".")84if len(parts) != 3:85    raise ValueError("Expected a compact JWT with three segments")8687payload = parts[1]88if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:89    raise ValueError("JWT payload is not valid Base64URL")90padded_payload = payload + "=" * (-len(payload) % 4)91decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)92if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:93    raise ValueError("JWT payload is not valid Base64URL")94decoded_text = decoded.decode("utf-8")95claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)96if not isinstance(claims, dict):97    raise ValueError("JWT payload is not a JSON object")98print(decoded_text)99```100101```go102package main103104import (105	"bytes"106	"encoding/base64"107	"encoding/json"108	"fmt"109	"os"110	"strings"111	"unicode/utf8"112)113114func decodeSegment(segment string) (json.RawMessage, error) {115	if !isBase64URLSegment(segment) {116		return nil, fmt.Errorf("JWT segment is not valid Base64URL")117	}118	decoded, err := base64.RawURLEncoding.DecodeString(segment)119	if err != nil {120		return nil, err121	}122	if base64.RawURLEncoding.EncodeToString(decoded) != segment {123		return nil, fmt.Errorf("JWT segment is not valid Base64URL")124	}125	if !utf8.Valid(decoded) {126		return nil, fmt.Errorf("JWT segment is not valid UTF-8")127	}128129	var value json.RawMessage130	if err := json.Unmarshal(decoded, &value); err != nil {131		return nil, err132	}133	if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {134		return nil, fmt.Errorf("JWT segment is not a JSON object")135	}136	return value, nil137}138139func isBase64URLSegment(segment string) bool {140	if segment == "" || len(segment)%4 == 1 {141		return false142	}143	for _, character := range segment {144		if !('A' <= character && character <= 'Z') &&145			!('a' <= character && character <= 'z') &&146			!('0' <= character && character <= '9') &&147			character != '-' &&148			character != '_' {149			return false150		}151	}152	return true153}154155func main() {156	parts := strings.Split(os.Getenv("TOKEN"), ".")157	if len(parts) != 3 {158		panic("Expected a compact JWT with three segments")159	}160161	payload, err := decodeSegment(parts[1])162	if err != nil {163		panic(err)164	}165	formatted, err := json.MarshalIndent(payload, "", "  ")166	if err != nil {167		panic(err)168	}169	fmt.Println(string(formatted))170}171```172173```java174// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.175import com.fasterxml.jackson.databind.DeserializationFeature;176import com.fasterxml.jackson.databind.JsonNode;177import com.fasterxml.jackson.databind.ObjectMapper;178import java.io.IOException;179import java.nio.ByteBuffer;180import java.nio.charset.CharacterCodingException;181import java.nio.charset.CodingErrorAction;182import java.nio.charset.StandardCharsets;183import java.util.Base64;184185public final class DecodeJwtPayloadExample {186  private static final ObjectMapper JSON =187      new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);188189  private DecodeJwtPayloadExample() {}190191  static String decodeUtf8(byte[] bytes) throws IOException {192    try {193      return StandardCharsets.UTF_8194          .newDecoder()195          .onMalformedInput(CodingErrorAction.REPORT)196          .onUnmappableCharacter(CodingErrorAction.REPORT)197          .decode(ByteBuffer.wrap(bytes))198          .toString();199    } catch (CharacterCodingException exception) {200      throw new IOException("JWT segment is not valid UTF-8", exception);201    }202  }203204  static String decodeSegment(String segment) throws IOException {205    if (!isBase64UrlSegment(segment)) {206      throw new IllegalArgumentException("JWT segment is not valid Base64URL");207    }208    byte[] bytes = Base64.getUrlDecoder().decode(segment);209    if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {210      throw new IllegalArgumentException("JWT segment is not valid Base64URL");211    }212    String decoded = decodeUtf8(bytes);213    JsonNode value = JSON.readTree(decoded);214    if (value == null || value.isMissingNode() || !value.isObject()) {215      throw new IOException("JWT segment is not a JSON object");216    }217    return decoded;218  }219220  static boolean isBase64UrlSegment(String segment) {221    if (segment.isEmpty() || segment.length() % 4 == 1) {222      return false;223    }224    return segment225        .chars()226        .allMatch(227            character ->228                character >= 'A' && character <= 'Z'229                    || character >= 'a' && character <= 'z'230                    || character >= '0' && character <= '9'231                    || character == '-'232                    || character == '_');233  }234235  static String[] requireCompactJwt(String token) {236    if (token == null) {237      throw new IllegalArgumentException("Expected a compact JWT with three segments");238    }239    String[] parts = token.split("\\.", -1);240    if (parts.length != 3) {241      throw new IllegalArgumentException("Expected a compact JWT with three segments");242    }243    return parts;244  }245246  public static void main(String[] args) throws IOException {247    String[] parts = requireCompactJwt(System.getenv("TOKEN"));248    System.out.println(decodeSegment(parts[1]));249  }250}251```252253```csharp254using System.Text;255using System.Text.Json;256257static string DecodeSegment(string segment)258{259    if (260        segment.Length % 4 == 1 ||261        segment.Any(262            character =>263                !(264                    character is >= 'A' and <= 'Z' ||265                    character is >= 'a' and <= 'z' ||266                    character is >= '0' and <= '9' ||267                    character is '-' or '_'268                )269        )270    )271    {272        throw new FormatException("JWT segment is not valid Base64URL");273    }274275    byte[] decoded = Convert.FromBase64String(276        segment.Replace('-', '+').Replace('_', '/') +277        new string('=', (4 - segment.Length % 4) % 4)278    );279    string canonicalSegment = Convert280        .ToBase64String(decoded)281        .TrimEnd('=')282        .Replace('+', '-')283        .Replace('/', '_');284    if (canonicalSegment != segment)285    {286        throw new FormatException("JWT segment is not valid Base64URL");287    }288    string decodedJson = new UTF8Encoding(false, true).GetString(decoded);289    using JsonDocument document = JsonDocument.Parse(decodedJson);290    if (document.RootElement.ValueKind is not JsonValueKind.Object)291    {292        throw new FormatException("JWT segment is not a JSON object");293    }294    return decodedJson;295}296297string? token = Environment.GetEnvironmentVariable("TOKEN");298if (token is null)299{300    throw new InvalidOperationException(301        "Expected a compact JWT with three segments"302    );303}304string[] parts = token.Split('.');305if (parts.Length != 3)306{307    throw new InvalidOperationException(308        "Expected a compact JWT with three segments"309    );310}311312Console.WriteLine(DecodeSegment(parts[1]));313```314315```ruby316require "base64"317require "json"318319parts = ENV.fetch("TOKEN", "").split(".", -1)320raise "Expected a compact JWT with three segments" unless parts.length == 3321322unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1323  raise "JWT payload is not valid Base64URL"324end325326begin327  payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))328rescue ArgumentError329  raise "JWT payload is not valid Base64URL"330end331unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]332  raise "JWT payload is not valid Base64URL"333end334335payload.force_encoding(Encoding::UTF_8)336raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?337338claims = JSON.parse(payload)339raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)340341puts(payload)342```343344345This command decodes the JWT payload without verifying the token signature. Use a local decoder for production tokens, and avoid pasting production tokens into third-party tools. Never log the raw GitHub OIDC token or the exchanged OpenAI access token.346347A decoded GitHub Actions OIDC token will look similar to:348349```json350{351  "iss": "https://token.actions.githubusercontent.com",352  "aud": "https://api.openai.com/v1",353  "sub": "repo:my-org/my-repo:environment:production",354  "repository": "my-org/my-repo",355  "repository_owner": "my-org",356  "ref": "refs/heads/main",357  "workflow": "deploy",358  "workflow_ref": "my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main",359  "environment": "production",360  "run_id": "1234567890",361  "run_attempt": "1"362}363```364365Use the decoded payload to compare the token you received with the issuer, audience, and mapping values configured in OpenAI. Most configuration issues are visible in the `iss`, `aud`, `repository`, `ref`, and `workflow_ref` claims before you exchange the token.366367## Setting up workload identity federation368369Create a Workload Identity Provider in OpenAI for GitHub Actions, then add a service account mapping that matches the GitHub workflow claims you trust.370371Configure the Workload Identity Provider first, then create the service account mapping.372373### Set up the Workload Identity Provider3743751. **Create the Workload Identity Provider.** Set **Name** to a unique value, such as `github-actions-prod`. Use **Description**, such as `Production GitHub Actions workflows`, to help admins identify the provider.3763772. **Set the issuer and audience.** Set **OIDC Issuer URL** to `https://token.actions.githubusercontent.com`. Set **Audience** to the exact audience your workflow requests, such as `your-wif-audience` or `https://api.openai.com/v1`.3783793. **Use GitHub OIDC discovery.** Leave **Use uploaded JWKS for token verification** disabled. OpenAI uses GitHub's OIDC discovery metadata and JWKS to verify the GitHub-signed token.3803814. **Add attribute transformations only if you need derived mapping attributes.** Raw GitHub claims such as `repository`, `ref`, and `workflow` can be used directly in mapping assertions. If you create derived attributes, the dashboard applies the `openai.` prefix automatically; for example, enter `github_repository` with expression `assertion.repository` to create `openai.github_repository`. Raw token claims that already start with `openai.` are ignored for `openai.` mapping keys unless a matching transformation is configured.382383### Set up the service account mapping3843851. **Create a service account mapping.** Set **Name** to a unique value within the Workload Identity Provider, such as `github-actions-main-deploy`. Use **Description**, such as `Production deploy workflow on main`, to explain which workflow can use the mapping.3863872. **Add exact claim assertions.** Add one **Key** and **Value** row for each GitHub claim that must match. OpenAI requires every configured row to match before it issues an access token. For a production deploy workflow, use assertions like:388389```text390   iss == "https://token.actions.githubusercontent.com"391   aud == "https://api.openai.com/v1"392   repository == "my-org/my-repo"393   ref == "refs/heads/main"394   workflow_ref == "my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main"395```396397   Prefer `workflow_ref` over `workflow` for privileged mappings because admins usually intend to trust a specific workflow file path and ref. Workflow names can be renamed, and multiple workflow files can share the same name.398399   In the mapping UI, enter these as key/value rows, such as **Key** `repository` with **Value** `my-org/my-repo`, **Key** `ref` with **Value** `refs/heads/main`, and **Key** `workflow_ref` with **Value** `my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main`. If the job uses a GitHub environment, also add **Key** `environment` with **Value** `production`.400401   > **Caution:** Avoid overly broad mappings, such as trusting only `repository_owner == "my-org"`, unless every repository in that owner namespace should be able to mint OpenAI access tokens.4024033. **Choose the OpenAI target.** Set **Project** to the OpenAI project that owns the target service account. Set **Service account** to the OpenAI service account the GitHub workflow can use, such as `github-actions-prod-deploy`.4044054. **Narrow API permissions if needed.** Select appropriate **Permissions** such as `api.model.request` and `api.vector_store.read` to further narrow access tokens minted from this mapping. Leave permissions blank to avoid adding a WIF-specific scope restriction; the token still authorizes as the mapped service account.406407## Using the token in a workflow408409Configure your OpenAI SDK client to request a GitHub OIDC token and exchange it for an OpenAI-issued access token.410411The workflow must grant `id-token: write` permission and pass the workload identity federation settings to the SDK code. The SDK requests the GitHub OIDC token from the `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variables that GitHub exposes to the job, then uses the exchanged OpenAI access token to authenticate API requests.412413For example, run your application code from a workflow like this:414415```yaml416name: deploy417418on:419  push:420    branches:421      - main422  workflow_dispatch:423424permissions:425  id-token: write426  contents: read427428jobs:429  deploy:430    runs-on: ubuntu-latest431    environment: production432    steps:433      - uses: actions/checkout@v4434435      - name: Run OpenAI SDK code436        env:437          OPENAI_WIF_AUDIENCE: ${{ vars.OPENAI_WIF_AUDIENCE }}438          OPENAI_IDENTITY_PROVIDER_ID: ${{ vars.OPENAI_IDENTITY_PROVIDER_ID }}439          OPENAI_SERVICE_ACCOUNT_ID: ${{ vars.OPENAI_SERVICE_ACCOUNT_ID }}440        run: node ./scripts/call-openai.js441```442443Store `OPENAI_WIF_AUDIENCE`, `OPENAI_IDENTITY_PROVIDER_ID`, and `OPENAI_SERVICE_ACCOUNT_ID` as GitHub Actions variables. They identify the provider and service account but are not bearer credentials.444445The following examples initialize an OpenAI client with a custom subject token provider. The provider requests a GitHub OIDC token for the configured audience and uses it as the subject token for workload identity federation.446447Authenticate from a GitHub Actions OIDC token448449```javascript450import OpenAI from "openai";451452const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;453const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;454const audience = process.env.OPENAI_WIF_AUDIENCE;455const requestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;456const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;457458if (459  !identityProviderId ||460  !serviceAccountId ||461  !audience ||462  !requestURL ||463  !requestToken464) {465  throw new Error(466    "Set OPENAI_IDENTITY_PROVIDER_ID, OPENAI_SERVICE_ACCOUNT_ID, OPENAI_WIF_AUDIENCE, and run inside GitHub Actions with id-token: write"467  );468}469470function githubActionsOIDCTokenProvider(requestURL, requestToken, audience) {471  return {472    tokenType: "jwt",473    getToken: async () => {474      const url = new URL(requestURL);475      url.searchParams.set("audience", audience);476477      const response = await fetch(url, {478        headers: { Authorization: `bearer ${requestToken}` },479      });480481      if (!response.ok) {482        throw new Error(483          `Failed to request GitHub OIDC token: ${response.status} ${response.statusText}`484        );485      }486487      const body = await response.json();488      if (!body.value) {489        throw new Error("GitHub OIDC token response did not include a value.");490      }491492      return body.value;493    },494  };495}496497const client = new OpenAI({498  workloadIdentity: {499    identityProviderId,500    serviceAccountId,501    provider: githubActionsOIDCTokenProvider(502      requestURL,503      requestToken,504      audience505    ),506  },507});508509const response = await client.responses.create({510  model: "gpt-5.6-terra",511  input: "Say hello from GitHub Actions workload identity federation.",512});513514console.log(response.output_text);515```516517```python518import json519import os520import urllib.parse521import urllib.request522523from openai import OpenAI524from openai.auth import SubjectTokenProvider525526527def github_actions_oidc_token_provider(audience: str) -> SubjectTokenProvider:528    request_url = os.environ["ACTIONS_ID_TOKEN_REQUEST_URL"]529    request_token = os.environ["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]530531    def get_token() -> str:532        parsed_url = urllib.parse.urlparse(request_url)533        query = dict(urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True))534        query["audience"] = audience535        url = urllib.parse.urlunparse(536            parsed_url._replace(query=urllib.parse.urlencode(query))537        )538539        request = urllib.request.Request(540            url,541            headers={"Authorization": f"bearer {request_token}"},542        )543        with urllib.request.urlopen(request) as response:544            payload = json.loads(response.read().decode("utf-8"))545546        token = payload.get("value")547        if not token:548            raise RuntimeError("GitHub OIDC token response did not include a value.")549        return token550551    return {"token_type": "jwt", "get_token": get_token}552553554client = OpenAI(555    workload_identity={556        "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],557        "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],558        "provider": github_actions_oidc_token_provider(559            os.environ["OPENAI_WIF_AUDIENCE"]560        ),561    },562)563564response = client.responses.create(565    model="gpt-5.6-terra",566    input="Say hello from GitHub Actions workload identity federation.",567)568569print(response.output_text)570```571572```go573package main574575import (576	"context"577	"encoding/json"578	"fmt"579	"log"580	"net/http"581	"net/url"582	"os"583584	"github.com/openai/openai-go/v3"585	"github.com/openai/openai-go/v3/auth"586	"github.com/openai/openai-go/v3/option"587	"github.com/openai/openai-go/v3/responses"588)589590type githubActionsOIDCTokenProvider struct {591	requestURL   string592	requestToken string593	audience     string594}595596func (p githubActionsOIDCTokenProvider) TokenType() auth.SubjectTokenType {597	return auth.SubjectTokenTypeJWT598}599600func (p githubActionsOIDCTokenProvider) GetToken(ctx context.Context, httpClient auth.HTTPDoer) (string, error) {601	if httpClient == nil {602		httpClient = http.DefaultClient603	}604605	oidcURL, err := url.Parse(p.requestURL)606	if err != nil {607		return "", &auth.SubjectTokenProviderError{608			Provider: "github-actions",609			Message:  "failed to parse GitHub OIDC request URL",610			Cause:    err,611		}612	}613	query := oidcURL.Query()614	query.Set("audience", p.audience)615	oidcURL.RawQuery = query.Encode()616617	req, err := http.NewRequestWithContext(ctx, http.MethodGet, oidcURL.String(), nil)618	if err != nil {619		return "", &auth.SubjectTokenProviderError{620			Provider: "github-actions",621			Message:  "failed to create GitHub OIDC token request",622			Cause:    err,623		}624	}625	req.Header.Set("Authorization", "bearer "+p.requestToken)626627	resp, err := httpClient.Do(req)628	if err != nil {629		return "", &auth.SubjectTokenProviderError{630			Provider: "github-actions",631			Message:  "failed to request GitHub OIDC token",632			Cause:    err,633		}634	}635	defer resp.Body.Close()636637	if resp.StatusCode < 200 || resp.StatusCode >= 300 {638		return "", &auth.SubjectTokenProviderError{639			Provider: "github-actions",640			Message:  fmt.Sprintf("GitHub OIDC token request failed with status %s", resp.Status),641		}642	}643644	var body struct {645		Value string `json:"value"`646	}647	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {648		return "", &auth.SubjectTokenProviderError{649			Provider: "github-actions",650			Message:  "failed to decode GitHub OIDC token response",651			Cause:    err,652		}653	}654	if body.Value == "" {655		return "", &auth.SubjectTokenProviderError{656			Provider: "github-actions",657			Message:  "GitHub OIDC token response did not include a value",658		}659	}660661	return body.Value, nil662}663664func main() {665	client := openai.NewClient(666		option.WithWorkloadIdentity(auth.WorkloadIdentity{667			IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),668			ServiceAccountID:   os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),669			Provider: githubActionsOIDCTokenProvider{670				requestURL:   os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL"),671				requestToken: os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN"),672				audience:     os.Getenv("OPENAI_WIF_AUDIENCE"),673			},674		}),675	)676677	response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{678		Model: openai.ChatModelGPT4_1Mini,679		Input: responses.ResponseNewParamsInputUnion{680			OfString: openai.String("Say hello from GitHub Actions workload identity federation."),681		},682	})683	if err != nil {684		log.Fatal(err)685	}686687	fmt.Println(response.OutputText())688}689```690691```java692import com.fasterxml.jackson.databind.JsonNode;693import com.fasterxml.jackson.databind.json.JsonMapper;694import com.openai.auth.SubjectTokenProvider;695import com.openai.auth.SubjectTokenType;696import com.openai.auth.WorkloadIdentity;697import com.openai.client.OpenAIClient;698import com.openai.client.okhttp.OpenAIOkHttpClient;699import com.openai.errors.SubjectTokenProviderException;700import com.openai.models.responses.ResponseCreateParams;701import java.net.URI;702import java.net.URLEncoder;703import java.net.http.HttpRequest;704import java.net.http.HttpResponse;705import java.nio.charset.StandardCharsets;706import java.util.concurrent.CompletableFuture;707708public final class GitHubActionsWorkloadIdentityExample {709  private GitHubActionsWorkloadIdentityExample() {}710711  static final class GitHubActionsOidcTokenProvider implements SubjectTokenProvider {712    private final String requestUrl;713    private final String requestToken;714    private final String audience;715716    GitHubActionsOidcTokenProvider(String requestUrl, String requestToken, String audience) {717      this.requestUrl = requestUrl;718      this.requestToken = requestToken;719      this.audience = audience;720    }721722    @Override723    public SubjectTokenType tokenType() {724      return SubjectTokenType.JWT;725    }726727    @Override728    public String getToken(com.openai.core.http.HttpClient httpClient, JsonMapper jsonMapper) {729      try {730        String separator = requestUrl.contains("?") ? "&" : "?";731        URI uri =732            URI.create(733                requestUrl734                    + separator735                    + "audience="736                    + URLEncoder.encode(audience, StandardCharsets.UTF_8));737738        HttpRequest request =739            HttpRequest.newBuilder(uri)740                .header("Authorization", "bearer " + requestToken)741                .GET()742                .build();743744        HttpResponse<String> response =745            java.net.http.HttpClient.newHttpClient()746                .send(request, HttpResponse.BodyHandlers.ofString());747748        if (response.statusCode() < 200 || response.statusCode() >= 300) {749          throw new SubjectTokenProviderException(750              "github-actions",751              "GitHub OIDC token request failed with status " + response.statusCode(),752              null);753        }754755        JsonNode payload = jsonMapper.readTree(response.body());756        String token = payload.path("value").asText("");757        if (token.isEmpty()) {758          throw new SubjectTokenProviderException(759              "github-actions", "GitHub OIDC token response did not include a value", null);760        }761762        return token;763      } catch (SubjectTokenProviderException e) {764        throw e;765      } catch (Exception e) {766        throw new SubjectTokenProviderException(767            "github-actions", "failed to request GitHub OIDC token", e);768      }769    }770771    @Override772    public CompletableFuture<String> getTokenAsync(773        com.openai.core.http.HttpClient httpClient, JsonMapper jsonMapper) {774      return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));775    }776  }777778  public static void main(String[] args) {779    WorkloadIdentity workloadIdentity =780        WorkloadIdentity.builder()781            .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))782            .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))783            .provider(784                new GitHubActionsOidcTokenProvider(785                    System.getenv("ACTIONS_ID_TOKEN_REQUEST_URL"),786                    System.getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN"),787                    System.getenv("OPENAI_WIF_AUDIENCE")))788            .build();789790    OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();791792    ResponseCreateParams params =793        ResponseCreateParams.builder()794            .model("gpt-5.6-terra")795            .input("Say hello from GitHub Actions workload identity federation.")796            .build();797798    client.responses().create(params).output().stream()799        .flatMap(item -> item.message().stream())800        .flatMap(message -> message.content().stream())801        .flatMap(content -> content.outputText().stream())802        .forEach(outputText -> System.out.println(outputText.text()));803  }804}805```806807```ruby808require "json"809require "net/http"810require "openai"811require "uri"812813class GitHubActionsOIDCTokenProvider814  include OpenAI::Auth::SubjectTokenProvider815816  def initialize(request_url:, request_token:, audience:)817    @request_url = request_url818    @request_token = request_token819    @audience = audience820  end821822  def token_type823    OpenAI::Auth::TokenType::JWT824  end825826  def get_token827    uri = URI(@request_url)828    params = URI.decode_www_form(uri.query || "")829    params.reject! { |key, _| key == "audience" }830    params << ["audience", @audience]831    uri.query = URI.encode_www_form(params)832833    request = Net::HTTP::Get.new(uri)834    request["Authorization"] = "bearer #{@request_token}"835836    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|837      http.request(request)838    end839840    unless response.is_a?(Net::HTTPSuccess)841      raise OpenAI::Errors::SubjectTokenProviderError.new(842        message: "GitHub OIDC token request failed with status #{response.code}",843        provider: "github-actions"844      )845    end846847    token = JSON.parse(response.body).fetch("value", "").to_s848    if token.empty?849      raise OpenAI::Errors::SubjectTokenProviderError.new(850        message: "GitHub OIDC token response did not include a value",851        provider: "github-actions"852      )853    end854855    token856  rescue JSON::ParserError, SystemCallError => e857    raise OpenAI::Errors::SubjectTokenProviderError.new(858      message: "Failed to request GitHub OIDC token: #{e.message}",859      provider: "github-actions",860      cause: e861    )862  end863end864865provider = GitHubActionsOIDCTokenProvider.new(866  request_url: ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_URL"),867  request_token: ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_TOKEN"),868  audience: ENV.fetch("OPENAI_WIF_AUDIENCE")869)870871workload_identity = OpenAI::Auth::WorkloadIdentity.new(872  identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),873  service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),874  provider: provider875)876877client = OpenAI::Client.new(workload_identity: workload_identity)878879response = client.responses.create(880  model: "gpt-5.6-terra",881  input: "Say hello from GitHub Actions workload identity federation."882)883884puts(response.output_text)885```886887888## GitHub Actions best practices889890- Use environment protections for production deployments. Require approvals or branch restrictions before workflows can access production OpenAI resources.891- Restrict mappings by repository. Match on repository-specific claims whenever possible instead of allowing access from all repositories within an organization.892- Restrict mappings by branch or workflow. Consider matching claims such as `repository`, `ref`, `environment`, or `workflow_ref` to limit token issuance.893- Use separate OpenAI service accounts for CI/CD and production workloads. Build pipelines often require different permissions than deployed applications.894- Avoid granting access to pull requests from untrusted forks. Forked pull requests may execute attacker-controlled code and should not receive production credentials.895- Use short-lived exchanges. GitHub OIDC tokens are intended for ephemeral authentication and should be exchanged only when needed.896- Audit repository ownership changes. Repository transfers, renames, and permission changes can affect the security assumptions behind existing mappings.897- Prefer exact claim matching. Match on claims such as `repository`, `ref`, and `environment` instead of relying on organization-wide trust relationships.
Files main
Menu

Theme