Skip to content

Validate Forge GitHub OAuth state#15812

Open
jamesfredley wants to merge 1 commit into
8.0.xfrom
fix/forge-oauth-state-validation
Open

Validate Forge GitHub OAuth state#15812
jamesfredley wants to merge 1 commit into
8.0.xfrom
fix/forge-oauth-state-validation

Conversation

@jamesfredley

@jamesfredley jamesfredley commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

The Problem

The Forge GitHub "Create in GitHub" flow starts an OAuth authorization request but never tied the returned callback back to the request that started it. The OAuth state value was not persisted server-side and was not checked when GitHub redirected the user back with an authorization code.

Without a bound, verified state:

  • The callback is vulnerable to CSRF / login-stitching. A forged callback could get the app to exchange an attacker-supplied code.
  • There is no way to distinguish a legitimate in-flight authorization from an unsolicited or replayed callback.
  • Multiple OAuth attempts from the same user cannot be told apart.

The Fix

State is now generated up front, persisted, and validated on the way back before any code exchange happens.

  • Persist the generated state in a path-scoped, HttpOnly cookie when the authorization request is created, so the callback can be verified against it.
  • Validate the callback state against the stored value before exchanging the GitHub authorization code. No state, no exchange.
  • Reject missing or mismatched callbacks - for both the normal and error callbacks - without mutating any of the outstanding OAuth state cookies, so a bad/forged callback cannot clobber a legitimate in-flight flow.
  • Consume only the matched valid state, leaving other outstanding states untouched, so sequential/concurrent OAuth flows for the same user can each complete.

Files

  • GitHubRedirectService.java - issue, store, look up, and consume the path-scoped HttpOnly state cookie.
  • GitHubCreateController.java - validate callback state (success and error paths) before code exchange; reject on missing/mismatch.
  • GitHubOAuthStateSpec.groovy - new spec covering issuance, happy-path validation, missing/mismatched rejection, non-mutation of other outstanding states, and concurrent-flow consumption.

Testing

  • :grails-forge-api:test --tests "org.grails.forge.api.create.github.GitHubOAuthStateSpec" and the full :grails-forge-api:test module suite pass.
  • Aggregate violations (clean aggregateViolations :grails-test-report:check) clean.

Persist generated GitHub OAuth state in a path-scoped HttpOnly cookie and validate callbacks before exchanging GitHub codes.
Reject missing or mismatched callback states without mutating outstanding OAuth state cookies, and consume only the returned valid state so concurrent flows can continue.

Assisted-by: Hephaestus:openai/gpt-5.5 codex-review review-gate
Copilot AI review requested due to automatic review settings July 2, 2026 14:26

Copilot AI left a comment

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.

Pull request overview

This PR strengthens the Grails Forge GitHub OAuth flow by persisting the OAuth state in a path-scoped HttpOnly cookie and validating it on callback endpoints before proceeding, including support for multiple outstanding states.

Changes:

  • Add state generation, persistence, validation, and consumption via a path-scoped OAuth state cookie.
  • Enforce state validation for both normal and error callbacks, rejecting missing/mismatched state before exchanging OAuth codes.
  • Add a new Spock spec covering redirect/callback state cookie behavior, including multiple outstanding flows.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
grails-forge/grails-forge-api/src/test/groovy/org/grails/forge/api/create/github/GitHubOAuthStateSpec.groovy Adds test coverage for OAuth state cookie issuance, validation, and consumption across redirect/callback scenarios.
grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubRedirectService.java Introduces cookie construction/expiry/consumption helpers and wires state into the GitHub authorize URL.
grails-forge/grails-forge-api/src/main/java/org/grails/forge/api/create/github/GitHubCreateController.java Applies state validation and cookie mutation rules to the create endpoint and the OAuth error callback endpoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

throw e;
} else {
return HttpResponse.temporaryRedirect(redirectService.constructGrailsForgeErrorRedirectUrl(e.getMessage()));
return consumeOAuthStateCookie(HttpResponse.temporaryRedirect(redirectService.constructGrailsForgeErrorRedirectUrl(e.getMessage())), requestInfo, state);
if (!hasValidOAuthState(state)) {
return rejectInvalidOAuthState(redirectService.getLauncherURI());
}
redirect = redirectService.constructGrailsForgeErrorRedirectUrl(errorDescription);
.queryParam("state", state)
.build();
} catch (Exception e) {
String msg = "Failed to construct redirect URI using request " + requestInfo + " to GiHub OAuth: " + e.getMessage();
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 49.5026%. Comparing base (66cd24c) to head (e471132).

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15812        +/-   ##
==================================================
+ Coverage     49.4842%   49.5026%   +0.0184%     
- Complexity      16697      16702         +5     
==================================================
  Files            1947       1947                
  Lines           92474      92474                
  Branches        16152      16152                
==================================================
+ Hits            45760      45777        +17     
+ Misses          39606      39588        -18     
- Partials         7108       7109         +1     

see 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@testlens-app

testlens-app Bot commented Jul 2, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: e471132
▶️ Tests: 42633 executed
⚪️ Checks: 45/45 completed


Learn more about TestLens at testlens.app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants