Skip to content

feat(oauth2): send PKCE code challenge for OIDC login sources#38202

Open
yisding wants to merge 5 commits into
go-gitea:mainfrom
yisding:oidc-pkce
Open

feat(oauth2): send PKCE code challenge for OIDC login sources#38202
yisding wants to merge 5 commits into
go-gitea:mainfrom
yisding:oidc-pkce

Conversation

@yisding

@yisding yisding commented Jun 22, 2026

Copy link
Copy Markdown

What & why

When Gitea is configured as an OAuth2/OIDC login source — i.e. Gitea acts as the client, logging users in via an external identity provider — it never sent a PKCE code_challenge on the authorization request. Identity providers that require PKCE (RFC 7636) therefore reject the sign-in.

This implements PKCE (S256) for the OpenID Connect login flow:

  • In Callout, after goth builds the authorization URL, generate a per-request verifier with oauth2.GenerateVerifier(), stash it in the user session, and append code_challenge + code_challenge_method=S256 to the redirect URL.
  • In Callback, read the stashed verifier and inject code_verifier into the callback request so goth's openidConnect provider forwards it to the token exchange.

Notes:

  • Gated to the openidConnect provider, and purely additive: spec-compliant IdPs that don't require PKCE ignore code_challenge.
  • No new settings, no DB schema/migration, and no new dependencies — golang.org/x/oauth2 already provides GenerateVerifier / S256ChallengeFromVerifier.
  • goth's provider-level authCodeOptions are shared/static and cannot carry a per-request verifier, which is why the challenge is appended to the redirect URL and the verifier is kept in Gitea's own session rather than routed through goth.

Usage

No configuration required. Existing and new OpenID Connect authentication sources automatically send a PKCE code_challenge, so IdPs that require PKCE now work. Sources whose IdP does not require PKCE are unaffected. (No UI changes.)

Testing

Automated (services/auth/source/oauth2/pkce_test.go):

  • TestPKCEChallenge — the S256 challenge equals base64url(sha256(verifier)), code_challenge_method=S256, and existing authorization params (state, client_id, scope, redirect_uri) are preserved.
  • TestInjectPKCEVerifier — pins the seam with goth: the stored verifier lands in request.URL.Query() (exactly where gothic.CompleteUserAuth reads it), callback code/state are preserved, the verifier is single-use, and non-OIDC providers are untouched.

make fmt, golangci-lint run, go vet, and the package test suite all pass.

Manual verification against a PKCE-requiring IdP (the previously failing "code_challenge is missing" flow) is recommended before marking this ready for review.

Fixes #34747.
Resolves #21376.

AI assistance disclosure

This PR was prepared with AI assistance (Claude Code, Opus 4.8). I have reviewed the change, run the linters and tests, and take responsibility for its correctness and intent.

When Gitea acts as an OIDC client (login source), it never sent a PKCE
code_challenge on the authorization request, so identity providers that
require PKCE rejected the sign-in.

Generate a per-request S256 verifier in Callout, stash it in the user
session, and append code_challenge to the authorization redirect. On the
callback, inject the stored code_verifier into the request so goth forwards
it to the token exchange. Gated to the openidConnect provider; no settings,
schema, or dependency changes (golang.org/x/oauth2 already ships the helpers).

Assisted-by: Claude Code:claude-opus-4-8[1m]
@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label Jun 22, 2026
… cleanup

Replace the hardcoded `source.Provider == "openidConnect"` check with a
SupportsPKCE() method on the Provider interface, mirroring the existing
SupportSSHPublicKey() capability. This drops a duplicated provider-name
literal and a string special-case on the shared Callout/Callback path.

Because AwsCognitoProvider embeds OpenIDProvider, Cognito login sources now
also send PKCE -- correct, since Cognito is OIDC-based and runs the token
exchange through the same goth openidConnect Session that reads code_verifier.

Also log (instead of silently dropping) the Delete/Release errors during the
single-use verifier cleanup in injectPKCEVerifier; the cleanup stays
best-effort so a failure never breaks the login.

Assisted-by: Claude Code:claude-opus-4-8[1m]
@yisding yisding marked this pull request as ready for review June 22, 2026 22:01
@github-actions github-actions Bot added the type/feature Completely new functionality. Can only be merged if feature freeze is not active. label Jun 22, 2026

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 adds PKCE (S256) support to Gitea’s OAuth2/OpenID Connect login source flow (Gitea as the OAuth client), ensuring compatibility with OIDC identity providers that require PKCE during the authorization code flow.

Changes:

  • Append code_challenge + code_challenge_method=S256 to the OIDC authorization redirect URL and store the per-request verifier in the Gitea session.
  • Inject the stored code_verifier into the OIDC callback request so goth forwards it to the token exchange.
  • Extend the OAuth2 provider abstraction with SupportsPKCE() and add unit tests for PKCE URL mutation and verifier injection behavior.

Reviewed changes

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

Show a summary per file
File Description
services/auth/source/oauth2/source_callout.go Hooks PKCE into the Callout (challenge) and Callback (verifier) flow.
services/auth/source/oauth2/providers.go Extends the Provider interface with a SupportsPKCE() capability flag.
services/auth/source/oauth2/providers_openid.go Marks the OIDC provider as PKCE-capable.
services/auth/source/oauth2/providers_base.go Provides a default SupportsPKCE() == false implementation for providers embedding BaseProvider.
services/auth/source/oauth2/pkce.go Implements session-backed PKCE verifier storage, URL challenge appending, and callback query injection.
services/auth/source/oauth2/pkce_test.go Adds unit tests for PKCE challenge construction and verifier injection / single-use cleanup.

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

Comment on lines +30 to +32
// Append a PKCE code_challenge for OIDC login sources. Remove if upstream resolves gitea#21376.
if url, err = source.beginPKCE(request, url); err != nil {
return err
Comment on lines +48 to +49
// Inject the stashed PKCE code_verifier for OIDC login sources. Remove if upstream resolves gitea#21376.
source.injectPKCEVerifier(request)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. type/feature Completely new functionality. Can only be merged if feature freeze is not active.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

openID Connect: 'code_challenge' is missing in request Implement PKCE for OpenID Connect - Unable to login with LogTo

3 participants