feat(github): seal the OAuth state and authorization code - #546
Merged
Conversation
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
enabled auto-merge (squash)
August 20, 2026 14:59
viktormarinho
disabled auto-merge
August 20, 2026 15:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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
statecarrying 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 liverepo read:org read:usertoken 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 readsOAUTH_STATE_SECRETand 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 forGITHUB_CLIENT_IDandGITHUB_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./token, the sealed value does not contain the upstream token, a tampered sealed code is rejected asinvalid_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
bunx wrangler secret put OAUTH_STATE_SECRETfromgithub/.codeon the callback now starts withv1.and contains no readable JSON.Migration Notes
In-flight authorizations do not survive the rollout. A
stateissued 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_SECRETlater 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
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_SECRETis set, and the server fails fast if it is missing.Rollout and migration
github/: bunx wrangler secret put OAUTH_STATE_SECRET. Missing secret crashes the worker by design.OAUTH_STATE_SECRETlater invalidates only in-flight requests.Written for commit 4a1cc1a. Summary will update on new commits.