Skip to content

feat(github): seal the OAuth state and authorization code - #546

Merged
viktormarinho merged 1 commit into
mainfrom
feat/oauth-state-secret
Aug 20, 2026
Merged

feat(github): seal the OAuth state and authorization code#546
viktormarinho merged 1 commit into
mainfrom
feat/oauth-state-secret

Conversation

@viktormarinho

@viktormarinho viktormarinho commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Closes the last item from the original OAuth report. Follows #545 (which fixed the redirect validation) and decocms/studio#6330.

Important

Set the secret before merging, or the deploy takes the worker down:

cd github && bunx wrangler secret put OAUTH_STATE_SECRET

Use a high-entropy value, e.g. openssl rand -base64 32.

What is this contribution about?

The authorization code this server hands back to the client is the GitHub access and refresh token in plain base64url JSON, and the state carrying the redirect URI and PKCE challenge is unsigned. Anyone who observes a code, through a referrer header, a proxy log or browser history, can base64-decode it and hold a live repo read:org read:user token without ever calling /token.

#545 made that unreachable by validating the redirect target, but it did not change what the code contains. This does. Runtime 3.0.0 seals both values with AES-GCM when given a stateSecret, so this reads OAUTH_STATE_SECRET and passes it through.

Why it throws instead of warning

getStateSecret() throws on a missing variable rather than letting the runtime fall back to plaintext. A silent downgrade is indistinguishable from working, and it is precisely the failure mode this change exists to prevent. The file already fails this way for GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET.

The consequence is real: deploying without the secret set takes the worker down. That is the intended direction to fail, and it is why the secret goes in first.

How did you verify your code works?

  • bun run check github: 2 passed, 0 failed.
  • bun test github/: 112 pass, 0 fail across 7 files.
  • Sealing behaviour is covered upstream in fix(runtime)!: enforce registered redirect_uri on /authorize studio#6330: the sealed code round-trips through /token, the sealed value does not contain the upstream token, a tampered sealed code is rejected as invalid_grant, and a plaintext state is refused once a secret is configured.

I have not exercised this against the deployed worker, since it needs the production secret in place first.

How to Test

  1. bunx wrangler secret put OAUTH_STATE_SECRET from github/.
  2. Merge and let the deploy run.
  3. Connect the GitHub MCP from a real client and complete a GitHub consent.
  4. Expected: the flow completes, and the code on the callback now starts with v1. and contains no readable JSON.

Migration Notes

In-flight authorizations do not survive the rollout. A state issued before the deploy is plaintext and the sealed build refuses plaintext by design, so anyone sitting on the GitHub consent screen at cutover comes back to an error and has to retry. Deploy off-peak.

Rotating OAUTH_STATE_SECRET later has the same effect and nothing worse: it invalidates in-flight authorization requests only, not issued tokens.

Workers deploy as a single version, so there is no rolling window where some instances hold the secret and others do not, beyond a few seconds of global propagation.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Documentation is updated (if needed)
  • No breaking changes

Summary by cubic

Seals the GitHub OAuth state and authorization code to prevent plaintext token exposure. Previously the callback code contained base64url JSON tokens and the state was unsigned; now both are AES-GCM sealed when OAUTH_STATE_SECRET is set, and the server fails fast if it is missing.

Rollout and migration

  • Set the secret before merging: run from github/: bunx wrangler secret put OAUTH_STATE_SECRET. Missing secret crashes the worker by design.
  • In-flight authorizations issued before rollout will fail. Users must retry; deploy off-peak.
  • Rotating OAUTH_STATE_SECRET later invalidates only in-flight requests.
  • Callback codes change format (e.g., start with v1.) and no longer contain readable JSON.

Written for commit 4a1cc1a. Summary will update on new commits.

Review in cubic

The authorization code this server hands back is the GitHub access and
refresh token in plain base64url JSON, and the state carrying the redirect
URI and PKCE challenge is unsigned. Anyone who observes a code, through a
referrer, a proxy log or browser history, holds a live repo read:org
read:user token without ever calling /token.

@decocms/runtime 3.0.0 seals both with AES-GCM when given a stateSecret, so
read OAUTH_STATE_SECRET and pass it through.

getStateSecret() throws when the variable is missing rather than letting the
runtime fall back to plaintext. A silent downgrade here looks identical to
working and is exactly the failure this is meant to prevent.

Set the secret before merging:

    cd github && bunx wrangler secret put OAUTH_STATE_SECRET

Deploying this without it takes the worker down, which is the intended
direction to fail. In-flight authorizations do not survive the rollout: a
state issued before it is plaintext, and the sealed build refuses plaintext
by design. Users retry and it works.
@viktormarinho
viktormarinho enabled auto-merge (squash) August 20, 2026 14:59
@viktormarinho
viktormarinho disabled auto-merge August 20, 2026 15:00
@viktormarinho
viktormarinho merged commit 4b02e7c into main Aug 20, 2026
2 checks passed
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