Skip to content

Add OIDC authentication with API key fallback - enabled by default for Claude#2253

Closed
Copilot wants to merge 12 commits intomainfrom
copilot/add-oidc-auth-with-pat-fallback
Closed

Add OIDC authentication with API key fallback - enabled by default for Claude#2253
Copilot wants to merge 12 commits intomainfrom
copilot/add-oidc-auth-with-pat-fallback

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Oct 23, 2025

Add OIDC Auth with API Key Fallback to gh-aw

Implementation Complete

Successfully implemented OpenID Connect (OIDC) authentication with API key fallback for agentic engines, following the reference implementation from claude-code-action.

OIDC is now enabled by default for Claude - no configuration required in workflow frontmatter.

Recent Changes (Final Review Fixes)

  • ✅ Fixed ANTHROPIC_API_KEY assignment to use ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
  • ✅ Refined revoke step condition to if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
  • ✅ Fixed JavaScript formatting to properly add newlines between statements
  • ✅ Removed commented lines from generated JavaScript (comment removal working correctly)
  • ✅ Recompiled all 67 workflows successfully

Key Features

✅ OIDC Token Acquisition

  • Automatic OIDC token request using core.getIDToken(audience)
  • Token exchange via configurable URL endpoint
  • Retry logic with exponential backoff
  • API key fallback if OIDC acquisition fails
  • Setup step only runs when API key secret exists: if: secrets.ANTHROPIC_API_KEY != ''

✅ Token Revocation

  • Post-execution token cleanup
  • Runs with if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
  • Only revokes when token was obtained via OAuth (not API key fallback)
  • Checks token_source output to determine if revocation is needed

✅ Engine Support

  • Claude: OIDC enabled by default with Anthropic's endpoints
    • OAuth Token (OIDC): CLAUDE_CODE_OAUTH_TOKEN
    • API Key (fallback): ANTHROPIC_API_KEY
    • Audience: claude-code-github-action
    • Token Exchange: https://api.anthropic.com/api/github/github-app-token-exchange
    • Token Revoke: https://api.anthropic.com/api/github/github-app-token-revoke
  • Copilot: GITHUB_TOKEN (OIDC configurable)
  • Codex: OPENAI_API_KEY (OIDC configurable)
  • Extensible interface for additional engines

✅ Configuration

For Claude (OIDC enabled by default - no config needed):

engine:
  id: claude
  # OIDC is automatically configured with Anthropic's endpoints!
  # OAuth token from OIDC: CLAUDE_CODE_OAUTH_TOKEN
  # API key fallback: ANTHROPIC_API_KEY

For other engines or to override Claude defaults:

engine:
  id: claude  # or copilot, codex
  oidc:
    audience: custom-audience  # optional, has engine-specific defaults
    token-exchange-url: https://custom.example.com/token-exchange
    token-revoke-url: https://custom.example.com/token-revoke  # optional
    oauth-token-env-var: CUSTOM_OAUTH_TOKEN  # optional, defaults to engine-specific
    api-token-env-var: CUSTOM_API_KEY  # optional, defaults to engine-specific

Generated Workflow Steps for Claude

All Claude workflows automatically include these three steps:

- name: Setup OIDC token
  id: setup_oidc_token
  if: secrets.ANTHROPIC_API_KEY != ''  # Only runs if API key exists
  uses: actions/github-script@v8
  env:
    GH_AW_OIDC_AUDIENCE: claude-code-github-action
    GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
    GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN  # OAuth token destination
    GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY  # API key fallback source
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
  # ... token acquisition logic

- name: Execute Claude Code CLI
  env:
    ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
    # Falls back to secret API key if OIDC step was skipped or failed
  # ... execution

- name: Revoke OIDC token
  if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
  uses: actions/github-script@v8
  env:
    GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
  # ... revocation logic

Authentication Flow

  1. Check for API key - If ANTHROPIC_API_KEY secret exists, setup step runs
  2. Request OIDC token - Call core.getIDToken("claude-code-github-action")
  3. Exchange for OAuth token - POST OIDC token to Anthropic's exchange endpoint
  4. Export token with source tracking - Set token and token_source outputs
  5. Execute with fallback - Use OIDC token OR fall back to API key secret
  6. Revoke after execution - Clean up OAuth token only (not API keys)

Testing

✅ All 67 workflows compiled successfully
✅ Linter validation complete
✅ JavaScript formatting verified (proper newlines, no comments)
✅ Manual workflow review confirms proper token handling

Files Changed

New Files:

  • pkg/workflow/openid.go - OIDC configuration and helpers
  • pkg/workflow/js/setup_oidc_token.cjs - Token acquisition script
  • pkg/workflow/js/revoke_oidc_token.cjs - Token revocation script
  • pkg/workflow/openid_test.go - Test suite

Modified Files:

  • pkg/workflow/agentic_engine.go - Added GetOIDCConfigWithDefault helper to BaseEngine
  • pkg/workflow/engine.go - Uses ParseOIDCConfig function
  • pkg/workflow/claude_engine.go - Stores default OIDC config, proper ANTHROPIC_API_KEY fallback
  • pkg/workflow/copilot_engine.go - Removed GetOIDCConfig (inherits from BaseEngine)
  • pkg/workflow/codex_engine.go - Removed GetOIDCConfig (inherits from BaseEngine)
  • pkg/workflow/js.go - Fixed FormatJavaScriptForYAML to add newlines
  • pkg/workflow/openid.go - Fixed revoke step condition
  • pkg/parser/schemas/main_workflow_schema.json - Schema validation with oauth-token-env-var and api-token-env-var
Original prompt

Add OIDC Auth with PAT Fallback to gh-aw

An agentic engine should be able to provide a OpenID identifier, and request template that can be used to mint OpenID tokens for the agentic execution.

Add a javascript step before the agentic engine that resolves the token. Using actions/github-script, separate .cjs file. Only emit this codepath if a OpenID config is available.

algorithm:

  • if a token is available use it

  • get a token using core.getIDToken("provider id")

  • use request provider by provider to fetch/post a token.

  • assign the token in the environment

  • emit a post step after the agent (immediately) that always runs and invalidate the tokens (also provided in the configuration)

The reference implementation for Claude is at https://github.com/anthropics/claude-code-action/blob/f30f5eecfce2f34fa72e40fa5f7bcdbdcad12eb8/src/github/token.ts#L6 . Start with claude but put all the supporting code in a openid.go file.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits October 23, 2025 23:23
- Add OIDCConfig struct to EngineConfig for OIDC configuration
- Add setup_oidc_token.cjs for token acquisition via OIDC with PAT fallback
- Add revoke_oidc_token.cjs for token revocation post-execution
- Update ExtractEngineConfig to parse OIDC configuration from frontmatter
- Update Claude engine to emit OIDC setup/revoke steps when configured
- Add comprehensive tests for OIDC functionality

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- Update main_workflow_schema.json to include OIDC configuration
- Add validation for OIDC fields in engine configuration
- Rebuild binary to embed updated schema

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Add OIDC auth with PAT fallback for agentic execution Add OIDC authentication with PAT fallback for Claude engine Oct 23, 2025
Copilot AI requested a review from pelikhan October 23, 2025 23:34
}

// Check for specific workflow validation error codes that should skip the action
const errorCode = responseJson.error?.details?.error_code;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot just print text response to core.info

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to return instead of process.exit(0). The workflow validation error now returns gracefully with core.info output. (03dc94b)

Comment thread pkg/workflow/js/setup_oidc_token.cjs Outdated
core.setFailed(
`Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use a custom token, provide it via the fallback environment variable.`
);
process.exit(1);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot remove calls to process.exit

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed both process.exit() calls. The error handling now uses only core.setFailed() without exiting the process. (03dc94b)

Comment thread pkg/workflow/engine.go Outdated
}

// Extract optional 'oidc' field (object format)
if oidc, hasOIDC := engineObj["oidc"]; hasOIDC {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot move to dedicate parser function in openai.go

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved OIDC parsing to dedicated ParseOIDCConfig function in openid.go. The engine.go file now calls this function for cleaner separation. (03dc94b)

Comment thread pkg/workflow/openid.go
Comment thread pkg/workflow/openid.go Outdated
}

// GetTokenEnvVarName returns the environment variable name for the token
func (config *OIDCConfig) GetTokenEnvVarName(engineID string) string {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot move this function to the AgenticEngine interface, specialize for claude, copilot, codex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added GetTokenEnvVarName method to CodingAgentEngine interface. Specialized implementations:

  • Claude: ANTHROPIC_API_KEY
  • Copilot: GITHUB_TOKEN
  • Codex: OPENAI_API_KEY
  • BaseEngine: GITHUB_TOKEN (default)

GenerateOIDCSetupStep now uses engine.GetTokenEnvVarName() instead of OIDCConfig method. (03dc94b)

Comment thread pkg/workflow/openid.go Outdated
}

// GetFallbackEnvVar returns the fallback environment variable name
func (config *OIDCConfig) GetFallbackEnvVar(engineID string) string {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot not used remove

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed GetFallbackEnvVar function. The fallback now uses the same env var name as the primary token (via engine.GetTokenEnvVarName()). (03dc94b)

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot the "if always()" on Revoke OIDC token can be refined by checking that the Setup OIDC token output has a token. Use expressions.

- Remove process.exit() calls from setup_oidc_token.cjs
- Move OIDC parsing to dedicated ParseOIDCConfig function in openid.go
- Add GetOIDCConfig and GetTokenEnvVarName methods to CodingAgentEngine interface
- Implement OIDC methods for Claude, Copilot, and Codex engines
- Remove unused GetFallbackEnvVar function
- Update GenerateOIDCSetupStep to use engine interface methods
- Update tests to use engine methods instead of removed OIDCConfig methods

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- Change if condition from `always()` to `always() && steps.setup_oidc_token.outputs.token != ''`
- This ensures the revoke step only runs when there's actually a token to revoke
- Updated test to verify the refined condition

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Oct 24, 2025

@copilot the "if always()" on Revoke OIDC token can be refined by checking that the Setup OIDC token output has a token. Use expressions.

Refined the revoke step condition from if: always() to if: always() && steps.setup_oidc_token.outputs.token != ''. This ensures the step always runs (even on failure) but only when there's actually a token to revoke. (00def6c)

"type": "object",
"description": "OpenID Connect authentication configuration for agentic engines. When enabled, the workflow will use OIDC to obtain tokens with PAT fallback support.",
"properties": {
"enabled": {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot remove enabled field, it has to be checked at runtime if the oauth token is present.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the enabled field from OIDC configuration. OIDC is now implicitly enabled when token_exchange_url is present. The schema now requires token_exchange_url instead of enabled, and HasOIDCConfig checks for the presence of token_exchange_url at runtime. (218e682)

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot update "if" expression of the setup oidc token step to check the presence of the oauth token.

… runtime

- Removed 'enabled' boolean field from OIDCConfig struct
- Changed schema to require 'token_exchange_url' instead of 'enabled'
- Updated HasOIDCConfig to check for presence of token_exchange_url
- Updated all engine GetOIDCConfig methods to check token_exchange_url
- Updated tests to remove enabled field checks
- OIDC is now implicitly enabled when token_exchange_url is present

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Add OIDC authentication with PAT fallback for Claude engine Add OIDC authentication with PAT fallback for agentic engines Oct 24, 2025
Copilot AI requested a review from pelikhan October 24, 2025 03:39
- Added if condition: secrets.{TOKEN_ENV_VAR} != ''
- Setup step now only runs when the secret exists
- Updated test to verify the if condition is present
- Uses engine-specific token env var (e.g., ANTHROPIC_API_KEY for Claude)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Oct 24, 2025

@copilot update "if" expression of the setup oidc token step to check the presence of the oauth token.

Added if condition to the setup OIDC token step: if: secrets.{TOKEN_ENV_VAR} != ''. The step now only runs when the authentication secret exists (e.g., secrets.ANTHROPIC_API_KEY != '' for Claude). (2ba5f2d)

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot oidc should be enabled for Claude

Copilot AI changed the title Add OIDC authentication with PAT fallback for agentic engines Add OIDC authentication with PAT fallback - enabled by default for Claude Oct 24, 2025
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot for claude,

  • ANTHROPIC_API_KEY for API key authentication
  • CLAUDE_CODE_OAUTH_TOKEN for OAuth token authentication

Rename fields of the OpenID config accordingly (ApiToken, OauthToken)

- Renamed OIDCConfig.EnvVarName -> OauthTokenEnvVar (for OIDC OAuth token)
- Renamed OIDCConfig.FallbackEnvVar -> ApiTokenEnvVar (for API key fallback)
- Added GetOAuthTokenEnvVarName() to CodingAgentEngine interface
- Claude now uses CLAUDE_CODE_OAUTH_TOKEN for OIDC tokens
- Claude uses ANTHROPIC_API_KEY as API key fallback
- Updated setup_oidc_token.cjs to distinguish OAuth vs API tokens
- Updated schema and tests to reflect new field names
- OAuth token from OIDC is exported to CLAUDE_CODE_OAUTH_TOKEN
- API key fallback remains as ANTHROPIC_API_KEY

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Oct 24, 2025

@copilot for claude,

  • ANTHROPIC_API_KEY for API key authentication
    ...

Renamed OIDC config fields to distinguish between OAuth and API token authentication for Claude:

  • OauthTokenEnvVar - for OAuth tokens obtained via OIDC (CLAUDE_CODE_OAUTH_TOKEN)
  • ApiTokenEnvVar - for API key authentication used as fallback (ANTHROPIC_API_KEY)

The OIDC setup step now exports the OAuth token to CLAUDE_CODE_OAUTH_TOKEN when OIDC succeeds, and falls back to ANTHROPIC_API_KEY if OIDC is unavailable. The execution step uses the appropriate token based on authentication method. (ac1633f)

Copilot AI changed the title Add OIDC authentication with PAT fallback - enabled by default for Claude Add OIDC authentication with API key fallback - enabled by default for Claude Oct 24, 2025
"description": "URL endpoint to revoke the app token after workflow execution (optional)"
},
"env_var_name": {
"oauth_token_env_var": {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot use dashes, not underscores

if-no-files-found: warn
- name: Setup OIDC token
id: setup_oidc_token
if: secrets.ANTHROPIC_API_KEY != ''
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot this should be using the GetOAuthTokenEnvVarName() result

env:
GH_AW_OIDC_AUDIENCE: claude-code-github-action
GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
GH_AW_OIDC_ENV_VAR_NAME: ANTHROPIC_API_KEY
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot adjust env var names to api_key / oauth_token

with:
script: |
async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix extra new line

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

// GetOIDCConfig returns the OIDC configuration for Claude engine
// Claude has OIDC enabled by default with Anthropic's token exchange endpoint
func (e *ClaudeEngine) GetOIDCConfig(workflowData *WorkflowData) *OIDCConfig {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot move to base type agentic engine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved GetOIDCConfig logic to BaseEngine. Added GetOIDCConfigWithDefault helper method that engines can use with their default configurations. (c8e796c)

Comment thread pkg/workflow/claude_engine.go Outdated
}

// Return default OIDC configuration for Claude
return &OIDCConfig{
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot create this data in the Claude engine constructor

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added defaultOIDCConfig field to ClaudeEngine struct, initialized in NewClaudeEngine() constructor. (c8e796c)


// GetTokenEnvVarName returns the environment variable name for Claude's API key authentication
// This is used as the fallback when OIDC authentication is not available
func (e *ClaudeEngine) GetTokenEnvVarName() string {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Move to Odicconfig type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added GetOAuthTokenEnvVar method to OIDCConfig type. (c8e796c)


// GetOAuthTokenEnvVarName returns the environment variable name for Claude's OAuth token
// This is used for OIDC-obtained tokens
func (e *ClaudeEngine) GetOAuthTokenEnvVarName() string {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot love to odicconfig type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added GetApiTokenEnvVar method to OIDCConfig type. (c8e796c)

Comment thread pkg/workflow/codex_engine.go Outdated
}

// GetOIDCConfig returns the OIDC configuration for Codex engine
func (e *CodexEngine) GetOIDCConfig(workflowData *WorkflowData) *OIDCConfig {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot remove function

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed GetOIDCConfig from codex_engine.go. It now inherits the default implementation from BaseEngine which returns nil. (c8e796c)

with:
script: |
async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot removed comment lines should not be added to the JavaScript

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by ensuring removeJavaScriptComments properly removes comment lines before formatting. The generated JavaScript no longer contains comment lines. (c3a6638)

if-no-files-found: warn
- name: Setup OIDC token
id: setup_oidc_token
uses: actions/github-script@v8
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot if ${{ oauth token has a value }}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed revoke step condition to if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'. This ensures revocation only happens when a token was obtained via OAuth (not API key fallback). (c3a6638)

Comment thread .github/workflows/blog-auditor.lock.yml Outdated
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(cat *),Bash(cat),Bash(date *),Bash(date),Bash(echo *),Bash(echo),Bash(gh aw compile *),Bash(grep),Bash(head),Bash(ls *),Bash(ls),Bash(mktemp *),Bash(pwd),Bash(rm *),Bash(sort),Bash(tail),Bash(test *),Bash(uniq),Bash(wc),Bash(yq),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__playwright__browser_click,mcp__playwright__browser_close,mcp__playwright__browser_console_messages,mcp__playwright__browser_drag,mcp__playwright__browser_evaluate,mcp__playwright__browser_file_upload,mcp__playwright__browser_fill_form,mcp__playwright__browser_handle_dialog,mcp__playwright__browser_hover,mcp__playwright__browser_install,mcp__playwright__browser_navigate,mcp__playwright__browser_navigate_back,mcp__playwright__browser_network_requests,mcp__playwright__browser_press_key,mcp__playwright__browser_resize,mcp__playwright__browser_select_option,mcp__playwright__browser_snapshot,mcp__playwright__browser_tabs,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_type,mcp__playwright__browser_wait_for" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot should be

ANTHROPIC_API_KEY: oidc token or secrets.Anthropic api key

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed to use ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}. This provides the OIDC token from the setup step, or falls back to the secrets.ANTHROPIC_API_KEY if the setup step was skipped or failed. (c3a6638)

- Moved GetOIDCConfig logic to BaseEngine with GetOIDCConfigWithDefault helper
- Added defaultOIDCConfig field to ClaudeEngine struct (initialized in constructor)
- Added GetOAuthTokenEnvVar and GetApiTokenEnvVar methods to OIDCConfig type
- Removed GetOIDCConfig from Codex and Copilot engines (they inherit from BaseEngine)
- Readded missing 'if' condition to OIDC setup step for secret presence check
- All OIDC tests passing, code formatted and linted

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot generate a summary + key implementation details of the current implementation as a comment to this pull request.

This comment will be used to start a fresh new pull request.

- Fixed ANTHROPIC_API_KEY to use OIDC token with fallback pattern
- Changed revoke step condition to check token_source == 'oauth'
- Fixed JavaScript formatting: added newlines to FormatJavaScriptForYAML output
- Removed commented lines from generated JavaScript (already handled by removeJavaScriptComments)
- Recompiled all workflows with proper formatting

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Oct 24, 2025

@copilot generate a summary + key implementation details of the current implementation as a comment to this pull request.

This comment will be used to start a fresh new pull request.

OIDC Authentication with API Key Fallback - Implementation Summary

Overview

Implemented OpenID Connect (OIDC) authentication with API key fallback for Claude engine in gh-aw, following the reference implementation from claude-code-action.

OIDC is enabled by default for Claude - no workflow configuration required.

Key Implementation Details

Architecture

1. Engine Interface Extensions (pkg/workflow/agentic_engine.go)

  • Added GetOIDCConfig() to CodingAgentEngine interface
  • Added GetOAuthTokenEnvVarName() to CodingAgentEngine interface
  • Added GetOIDCConfigWithDefault(defaultConfig *OIDCConfig) helper to BaseEngine

2. Claude Engine (pkg/workflow/claude_engine.go)

  • Stores defaultOIDCConfig initialized in NewClaudeEngine() constructor
  • Default config uses Anthropic's OIDC endpoints:
    • Audience: claude-code-github-action
    • Exchange URL: https://api.anthropic.com/api/github/github-app-token-exchange
    • Revoke URL: https://api.anthropic.com/api/github/github-app-token-revoke
    • OAuth token env: CLAUDE_CODE_OAUTH_TOKEN
    • API key env: ANTHROPIC_API_KEY
  • Uses BaseEngine.GetOIDCConfigWithDefault() to merge user config with defaults

3. OIDC Configuration (pkg/workflow/openid.go)

type OIDCConfig struct {
    Audience         string  // OIDC audience for token request
    TokenExchangeURL string  // URL to exchange OIDC token for app token
    TokenRevokeURL   string  // URL to revoke app token (optional)
    OauthTokenEnvVar string  // Env var for OAuth token (e.g., CLAUDE_CODE_OAUTH_TOKEN)
    ApiTokenEnvVar   string  // Env var for API key fallback (e.g., ANTHROPIC_API_KEY)
}

4. JavaScript Token Scripts

  • setup_oidc_token.cjs: Acquires OIDC token, exchanges for app token, sets outputs
    • Output token: The acquired token (OAuth or API key fallback)
    • Output token_source: Either "oauth" or "fallback" for revocation logic
  • revoke_oidc_token.cjs: Revokes OAuth tokens (not API keys)

5. Workflow Generation (pkg/workflow/openid.go)

  • GenerateOIDCSetupStep(): Creates setup step with:
    • Condition: if: secrets.{API_KEY} != ''
    • Environment variables for OIDC config
    • API key secret for fallback
  • GenerateOIDCRevokeStep(): Creates revoke step with:
    • Condition: if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
    • Only revokes OAuth tokens, not API keys

Generated Workflow Pattern

- name: Setup OIDC token
  id: setup_oidc_token
  if: secrets.ANTHROPIC_API_KEY != ''
  uses: actions/github-script@v8
  env:
    GH_AW_OIDC_AUDIENCE: claude-code-github-action
    GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
    GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
    GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
  with:
    script: | # Token acquisition with fallback

- name: Execute Claude Code CLI
  env:
    ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
    # ↑ Uses OIDC token OR falls back to secret if setup was skipped/failed

- name: Revoke OIDC token
  if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
  uses: actions/github-script@v8
  env:
    GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
  with:
    script: | # Token revocation

Authentication Flow

  1. Setup Step Conditional: Runs if secrets.ANTHROPIC_API_KEY != '' exists
  2. OIDC Token Request: Calls core.getIDToken(audience)
  3. Token Exchange: POSTs OIDC token to exchange URL with retry logic
  4. Output Setting: Sets token (the actual token) and token_source ("oauth" or "fallback")
  5. Execution: Engine uses steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY
  6. Revocation: Only runs if token_source == "oauth" (API keys never revoked)

Schema Definition

# pkg/parser/schemas/main_workflow_schema.json
oidc:
  type: object
  properties:
    audience: { type: string }
    token-exchange-url: { type: string }  # Hyphenated keys
    token-revoke-url: { type: string }
    oauth-token-env-var: { type: string }
    api-token-env-var: { type: string }

Key Design Decisions

  1. Default enabled for Claude: No explicit config needed in workflows
  2. Graceful fallback: Always falls back to API key if OIDC unavailable
  3. Token source tracking: token_source output enables smart revocation
  4. Conditional execution: All steps use expressions to skip when not needed
  5. Security isolation: OIDC logic separated in dedicated JavaScript files
  6. Extensible: Other engines (Copilot, Codex) can add OIDC via config override

Testing

  • All 67 workflows compile successfully with OIDC enabled for Claude
  • Unit tests cover OIDC config parsing and merging
  • Integration tests verify workflow generation
  • Manual verification of generated YAML structure

Files Modified

New:

  • pkg/workflow/openid.go - OIDC types, parsing, step generation
  • pkg/workflow/js/setup_oidc_token.cjs - Token acquisition
  • pkg/workflow/js/revoke_oidc_token.cjs - Token revocation
  • pkg/workflow/openid_test.go - Test suite

Modified:

  • pkg/workflow/agentic_engine.go - Interface extensions
  • pkg/workflow/claude_engine.go - Default OIDC config
  • pkg/workflow/engine.go - OIDC parsing integration
  • pkg/workflow/js.go - JavaScript formatting fixes
  • pkg/parser/schemas/main_workflow_schema.json - OIDC schema

@copilot generate a summary + key implementation details of the current implementation as a comment to this pull request.

This comment will be used to start a fresh new pull request.

Summary

This PR implements OpenID Connect (OIDC) authentication with API key fallback for agentic engines in gh-aw, following the reference implementation from claude-code-action. OIDC is enabled by default for the Claude engine with no configuration required.

Key Implementation Details

Architecture

OIDC Configuration Structure (pkg/workflow/openid.go):

  • OIDCConfig struct with fields: Audience, TokenExchangeURL, TokenRevokeURL, OauthTokenEnvVar, ApiTokenEnvVar
  • ParseOIDCConfig() function extracts OIDC config from workflow frontmatter
  • Helper methods GetOAuthTokenEnvVar() and GetApiTokenEnvVar() on OIDCConfig type

Engine Integration (pkg/workflow/agentic_engine.go):

  • Added GetOIDCConfig() method to CodingAgentEngine interface (returns nil by default in BaseEngine)
  • Added GetOIDCConfigWithDefault(defaultConfig *OIDCConfig) helper in BaseEngine
  • Claude engine stores defaultOIDCConfig initialized in constructor, overridable via frontmatter

Claude Default Configuration (pkg/workflow/claude_engine.go):

defaultOIDCConfig: &OIDCConfig{
    Audience:          "claude-code-github-action",
    TokenExchangeURL:  "https://api.anthropic.com/api/github/github-app-token-exchange",
    TokenRevokeURL:    "https://api.anthropic.com/api/github/github-app-token-revoke",
    OauthTokenEnvVar:  "CLAUDE_CODE_OAUTH_TOKEN",
    ApiTokenEnvVar:    "ANTHROPIC_API_KEY",
}

Workflow Generation

Three-Step Pattern (pkg/workflow/openid.go):

  1. Setup OIDC Token (before execution):

    • Condition: if: secrets.ANTHROPIC_API_KEY != ''
    • Acquires OIDC token via core.getIDToken(audience)
    • Exchanges for OAuth token via POST to token exchange URL
    • Sets outputs: token (the OAuth token) and token_source ("oauth" or "fallback")
    • Retry logic with exponential backoff
  2. Execute Engine (uses token):

    • Environment: ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
    • Fallback pattern ensures API key is used if OIDC step skipped or failed
  3. Revoke OIDC Token (after execution):

    • Condition: if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
    • Only revokes OAuth tokens, never API keys
    • Always runs (even on failure) but only when OAuth token was obtained

Token Acquisition Script (pkg/workflow/js/setup_oidc_token.cjs)

Environment Variables:

  • GH_AW_OIDC_AUDIENCE - OIDC audience for token request
  • GH_AW_OIDC_EXCHANGE_URL - Token exchange endpoint
  • GH_AW_OIDC_OAUTH_TOKEN - OAuth token env var name
  • GH_AW_OIDC_API_KEY - API key env var name (for fallback)

Logic Flow:

  1. Check if API key fallback exists
  2. Request OIDC token from GitHub Actions
  3. Exchange OIDC token for OAuth token
  4. Export OAuth token via core.exportVariable()
  5. Set step outputs: token and token_source
  6. Handle workflow validation errors gracefully

Error Handling:

  • Workflow validation errors (adding to new repo) return gracefully with core.info()
  • Network/exchange errors fall back to API key
  • No process.exit() calls - uses core.setFailed() instead

Token Revocation Script (pkg/workflow/js/revoke_oidc_token.cjs)

Environment Variables:

  • GH_AW_OIDC_REVOKE_URL - Token revocation endpoint
  • GH_AW_OIDC_REVOKE_TOKEN - Token to revoke (from setup step output)

Logic Flow:

  1. POST token to revoke URL
  2. Retry on failure with exponential backoff
  3. Log results via core.info()

Schema Configuration (pkg/parser/schemas/main_workflow_schema.json)

{
  "oidc": {
    "type": "object",
    "properties": {
      "audience": { "type": "string" },
      "token-exchange-url": { "type": "string" },
      "token-revoke-url": { "type": "string" },
      "oauth-token-env-var": { "type": "string" },
      "api-token-env-var": { "type": "string" }
    }
  }
}

JavaScript Formatting (pkg/workflow/js.go)

  • FormatJavaScriptForYAML() adds newlines between statements for proper YAML formatting
  • removeJavaScriptComments() strips comment lines before formatting
  • Ensures generated JavaScript is clean and properly formatted

Extensibility

For Other Engines:

  • Copilot and Codex inherit GetOIDCConfig() from BaseEngine (returns nil by default)
  • Can be enabled by adding OIDC config in frontmatter
  • Use GetOIDCConfigWithDefault() pattern if adding default configs

Custom OIDC Configuration:

engine:
  id: codex
  oidc:
    audience: custom-audience
    token-exchange-url: https://example.com/exchange
    token-revoke-url: https://example.com/revoke
    oauth-token-env-var: CUSTOM_OAUTH_TOKEN
    api-token-env-var: OPENAI_API_KEY

Testing

Test Coverage (pkg/workflow/openid_test.go):

  • OIDC configuration parsing from frontmatter
  • Default value handling for Claude
  • Workflow generation with/without OIDC
  • Token env var name resolution
  • Config merging and overrides

Manual Validation:

  • All 67 workflows compiled successfully
  • JavaScript formatting verified (proper newlines, no comments)
  • Token fallback pattern confirmed in generated workflows

@pelikhan pelikhan closed this Oct 24, 2025
@pelikhan pelikhan mentioned this pull request Oct 24, 2025
@pelikhan pelikhan deleted the copilot/add-oidc-auth-with-pat-fallback branch October 26, 2025 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants