Skip to content

feat(embed): add GitHub Actions OIDC as primary auth path - #36

Merged
baladithyab merged 2 commits into
mainfrom
feat/embed-oidc-auth
May 28, 2026
Merged

baladithyab merged 2 commits into
mainfrom
feat/embed-oidc-auth

Conversation

@baladithyab

Copy link
Copy Markdown
Owner

Summary

Replaces the static `PROJECT_EMBED_UPLOAD_TOKEN` shared bearer with GitHub Actions OIDC as the primary auth path on `/api/embed-upload`. Static bearer remains as fallback for manual uploads / repos without OIDC support.

Why now: the static bearer requires `gh secret set PROJECT_EMBED_UPLOAD_TOKEN` on every new repo we want to embed. After today's discovery audits, we'll have at least 4 codeseys-embed repos (CSE 160, CSE 101 source, CSCI 585 source, web-embed-workflows). Adding a 5th, 6th, etc. should be zero-config. OIDC achieves that — any GitHub Actions workflow under our owner can mint an audience-scoped JWT that the Worker verifies against GitHub's JWKS.

What's new

File Purpose
`src/lib/github-oidc.ts` JWS-Compact JWT verifier; module-level JWKS cache (1h TTL); audience + issuer + nbf/exp checks; RS256 only
`src/lib/github-oidc.test.ts` 16 unit tests with synthesized RS256 tokens covering happy path + every rejection branch
`src/lib/embed-upload.ts` New `authenticateUpload()` — tries OIDC if token looks like JWT, falls back to bearer; `AuthResult` now carries `via: 'oidc' | 'bearer'`
`src/pages/api/embed-upload.ts` Wires the new authenticator into the route

Token shape & verification

A workflow OIDC token is a 3-segment RS256 JWT with these claims (subset):

```
iss: https://token.actions.githubusercontent.com
aud: https://codeseys.io (matches our pinned EXPECTED_AUDIENCE)
sub: repo:/:ref:refs/heads/
repository: /
repository_owner: baladithyab ← we authorize on this
job_workflow_ref: //.github/workflows/@
ref: refs/heads/
sha:
actor:
exp: now + 5min
```

We verify the JWT signature against `https://token.actions.githubusercontent.com/.well-known/jwks\` and check `repository_owner === 'baladithyab'`.

Future hardening hooks (commented out, not in this PR)

  • Require `job_workflow_ref` to start with `baladithyab/web-embed-workflows/` so only OUR reusable workflow can upload, not arbitrary workflows in our repos
  • Require `event_name === 'push' || 'workflow_dispatch'` (block PRs from forks)
  • Require `ref === 'refs/heads/master' || 'refs/heads/main'`

Companion change

`baladithyab/web-embed-workflows#` updates `static-passthrough.yml` to mint and present OIDC tokens. Caller workflows need `permissions: id-token: write`.

Tests

  • 163 / 163 passing locally (was 147; +16 OIDC)
  • All existing checkBearer tests pass — the auth-result shape change (`via: 'bearer'` field added) is additive

Rollout

  1. Merge this PR (Worker now accepts OIDC + bearer)
  2. Merge web-embed-workflows OIDC PR
  3. Re-trigger one repo's workflow → confirm it logs `Authenticating via: oidc`
  4. After all 3 repos verify, remove `PROJECT_EMBED_UPLOAD_TOKEN` from each repo's secrets
  5. Eventually remove `PROJECT_EMBED_UPLOAD_TOKEN` from the Worker entirely (purely OIDC)

🤖 Generated with the help of Claude

Adds a GitHub Actions OIDC verifier (src/lib/github-oidc.ts) that
verifies a workflow-minted JWT against GitHub's JWKS and authorizes
on repository_owner. The embed-upload route now prefers OIDC and
falls back to the static PROJECT_EMBED_UPLOAD_TOKEN bearer.

Why: the static bearer requires gh secret set on every new
codeseys-embed repo. With OIDC, any future repo under our owner
can upload with zero per-repo configuration — they just request
permissions: id-token: write and the runtime token broker mints
an audience-scoped JWT.

Implementation notes:
- JWKS cached 1h in-process via a module-level cache; CF Worker
  also caches the JWKS fetch via cf.cacheTtl.
- Audience pinned to https://codeseys.io to scope tokens to this
  service (workflows minting tokens for other audiences won't
  pass).
- Authorization currently checks repository_owner === 'baladithyab'.
  Future hardening hooks documented in github-oidc.ts (require
  job_workflow_ref to come from web-embed-workflows, etc.).
- 16 unit tests with synthesized RS256 tokens cover happy path +
  every rejection branch (issuer / audience / nbf / exp / kid /
  alg / signature). 163 tests total (was 147).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f55edc22f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/github-oidc.ts
}

// Find the signing key.
let jwks = await getGithubJwks(false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle JWKS fetch failures

When the JWKS cache is cold or expired and GitHub's JWKS request rejects or returns a non-2xx response, getGithubJwks() throws, but this await is outside any try/catch. That means an otherwise expected auth dependency outage bubbles out of PUT /api/embed-upload as an unhandled 500 instead of the typed 503 auth failure this verifier returns for other JWKS problems, breaking OIDC uploads with no controlled JSON error.

Useful? React with 👍 / 👎.

Two TS errors caught by astro check:
1. The `cf: { cacheTtl }` RequestInit extension is CF-Workers-only and
   our type model doesn't have it; the @ts-expect-error suppression
   was unused after the type sharpened. Drop the hint.
2. crypto.subtle.verify wants an ArrayBuffer-backed BufferSource, not
   a Uint8Array<ArrayBufferLike>. Use .slice().buffer to get a fresh
   ArrayBuffer (TextEncoder + atob both can produce Shared-backed
   buffers under recent lib.dom.d.ts).

16 OIDC tests still pass.
@github-actions

Copy link
Copy Markdown

🚀 Preview deployed

URL: https://798030dc-codeseys-personal-website.codeseys.workers.dev
Commit: 40868ab1fd3a2bc0c884c4dae6fdc0fa06cb9e23
Worker version: uploaded but not promoted (production at codeseys.io is unchanged)

Updated on every push to this PR.

@baladithyab
baladithyab merged commit 26a5cff into main May 28, 2026
3 checks passed
@baladithyab
baladithyab deleted the feat/embed-oidc-auth branch May 28, 2026 03:47
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.

1 participant