Skip to content

feat: authenticate in GitHub Actions with workload identity federation - #268

Open
nicknisi wants to merge 8 commits into
mainfrom
riker/239-spike-the-workos-cli-workos-cli-workos-a
Open

nicknisi wants to merge 8 commits into
mainfrom
riker/239-spike-the-workos-cli-workos-cli-workos-a

Conversation

@nicknisi

Copy link
Copy Markdown
Member

Why

Spike: the WorkOS CLI (workos/cli; workos api and any other command that calls the WorkOS REST API) works in GitHub Actions with no WORKOS_API_KEY, using workload identity federation.

Credential order: 1) WORKOS_API_KEY if set (unchanged); 2) else, if running in GitHub Actions with ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN present and WORKOS_CLIENT_ID (the M2M app's client id) is set: request a GitHub OIDC ID token with audience https://api.workos.com (overridable via WORKOS_WIF_AUDIENCE), POST it to the WorkOS token endpoint (form: grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, assertion, client_id, resource=https://api.workos.com, optional scope) and use the returned access_token as the Bearer for API calls, cached in memory and re-exchanged shortly before expiry (tokens last 5–10 min); 3) else today's behaviour (workos login / existing profile). Token endpoint URL from WORKOS_WIF_TOKEN_URL (server side still being built; require it and say so). Send x-url-environment-id when WORKOS_ENVIRONMENT_ID is set. Clear error per failure (missing id-token permission → tell them to add permissions: id-token: write; invalid_grant / invalid_target / invalid_client surfaced with the server's description). Never print either token.

Also add examples/wif-github-actions/: .github/workflows/wif-test.yml (workflow_dispatch, permissions: id-token: write, contents: read, vars WORKOS_CLIENT_ID, WORKOS_WIF_TOKEN_URL, WORKOS_API_URL, WORKOS_ENVIRONMENT_ID) with two variants: plain curl/bash doing the exchange then GET /organizations and POST /organizations (org named wif-test-<run id>), and the same through the CLI; plus a README (create repo, set variables, seed the M2M app + trust: issuer https://token.actions.githubusercontent.com, audience, claims repository_id + ref, run it, what success looks like).

Unit tests with mocked GitHub token request and token endpoint covering the order, caching/refresh and each error message. Open a PR ready for review.

Decided

  • Two small WIF edge cases the brief doesn't settle; here is what I plan to do unless you say otherwise: 1) GITHUB_ACTIONS=true + WORKOS_CLIENT_ID set, but ACTIONS_ID_TOKEN_REQUEST_URL/TOKEN missing (no `id-token: [redacted] The brief says both "otherwise keep today's behaviour (profile key)" and "tel… → A: Refuse verify-login on WIF tokens. This is Riker's call 245, from its brief: a command that changes real data shouldn't run when the CLI can't tell whether the target is production, and a clear error costs a spike almost nothing.. The user hasn't decided yet; if they choose otherwise, you'll be t… (Riker's call)
  • Job 239 takes away in GitHub Actions jobs with WORKOS_CLIENT_ID, id-token: [redacted] and no API key, REST commands and verify-login no longer use a stored profile key (WIF is used or the command fails/refuses); scripts matching no_api_key see wif_id_token_unavailable when WORKOS_CLIENT_ID is se… → A: Publish it as asked (the author's answer)
  • Job 239 takes away in GitHub Actions jobs with WORKOS_CLIENT_ID, id-token: [redacted] and no API key, REST commands and verify-login no longer use a stored profile key (WIF is used or the command fails/refuses); scripts matching no_api_key see wif_id_token_unavailable when WORKOS_CLIENT_ID is se… → A: Refuse verify-login on WIF tokens (the author's answer)

Proven

  • if git rev-parse -q --verify MERGE_HEAD >/dev/null; then B=HEAD; else B=$(git merge-base origin/HEAD HEAD 2>/dev/null || git merge-base main HEAD) && git diff --quiet HEAD --; fi && test -f src/lib/wif.ts && test -f src/lib/wif.spec.ts && test -f examples/wif-github-actions/.github/workflows/wif-test.yml && test -f examples/wif-github-actions/README.md && git grep -q -F "never leaks either token" -- src/lib/wif.spec.ts && git grep -q -F "id-token: write" -- src/lib/wif.ts && git grep -q -F "WORKOS_WIF_TOKEN_URL" -- src/lib/wif.ts examples/wif-github-actions/README.md && git grep -q -F "WORKOS_WIF_AUDIENCE" -- src/lib/wif.ts && git grep -q -F "x-url-environment-id" -- src && git grep -q -F "urn:ietf:params:oauth:grant-type:jwt-bearer" -- src/lib/wif.ts examples/wif-github-actions/.github/workflows/wif-test.yml && git grep -q -F "token.actions.githubusercontent.com" -- examples/wif-github-actions/README.md && git diff --quiet $B -- package.json bun.lock src/cli.config.ts && (bun install --frozen-lockfile >/dev/null 2>&1 || bun install --frozen-lockfile --prefer-offline >/dev/null 2>&1) && node -e 'const y=require("yaml");const d=y.parse(require("fs").readFileSync("examples/wif-github-actions/.github/workflows/wif-test.yml","utf8"));const p=d.permissions||{};const ok=d.on&&("workflow_dispatch" in (typeof d.on==="string"?{[d.on]:1}:d.on))&&p["id-token"]==="write"&&p.contents==="read"&&Object.keys(d.jobs||{}).length>=2;process.exit(ok?0:1)' && bun run typecheck && bun run lint && bun run format:check && NODE_OPTIONS=--no-warnings /opt/homebrew/bin/timeout 480 bunx vitest run src/lib/wif.spec.ts src/lib/api-key.spec.ts src/lib/workos-client.spec.ts src/commands/api passed on fba0bd8 (run 1).
  • New tests not run on base: no test runner I recognise in the check.
  • Review of 8b223d6..cf612b7: approved.
  • if git rev-parse -q --verify MERGE_HEAD >/dev/null; then B=HEAD; else B=$(git merge-base origin/HEAD HEAD 2>/dev/null || git merge-base main HEAD) && git diff --quiet HEAD --; fi && test -f src/lib/wif.ts && test -f src/lib/wif.spec.ts && test -f examples/wif-github-actions/.github/workflows/wif-test.yml && test -f examples/wif-github-actions/README.md && git grep -q -F "never leaks either token" -- src/lib/wif.spec.ts && git grep -q -F "id-token: write" -- src/lib/wif.ts && git grep -q -F "WORKOS_WIF_TOKEN_URL" -- src/lib/wif.ts examples/wif-github-actions/README.md && git grep -q -F "WORKOS_WIF_AUDIENCE" -- src/lib/wif.ts && git grep -q -F "x-url-environment-id" -- src && git grep -q -F "urn:ietf:params:oauth:grant-type:jwt-bearer" -- src/lib/wif.ts examples/wif-github-actions/.github/workflows/wif-test.yml && git grep -q -F "token.actions.githubusercontent.com" -- examples/wif-github-actions/README.md && git diff --quiet $B -- package.json bun.lock src/cli.config.ts && (bun install --frozen-lockfile >/dev/null 2>&1 || bun install --frozen-lockfile --prefer-offline >/dev/null 2>&1) && node -e 'const y=require("yaml");const d=y.parse(require("fs").readFileSync("examples/wif-github-actions/.github/workflows/wif-test.yml","utf8"));const p=d.permissions||{};const ok=d.on&&("workflow_dispatch" in (typeof d.on==="string"?{[d.on]:1}:d.on))&&p["id-token"]==="write"&&p.contents==="read"&&Object.keys(d.jobs||{}).length>=2;process.exit(ok?0:1)' && bun run typecheck && bun run lint && bun run format:check && NODE_OPTIONS=--no-warnings /opt/homebrew/bin/timeout 480 bunx vitest run src/lib/wif.spec.ts src/lib/api-key.spec.ts src/lib/workos-client.spec.ts src/commands/api passed on fba0bd8 merged onto main at a3cb418 (run 2).

Not checked

  • Run examples/wif-github-actions in a throwaway GitHub repo against a WorkOS environment with job 232's server side deployed. Both the curl job and the CLI job should create an org named wif-test-<run id>.
  • Read the error messages for a missing `id-token: [redacted], a missing WORKOS_WIF_TOKEN_URL, invalid_grant, invalid_target and invalid_client. Check that each tells a user what to fix.

Opened as a draft by Riker (job 239).

…entity federation

When WORKOS_API_KEY and --api-key are unset and the CLI runs in GitHub
Actions with WORKOS_CLIENT_ID and an OIDC token request URL, exchange the
workflow's GitHub OIDC token at WORKOS_WIF_TOKEN_URL for a short-lived
WorkOS access token and use it as the Bearer for REST calls, sending
x-url-environment-id when WORKOS_ENVIRONMENT_ID is set. The token is cached
in memory only and refreshed inside the expiry margin.
A failed WIF token refresh inside the SDK's fetch is already reported;
re-throw it instead of printing a second 'Unexpected error'.
@nicknisi
nicknisi marked this pull request as ready for review October 28, 2026 13:50
@greptile-apps

greptile-apps Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Critical impact] The latest changes appear safe to merge and fix the earlier test-order issue.

Summary

Adds GitHub Actions workload identity federation for REST commands without an API key.

  • GitHub Actions jobs can use short-lived WorkOS tokens for REST commands.
  • WIF credentials stay hidden, and verify-login refuses to use them.
  • A sample GitHub workflow checks WIF calls with curl and the CLI.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[REST command] --> B{Explicit API key?}
  B -->|Yes| C[Use API key]
  B -->|No| D{GitHub WIF ready?}
  D -->|Yes| E[Request GitHub OIDC token]
  E --> F[Exchange for WorkOS token]
  F --> G[Cache in memory and refresh before expiry]
  G --> H[Call API with environment header when set]
  D -->|No| I[Use stored key or report missing credentials]
Loading

Reviews (2) · Last reviewed commit: "test(wif): make the token-leak test inde..." · Reviewed by Greptile

Comment thread src/lib/wif.spec.ts Outdated
Clear registered secrets before every test and give the leak test its own
token values, so each case only echoes values the CLI has already handled.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant