Skip to content

fix(oauth): validate discovered endpoints before merge/use (#511)#739

Merged
gnanam1990 merged 1 commit into
mainfrom
fix/oauth-validate-discovered-endpoints
Jul 19, 2026
Merged

fix(oauth): validate discovered endpoints before merge/use (#511)#739
gnanam1990 merged 1 commit into
mainfrom
fix/oauth-validate-discovered-endpoints

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #511.

Discovery-learned OAuth endpoints were merged and used without the https/loopback endpoint-safety rule that configured endpoints already pass. A malicious issuer's discovery document could therefore downgrade the authorization, token, device, or MCP registration endpoint after the provider config had already validated — silently redirecting the login to an attacker-controlled http:// origin. This affects both the provider OAuth flow and the MCP OAuth flow (MCP delegates discovery to internal/oauth).

Root cause

  • internal/oauth/providers.go validates configured endpoints via validateTokenEndpoint, but internal/oauth/manager.go resolveEndpoints merged discovered endpoints with no revalidation.
  • internal/mcp/oauth.go resolveAuthorizationServer likewise merged discovered MCP metadata without validation.
  • internal/oauth/flow.go BuildAuthorizationURL parsed the authorization endpoint but never checked its scheme, so a downgraded endpoint reached the browser.

Fix — defense in depth (3 layers)

  1. One shared rule. New exported oauth.ValidateEndpointURL (https, or http on a loopback host); validateTokenEndpoint now delegates to it so configured and discovered endpoints share a single rule.
  2. Backstop at the choke point. BuildAuthorizationURL validates the authorization endpoint. Both the provider loopback flow and the MCP flow build their browser URL through this function, so a downgraded endpoint can never open in the browser even if a merge site is missed.
  3. Validate before merge, fail closed.
    • Provider: resolveEndpoints validates each discovered authorization / token / device endpoint before merging.
    • MCP: new validateResolvedEndpoints applied in both return branches of resolveAuthorizationServer (authorization / token / registration, configured and discovered alike).

Scope

internal/oauth + internal/mcp only. No config schema change. No behavior change for valid endpoints — https and http-loopback logins are unaffected. No performance change expected.

Testing

  • 3 regression tests — provider resolveEndpoints, MCP resolveAuthorizationServer, and the BuildAuthorizationURL backstop. Each was confirmed to fail without the fix (reverted the source, kept the tests) and pass with it; the pre-existing configured-endpoint test is unchanged.
  • go test -race ./internal/oauth/... ./internal/mcp/... green. go build ./..., go vet ./..., gofmt, govulncheck, git diff HEAD --check all clean.
  • Manual end-to-end through the real CLI. With a discovery document serving authorization_endpoint: http://evil.example/authorize, zero auth login <provider> now exits 1 with refusing to send credential to a non-https token endpoint: http://evil.example and never opens the browser — whereas unpatched main builds and opens that attacker URL. A legitimate https discovery login still succeeds (happy path intact).

Follow-up

The paired hardening for #729 (token-endpoint redirect replay in PostToken) is a separate PR on the same files.

Summary by CodeRabbit

  • Security

    • OAuth authorization, token, registration, and device-authorization endpoints now require HTTPS (HTTP allowed only for loopback).
    • Insecure endpoints from configuration or discovery are rejected before proceeding with OAuth flows.
    • Authorization URL building now validates the endpoint and won’t generate downgraded HTTP links.
  • Bug Fixes

    • Discovery/endpoint resolution now validates discovered endpoint URLs and fails safely when invalid.
  • Tests

    • Added coverage to ensure insecure discovered/configured endpoints are rejected.

Discovery metadata was merged without the https/loopback endpoint rule that
configured endpoints already pass, letting a malicious issuer downgrade the
authorization/token/device/registration endpoint after config validation.

- Add exported ValidateEndpointURL as the single endpoint-safety rule;
  validateTokenEndpoint now delegates to it.
- Backstop in BuildAuthorizationURL (the choke point both the provider and MCP
  flows build their browser URL through).
- Validate each discovered endpoint before merge in the provider resolveEndpoints
  and validate resolved MCP metadata in both branches; fail closed on insecure.
@gnanam1990
gnanam1990 force-pushed the fix/oauth-validate-discovered-endpoints branch from e5be0f1 to d6a3ff6 Compare July 18, 2026 18:27
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1a5933a7-56b6-4110-a7e5-6068fc5c1c00

📥 Commits

Reviewing files that changed from the base of the PR and between e5be0f1 and d6a3ff6.

📒 Files selected for processing (6)
  • internal/mcp/oauth.go
  • internal/mcp/oauth_test.go
  • internal/oauth/flow.go
  • internal/oauth/flow_test.go
  • internal/oauth/manager.go
  • internal/oauth/manager_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • internal/mcp/oauth.go
  • internal/mcp/oauth_test.go
  • internal/oauth/flow_test.go
  • internal/oauth/manager_test.go
  • internal/oauth/manager.go
  • internal/oauth/flow.go

Walkthrough

OAuth endpoint validation is centralized in ValidateEndpointURL and applied to configured, discovered, and resolved provider/MCP endpoints. Authorization URL construction and discovery merges now reject insecure non-loopback HTTP endpoints.

Changes

OAuth endpoint safety

Layer / File(s) Summary
Shared endpoint validation
internal/oauth/flow.go, internal/oauth/flow_test.go
Adds the shared HTTPS/loopback validation rule, reuses it for token endpoints, and rejects insecure authorization URLs.
Provider discovery validation
internal/oauth/manager.go, internal/oauth/manager_test.go
Validates discovered authorization, token, and device-authorization endpoints before merging them into configuration.
MCP resolved metadata validation
internal/mcp/oauth.go, internal/mcp/oauth_test.go
Validates configured and discovered authorization, token, and registration endpoints before returning OAuth metadata.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • Gitlawb/zero#210: Introduces the shared OAuth endpoint-validation logic reused by this change.
  • Gitlawb/zero#212: Modifies MCP OAuth endpoint resolution and shared authorization-flow integration.
  • Gitlawb/zero#586: Modifies the MCP OAuth endpoint-resolution flow updated here.

Suggested reviewers: vasanthdev2004, jatmn, kevincodex1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: validating discovered OAuth endpoints before they are merged or used.
Linked Issues check ✅ Passed The PR matches #511 by rejecting insecure discovered authorization, token, device authorization, and MCP registration endpoints in both flows.
Out of Scope Changes check ✅ Passed The changes stay within the OAuth endpoint-validation work and only add supporting tests and validation helpers.
✨ 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/oauth-validate-discovered-endpoints

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/oauth/flow.go (1)

93-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider renaming ErrInsecureTokenEndpoint to a general sentinel.

ValidateEndpointURL is now the shared rule for authorization, token, device-authorization, and registration endpoints, but the wrapped sentinel is still named ErrInsecureTokenEndpoint. When an authorization or registration endpoint fails, the error chain reads "…insecure token endpoint: http://evil.example", which is confusing for debugging.

Renaming to ErrInsecureEndpoint (or similar) would align the sentinel with its broader usage. This is a low-priority naming improvement since the errors.Is checks work correctly regardless.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/oauth/flow.go` around lines 93 - 110, Rename the shared sentinel
ErrInsecureTokenEndpoint to a general ErrInsecureEndpoint and update
ValidateEndpointURL plus all references and errors.Is checks to use the new
name. Preserve the existing validation behavior and error wrapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/oauth/flow.go`:
- Around line 93-110: Rename the shared sentinel ErrInsecureTokenEndpoint to a
general ErrInsecureEndpoint and update ValidateEndpointURL plus all references
and errors.Is checks to use the new name. Preserve the existing validation
behavior and error wrapping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c2003f85-6a1c-4bb4-9722-1812c981c959

📥 Commits

Reviewing files that changed from the base of the PR and between 015452c and e5be0f1.

📒 Files selected for processing (6)
  • internal/mcp/oauth.go
  • internal/mcp/oauth_test.go
  • internal/oauth/flow.go
  • internal/oauth/flow_test.go
  • internal/oauth/manager.go
  • internal/oauth/manager_test.go

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: d6a3ff62c09e
Changed files (6): internal/mcp/oauth.go, internal/mcp/oauth_test.go, internal/oauth/flow.go, internal/oauth/flow_test.go, internal/oauth/manager.go, internal/oauth/manager_test.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at d6a3ff6. This is the right shape for the fix: one exported rule, validation at every merge site, and a backstop at the choke point both flows funnel through. Approving.

What I checked:

  • Traced every credential-bearing path to a validation: device start and poll (device.go:49/157), token exchange (flow.go:176), the browser URL choke point (flow.go:46), provider config (providers.go:112), both discovered merges (manager.go:226-238), and both MCP branches (mcp/oauth.go:149). I could not find an endpoint that reaches the wire unvalidated; PostToken redirect replay is scoped out to your #729 follow-up, which is fine.
  • Threw some hostile URLs at ValidateEndpointURL: http://localhost@evil.com resolves Hostname to evil.com and is rejected, scheme-relative //evil.com/x has no scheme and is rejected, http://127.0.0.1.evil.com fails ParseIP and is rejected, http://[::1]:8080 is correctly allowed. Everything ambiguous fails closed.
  • Independently re-verified your fail-without-fix claim: kept the three new tests, reverted flow.go/manager.go/mcp/oauth.go to main, and all three fail; restored, they pass. Race run on both packages is green here too.

One behavior change worth stating plainly for the changelog: the MCP config branch now validates CONFIGURED endpoints as well, so a working (if ill-advised) http non-loopback MCP OAuth setup goes from failing at token time with a confusing error to failing up front with a clear one. That is the right call, just worth remembering if someone reports it as a regression.

Non-blocking nit: the backstop surfaces ErrInsecureTokenEndpoint for an authorization endpoint, so the message says "token endpoint" about a URL that is not one (your own manual-test transcript shows it). Renaming the exported error would break errors.Is callers, so leaving it is fine; a message tweak can ride along some other day.

@anandh8x anandh8x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed head d6a3ff6. The fix is sound and defense-in-depth is placed correctly: one shared HTTPS-or-loopback rule, validation before provider/MCP discovery merges, validation of configured MCP endpoints in both resolver branches, and a BuildAuthorizationURL backstop before either flow opens the browser. I traced authorization, token, device-authorization, and registration endpoints to validation before credential-bearing use; spoofed loopback host forms fail through URL Hostname plus net.ParseIP handling. Redirect replay remains correctly scoped to the separate #729 follow-up. OAuth/MCP race tests, vet, and diff hygiene pass. Approving.

@gnanam1990
gnanam1990 merged commit ce4a996 into main Jul 19, 2026
9 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.

oauth: validate discovered OAuth endpoints before login

3 participants