Skip to content

fix(google-oauth): add PKCE support to fix missing code_verifier error#1481

Merged
mergify[bot] merged 1 commit intomainfrom
fix/google-oauth-pkce-missing-code-verifier
Apr 29, 2026
Merged

fix(google-oauth): add PKCE support to fix missing code_verifier error#1481
mergify[bot] merged 1 commit intomainfrom
fix/google-oauth-pkce-missing-code-verifier

Conversation

@markturansky
Copy link
Copy Markdown
Contributor

@markturansky markturansky commented Apr 29, 2026

Summary

  • workspace-mcp generates Google OAuth URLs with PKCE (code_challenge + code_challenge_method=S256), but the backend's /oauth2callback handler was calling exchangeOAuthCode without the corresponding code_verifier, causing every token exchange to fail with invalid_grant: Missing code verifier
  • Add PKCE generation in GetGoogleOAuthURLGlobal and GetOAuthURL: generate a random code_verifier, compute the S256 code_challenge, store the verifier in a K8s Secret (oauth-pkce-verifiers) keyed by SHA256(state), and include the challenge in the auth URL
  • Retrieve and delete the stored verifier at the start of HandleOAuth2Callback, then thread it through both the cluster-level (HandleGoogleOAuthCallback) and legacy session-specific exchange paths
  • Update exchangeOAuthCode to accept an optional codeVerifier param and append code_verifier to the POST body when non-empty; retrieval is non-fatal so unknown states (e.g. from third-party tools) still fall through without PKCE

Root cause

2026/04/29 18:21:24 Failed to exchange OAuth code: token exchange failed with status 400: {
  "error": "invalid_grant",
  "error_description": "Missing code verifier."
}

The auth URL included code_challenge (PKCE-initiated), so Google required code_verifier during token exchange. The backend was omitting it entirely.

Test plan

  • 39 new unit tests added in handlers/oauth_test.go covering all new functions: generateCodeVerifier, generateCodeChallenge, pkceSecretKey, storePKCEVerifier, retrievePKCEVerifier, exchangeOAuthCode, storeGoogleCredentials, GetGoogleCredentials, GetGoogleOAuthURLGlobal, GetGoogleOAuthStatusGlobal, DisconnectGoogleOAuthGlobal, and full PKCE verifier lifecycle
  • Run go test -tags test -run TestBackend ./handlers/ --ginkgo.label-filter="google-auth" — all 39 pass
  • Full handler test suite passes with no regressions (go test -tags test -run TestBackend ./handlers/)
  • Connect Google Drive via the platform UI integrations page and verify the callback succeeds

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Implemented PKCE (Proof Key for Code Exchange) support for Google OAuth authentication with verifier generation, persistence, and enhanced callback handling.
  • Tests

    • Added comprehensive test coverage for Google OAuth workflows, including PKCE code operations, verifier management, and end-to-end token exchange validation.

…ing code_verifier error

workspace-mcp generates Google OAuth URLs with PKCE (code_challenge +
code_challenge_method=S256), but the backend's /oauth2callback handler
was calling exchangeOAuthCode without the code_verifier, causing Google
to reject every token exchange with:

  invalid_grant: Missing code verifier.

Fix:
- Generate a cryptographically random code_verifier in GetGoogleOAuthURLGlobal
  and GetOAuthURL (session-specific flow)
- Store the verifier in a K8s Secret (oauth-pkce-verifiers) keyed by
  SHA256(state) so keys are always valid K8s secret data keys regardless
  of base64 padding characters in the state token
- Retrieve and delete the verifier (one-time use) at the start of
  HandleOAuth2Callback, then pass it through both the cluster-level and
  legacy session-specific token exchange paths
- Update exchangeOAuthCode to accept an optional codeVerifier parameter
  and include it as code_verifier in the POST body when non-empty
- Retrieval is non-fatal: unknown states (e.g. from third-party MCP tools)
  return "" and fall through to exchange without PKCE, preserving
  backwards compatibility

Also add 39 unit tests covering all new functions:
  generateCodeVerifier, generateCodeChallenge, pkceSecretKey,
  storePKCEVerifier, retrievePKCEVerifier, exchangeOAuthCode,
  storeGoogleCredentials, GetGoogleCredentials,
  GetGoogleOAuthURLGlobal, GetGoogleOAuthStatusGlobal,
  DisconnectGoogleOAuthGlobal, and the full PKCE lifecycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 29, 2026

Deploy Preview for cheerful-kitten-f556a0 canceled.

Name Link
🔨 Latest commit 4fbd7ea
🔍 Latest deploy log https://app.netlify.com/projects/cheerful-kitten-f556a0/deploys/69f252642d72810008cdf7d5

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

Adds PKCE (Proof Key for Code Exchange) support to the Google OAuth flow by generating and storing verifier/challenge pairs, persisting them in Kubernetes Secrets, and including the verifier during token exchange. Function signatures for OAuth callback and token exchange handlers are updated to accept the code verifier parameter.

Changes

Cohort / File(s) Summary
PKCE Implementation
components/backend/handlers/oauth.go
Implements PKCE flow with verifier/challenge generation, K8s Secret persistence keyed by HMAC state, retrieval during callback, and conditional code_verifier inclusion in token requests. Updates HandleGoogleOAuthCallback and exchangeOAuthCode function signatures.
PKCE Test Coverage
components/backend/handlers/oauth_test.go
Comprehensive test suite validating PKCE utilities (RFC 7636 compliance, S256 challenge computation), K8s secret persistence (round-trip, one-time consumption), token exchange HTTP behavior, and Gin handler integration with mocked Google endpoint.
Test Constants
components/backend/tests/constants/labels.go
Adds LabelGoogleAuth constant for test categorization.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as Client<br/>(Web App)
    participant Handler as Backend<br/>Handler
    participant K8s as Kubernetes<br/>Secret
    participant Google as Google OAuth<br/>Provider

    Client->>Handler: Request OAuth URL
    Handler->>Handler: Generate PKCE verifier<br/>& challenge
    Handler->>K8s: Store verifier<br/>(keyed by state)
    Handler->>Client: Return URL with<br/>code_challenge
    Client->>Google: Redirect with<br/>code_challenge
    User->>Google: Authenticate
    Google->>Client: Redirect with code
    Client->>Handler: Callback with code
    Handler->>K8s: Retrieve verifier
    Handler->>Google: Exchange code<br/>+ verifier
    Google->>Handler: Return token
    Handler->>K8s: Remove verifier
    Handler->>Client: Auth complete
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Security And Secret Handling ❌ Error Error handling at lines 285-289 downgrades all K8s infrastructure failures to empty verifier, masking backend outages behind generic OAuth errors. Only downgrade NotFound errors to empty verifier for backward compatibility; return real K8s failures as HTTP 500 to surface infrastructure issues.
Kubernetes Resource Safety ⚠️ Warning Two cluster-level Secrets (oauth-pkce-verifiers, google-oauth-credentials) lack OwnerReferences, creating orphaned resources without garbage collection. Add OwnerReferences to cluster-level Secrets or store in user/session namespaces. Replicate the session-scoped credentials pattern demonstrated in storeCredentialsInSecret().
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits format (fix(google-oauth): ...) and accurately describes the main change: adding PKCE support to resolve a missing code_verifier error in Google OAuth token exchange.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Performance And Algorithmic Complexity ✅ Passed PKCE implementation has no meaningful performance regressions or algorithmic complexity issues. All K8s operations are bounded (single Get/Update on named Secrets with no List operations), retry loops capped at 3 iterations, and expensive operations occur sequentially outside loops with O(1) API overhead per OAuth flow step.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/google-oauth-pkce-missing-code-verifier
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/google-oauth-pkce-missing-code-verifier

Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/backend/handlers/oauth.go (1)

284-302: ⚠️ Potential issue | 🔴 Critical

Validate state before routing the cluster callback.

Lines 294-302 still branch on JSON decoded from state without verifying the HMAC or timestamp first. With the new PKCE flow, Lines 285-289 also consume the stored verifier before that validation happens. A forged or replayed callback can therefore burn a real verifier, and the cluster path can exchange/store tokens for an attacker-controlled userID.

Please move state verification ahead of both retrievePKCEVerifier(...) and the cluster dispatch, and make the cluster path consume only validated state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/oauth.go` around lines 284 - 302, The state must
be validated (verify HMAC and timestamp) before parsing/dispatching or consuming
PKCE verifiers; move the call to retrievePKCEVerifier(c.Request.Context(),
state) and any single-use consumption until after you validate the state blob,
and only decode/unmarshal state into stateMap after validation succeeds; then,
when branching to the cluster path (the code that calls
HandleGoogleOAuthCallback), pass the validated stateMap and consume the
codeVerifier there (or retrieve it just before exchange) so a forged or replayed
callback cannot burn a real verifier or allow attacker-controlled userID
storage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/backend/handlers/oauth.go`:
- Around line 285-289: The current code in the oauth handler treats any non-nil
error from retrievePKCEVerifier as “not found” and falls back to
codeVerifier="", hiding real failures; change the logic in the block handling
retrievePKCEVerifier(c.Request.Context(), state) so that you only fall back to
an empty codeVerifier when the function returns the explicit “not found”
sentinel/zero-error case (the same value retrievePKCEVerifier uses to indicate
unknown state), but for all other non-nil errors (e.g., K8s read/update
failures) log the error with context and return/abort the request (e.g.,
500/internal error) instead of proceeding with an empty verifier; reference
retrievePKCEVerifier and the codeVerifier variable to locate and update the
branch.

---

Outside diff comments:
In `@components/backend/handlers/oauth.go`:
- Around line 284-302: The state must be validated (verify HMAC and timestamp)
before parsing/dispatching or consuming PKCE verifiers; move the call to
retrievePKCEVerifier(c.Request.Context(), state) and any single-use consumption
until after you validate the state blob, and only decode/unmarshal state into
stateMap after validation succeeds; then, when branching to the cluster path
(the code that calls HandleGoogleOAuthCallback), pass the validated stateMap and
consume the codeVerifier there (or retrieve it just before exchange) so a forged
or replayed callback cannot burn a real verifier or allow attacker-controlled
userID storage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 9876fdb9-ff3f-46d2-9c70-0549b48f4f82

📥 Commits

Reviewing files that changed from the base of the PR and between 975bb5e and 4fbd7ea.

📒 Files selected for processing (3)
  • components/backend/handlers/oauth.go
  • components/backend/handlers/oauth_test.go
  • components/backend/tests/constants/labels.go

Comment on lines +285 to +289
codeVerifier, verifierErr := retrievePKCEVerifier(c.Request.Context(), state)
if verifierErr != nil {
log.Printf("Warning: could not retrieve PKCE verifier for state: %v", verifierErr)
codeVerifier = ""
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't downgrade PKCE storage failures into a non-PKCE exchange.

retrievePKCEVerifier already returns ("", nil) when the state is unknown. Lines 286-289 treat all other errors the same way, so a real K8s read/update failure gets turned into codeVerifier="", which then fails later as invalid_grant and hides the actual outage.

Only fall back on the explicit “not found” case; surface real retrieval errors to the client/server logs as a backend failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/oauth.go` around lines 285 - 289, The current
code in the oauth handler treats any non-nil error from retrievePKCEVerifier as
“not found” and falls back to codeVerifier="", hiding real failures; change the
logic in the block handling retrievePKCEVerifier(c.Request.Context(), state) so
that you only fall back to an empty codeVerifier when the function returns the
explicit “not found” sentinel/zero-error case (the same value
retrievePKCEVerifier uses to indicate unknown state), but for all other non-nil
errors (e.g., K8s read/update failures) log the error with context and
return/abort the request (e.g., 500/internal error) instead of proceeding with
an empty verifier; reference retrievePKCEVerifier and the codeVerifier variable
to locate and update the branch.

@mergify mergify Bot added the queued label Apr 29, 2026
@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented Apr 29, 2026

Merge Queue Status

  • Entered queue2026-04-29 19:04 UTC · Rule: default
  • Checks skipped · PR is already up-to-date
  • Merged2026-04-29 19:04 UTC · at 4fbd7ead2104920d83b329973b2a6ca61c1f7f37 · squash

This pull request spent 15 seconds in the queue, including 2 seconds running CI.

Required conditions to merge

@mergify mergify Bot merged commit 0b405b4 into main Apr 29, 2026
68 checks passed
@mergify mergify Bot deleted the fix/google-oauth-pkce-missing-code-verifier branch April 29, 2026 19:04
@mergify mergify Bot removed the queued label Apr 29, 2026
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