Conversation
Documentation CheckUpdates Needed
Automated review via Coder Tasks |
|
@codex review |
There was a problem hiding this comment.
Code Review
This is a solid set of security hardening changes for the OAuth2 provider: CSRF enforcement on the consent endpoint, mandatory PKCE, exact redirect URI matching, clickjacking mitigations, and redirect_uri binding to auth codes. The overall approach is correct.
I found one important bug and a misleading comment worth fixing before merge.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 376a753e52
ℹ️ 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".
There was a problem hiding this comment.
Code Review
This PR implements important OAuth2 security hardening: CSRF protection on the consent page, mandatory PKCE (OAuth 2.1), exact redirect URI matching, clickjacking prevention, and redirect URI binding to authorization codes. The changes are well-structured and the security intent is sound.
Found 2 issues: one important logic bug where a check is unreachable, and one minor RFC compliance deviation.
|
@codex review |
There was a problem hiding this comment.
Code Review
Reviewed OAuth2 provider hardening: CSRF protection on consent endpoint, mandatory PKCE, exact redirect_uri matching, store/verify redirect_uri at token exchange, frame-ancestors CSP, and response_type=token rejection. The overall approach is solid and the security fixes are correct. Found two issues worth addressing.
Security Analysis
- CSRF protection: Correctly implemented — the nosurf
ExemptFuncnow keeps/oauth2/authorizein scope, and the HTML consent form includes<input name="csrf_token">matching nosurf'sFormFieldNameconstant. Bearer-token requests correctly bypass CSRF via the existing exempt path. ✅ - Mandatory PKCE:
code_challengeis now inRequiredNotEmptyandVerifyPKCEusessubtle.ConstantTimeCompare. Unconditional verification at token exchange is correct. ✅ - redirect_uri store & verify: Stored as
params.redirectURL.String()(normalized), verified as exact string match inauthorizationCodeGrant. RFC 6749 §4.1.3 compliance is correct — when redirect_uri was absent during authorization,RedirectUri.Valid=falseso no check is performed at token exchange. ✅ - frame-ancestors CSP: Added to
RenderOAuthAllowPagebefore the template executes. Both CSP and X-Frame-Options for defense-in-depth is correct. ✅ - Implicit grant rejection: Correctly returns
unsupported_response_typeinProcessAuthorize. ✅ - state_hash: Stored but intentionally not verified server-side (state verification is the OAuth2 client's responsibility). The migration is correct. ✅
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 64731246a2
ℹ️ 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".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93897f17f3
ℹ️ 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".
|
@Emyrk correction on my prior note: this is scoped to the OAuth consent page renderer (RenderOAuthAllowPage) only, and it does not change the SPA/global CSP used by AI tasks. So framing restrictions are limited to the consent UI as a clickjacking defense. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1cda1a91aa
ℹ️ 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".
jdomeracki-coder
left a comment
There was a problem hiding this comment.
@ThomasK33 we should adjust the PR description to remove the incorrect statement around state enforcement
As discussed state parameter isn't required in accordance with the corresponding RFC
Thanks, updated it. |
…ct URI matching Update all OAuth2 tests to comply with the new OAuth 2.1 security requirements where state, code_challenge, and code_verifier are now mandatory parameters. Changes: - coderd/oauth2_test.go: Add generatePKCE helper; update authorizationFlow to return verifier; update all 4 callers and 3 manual flows; update customTokenExchange to accept codeVerifier; update NestedPath test to expect authError (exact redirect URI matching) - coderd/oauth2provider/oauth2providertest/oauth2_test.go: Rename TestOAuth2WithoutPKCE to TestOAuth2WithoutPKCEIsRejected (expects 400); add PKCE to ClientSecretBasic and ClientSecretBasicInvalidSecret tests - coderd/mcp/mcp_e2e_test.go: Add mcpGeneratePKCE helper; add PKCE to static and dynamic client auth flows and token exchanges - coderd/database/dbgen/dbgen.go: Add missing RedirectUri field to OAuth2ProviderAppCode seed

Summary
Harden the OAuth2 provider with multiple security fixes addressing
coder/security#121(CSRF session takeover) and converge on OAuth 2.1 compliance.Security Fixes
/oauth2/authorizeba7d646,b94a64eframe-ancestorsCSPContent-Security-Policy: frame-ancestors 'none'+X-Frame-Options: DENY)597aeb273d64b1,93897f1redirect_uri50569b9,d7ca315code_challengeat authorization (forresponse_type=code) + unconditionalcode_verifierverification at token exchanged7ca315,1cda1a9response_type=tokennow returnsunsupported_response_typeerror page (OAuth 2.1 removes implicit flow)d7ca315,91b8863Changes by File
coderd/httpmw/csrf.go— Extended the CSRFExemptFuncto enforce CSRF on/oauth2/authorizein addition to/apiroutes. The consent form POST is now CSRF-protected to prevent cross-site authorization code theft.site/site.go— AddedContent-Security-Policy: frame-ancestors 'none'andX-Frame-Options: DENYheaders toRenderOAuthAllowPage(consent page only — does not affect the SPA/global CSP used by AI tasks).coderd/httpapi/queryparams.go— ChangedRedirectURLfrom prefix matching (strings.HasPrefix(v.Path, base.Path)) to full URI exact matching (v.String() != base.String()), comparing scheme, host, path, and query.coderd/oauth2provider/authorize.go— Added PKCE enforcement:code_challengeis required whenresponse_type=code(via a conditional check, notRequiredNotEmpty, soresponse_type=tokencan reach the explicit rejection path).ShowAuthorizePage(GET) validatesresponse_typebefore rendering and returns a 400 error page for unsupported types.ProcessAuthorize(POST) stores theredirect_uriwith the auth code when explicitly provided.coderd/oauth2provider/tokens.go— PKCE verification is now unconditional (not gated oncode_challengebeing present in DB). If the stored code has aredirect_uri, the token endpoint verifies it matches exactly — mismatch returnserrBadCode→invalid_grant. Missingcode_verifierreturnsinvalid_grant.codersdk/oauth2.go—OAuth2ProviderResponseTypeTokenconstant andValid()acceptance are kept so the authorize handler can parseresponse_type=tokenand return the properunsupported_response_typeerror rather than failing at parameter validation.coderd/database/migrations/000421_*— Addedredirect_uri textcolumn tooauth2_provider_app_codes.Design Decisions
stateparameter remains optional — The plan initially requiredstateviaRequiredNotEmpty, but this was reverted in376a753to avoid breaking existing clients. Thestateis still hashed and stored when provided (viastate_hashcolumn), securing clients that opt in.response_type=tokenkept inValid()— Removing it fromValid()would cause the parameter parser to reject the request before the authorize handler can return the properunsupported_response_typeerror. The constant is kept for correct error handling flow.CSP scoped to consent page only —
frame-ancestors 'none'is set only on the OAuth consent page renderer, not globally. The SPA/global CSP was previously changed to allow framing for AI tasks (#18102); this change does not regress that.Out of Scope (follow-up PRs)
📋 Implementation Plan
Plan: Harden OAuth2 Provider — Security Fixes + OAuth 2.1 Compliance
Context & Why
Security issue
coder/security#121reports a critical session takeover via CSRF on the OAuth2 provider. This plan covers all remaining security fixes from that issue plus convergence on OAuth 2.1 requirements. The goal is a single PR that closes all actionable gaps.Current State (already committed on branch
csrf-sjx1)/oauth2/authorizeba7d646,b94a64eb94a64estate_hashcolumn + storage9167d83,b94a64ee4119b5Remaining Work
Fix 2 — Require(DROPPED)stateparameterRollback: Remove
"state"from theRequiredNotEmptycall incoderd/oauth2provider/authorize.go:42:No test changes needed — tests already pass
statevoluntarily.Fix 4 — Exact redirect URI matching
Currently
coderd/httpapi/queryparams.go:233uses prefix matching:OAuth 2.1 requires exact string matching. Change to:
File:
coderd/httpapi/queryparams.go—RedirectURLmethodAlso update the error message from "must be a subset of" to "must exactly match".
Additionally, store
redirect_uriwith the auth code and verify at the token endpoint (RFC 6749 §4.1.3):000421): Addredirect_uri textcolumn tooauth2_provider_app_codescoderd/database/queries/oauth2.sqlto includeredirect_uricoderd/oauth2provider/authorize.go: Storeparams.redirectURL.String()when inserting the codecoderd/oauth2provider/tokens.go: After retrieving the code from DB, verify thatredirect_urifrom the token request matches the stored value exactly. Currentlytokens.go:103callsp.RedirectURL(vals, callbackURL, "redirect_uri")for prefix validation only — it must compare against the stored redirect_uri from the code, not just the app's callback URL.Why both exact match AND store+verify?
Exact matching at the authorize endpoint prevents open redirectors (attacker can't use a sub-path).
Storing and verifying at the token endpoint prevents code injection — an attacker who steals a code can't exchange it with a different redirect_uri than was originally authorized. This is required by RFC 6749 §4.1.3 and OAuth 2.1.
Fix 7 —
frame-ancestorsCSP on consent pageThe consent page can be iframed by a workspace app (same-site), which is the attack vector. Add a
Content-Security-Policyheader to prevent framing.File:
site/site.go—RenderOAuthAllowPagefunction (~line 731)Before writing the response, add:
Both headers for defense-in-depth (CSP for modern browsers, X-Frame-Options for legacy).
OAuth 2.1 — Mandatory PKCE
Currently PKCE is checked only when
code_challengewas provided during authorization (tokens.go:258):OAuth 2.1 requires PKCE for ALL authorization code flows. Change to:
File:
coderd/oauth2provider/authorize.go— Add"code_challenge"to required params:File:
coderd/oauth2provider/tokens.go:257-265— Make PKCE verification unconditional:File:
codersdk/oauth2.go— RemoveOAuth2ProviderResponseTypeTokenfrom the enum or reject it explicitly in the authorize handler. Currently it's defined at line 216 but the handler ignoresresponse_typeand always issues a code. We should either:"token"variant from the enum and reject it withunsupported_response_type, ORProcessAuthorizethat rejectsresponse_type=tokenOption (b) is simpler and more backwards-compatible:
OAuth 2.1 — Bearer tokens in query strings
coderd/httpmw/apikey.go:743acceptsaccess_tokenfrom URL query parameters. OAuth 2.1 prohibits this. However, this may be used internally (e.g., workspace apps, DERP). Need to audit callers before removing.Approach: This is a larger change with potential breakage. Mark as a separate follow-up issue rather than including in this PR. Document the finding.
OAuth 2.1 — Removed flows
✅ Already compliant.
tokens.goonly supportsauthorization_codeandrefresh_tokengrant types. The implicit grant (response_type=token) will be explicitly rejected per the PKCE section above.OAuth 2.1 — Refresh token rotation
✅ Already compliant.
tokens.go:442deletes the old API key when a refresh token is used.Migration Plan
All DB changes can go in a single new migration (or extend 000420 if the branch is rebased before merge). Columns to add:
redirect_uri textonoauth2_provider_app_codesThe
state_hashcolumn is already added by migration 000420.Implementation Order
Fix 2 — Require(DROPPED — state stays optional)stateparametercode_challenge+ rejectresponse_type=token"state"fromRequiredNotEmptyinauthorize.gomake genafter DB changesOut of Scope (separate PRs)
Generated with
mux• Model:anthropic:claude-opus-4-6• Thinking:xhigh