feat: generic OAuth2 handler with pluggable identity provider#203
Merged
Conversation
…rface Agent-Logs-Url: https://github.com/amalgamated-tools/goauth/sessions/787ac8e9-23f4-4628-916d-85886e3256a2 Co-authored-by: veverkap <22348+veverkap@users.noreply.github.com>
…md doc tables Agent-Logs-Url: https://github.com/amalgamated-tools/goauth/sessions/787ac8e9-23f4-4628-916d-85886e3256a2 Co-authored-by: veverkap <22348+veverkap@users.noreply.github.com>
Copilot created this pull request from a session on behalf of
veverkap
May 3, 2026 17:52
View session
…sues
- Resolve merge conflicts in docs/handler/oidc.md and handler/oidc_test.go,
keeping the more descriptive origin/main versions
- Fix errcheck: wrap defer resp.Body.Close() with func() { _ = }() pattern
- Fix staticcheck ST1005: lowercase "google userinfo" error string
- Remove unused OIDCHandler.linkOIDCSubjectBestEffort method
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a generic OAuth2-based authentication handler (for non-OIDC providers like GitHub) and refactors OIDC linking/login logic into shared helpers so both OIDC and OAuth2 flows use the same user-resolution and account-linking implementation.
Changes:
- Introduces
OAuth2Handlerwith login/callback/linking flows driven by a pluggableOAuth2IdentityProvider. - Adds shared helper functions (
findOrCreateUser, link state signing/parsing, nonce consumption, shared link callback handling) and refactorsOIDCHandlerto delegate to them. - Adds built-in OAuth2 providers (GitHub + Google userinfo fallback), comprehensive OAuth2 tests, and new/updated handler documentation.
Show a summary per file
| File | Description |
|---|---|
| handler/oauth2.go | New generic OAuth2 handler implementing login, callback, and account-linking flows. |
| handler/oauth2_common.go | New shared helpers extracted for reuse between OIDC and OAuth2 flows. |
| handler/oauth2_providers.go | New built-in OAuth2 identity providers (GitHub, Google userinfo fallback). |
| handler/oauth2_test.go | New OAuth2 handler test suite covering validation, login/callback/linking flows. |
| handler/oidc.go | Refactor to delegate linking/user-resolution logic to shared helpers. |
| handler/oidc_test.go | Fixes a broken linking test to use the nonce store abstraction. |
| docs/handler/oauth2.md | New OAuth2Handler documentation and behavior/status-code details. |
| docs/handler/oidc.md | Corrects documented meaning of link error redirect values. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
handler/oauth2.go:128
- The
Callbackdoc comment says “All error responses are JSON”, but in link flowshandleLinkCallback(...)redirects on failure via/?oauth2_link_error=.... Consider updating the comment to reflect that link-flow outcomes are communicated via redirects while non-link failures return JSON.
// Callback handles the OAuth2 provider redirect. It validates the CSRF state
// and PKCE verifier, exchanges the authorisation code, fetches the user's
// identity via Provider.FetchUserInfo, and issues JWT/session cookies.
//
// On success it redirects to "/?<LoginRedirect>". All error responses are JSON.
func (h *OAuth2Handler) Callback(w http.ResponseWriter, r *http.Request) {
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Prefix GoogleOAuth2Provider subject with "google:" to prevent cross-provider subject collisions (same convention as GitHubProvider) - Extend OAuth2Handler.Validate to check Provider, Users, and JWT for nil, failing fast at startup rather than panicking at first request - Clarify docs: link-callback early failures (missing cookies, code exchange, FetchUserInfo) still return JSON; only post-identity linking outcomes use redirect query parameters
This was referenced May 3, 2026
Closed
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
OIDCHandleris tied to providers that issueid_tokens (OIDC discovery + JWT verification). Providers like GitHub implement OAuth2 but not OIDC, requiring a separate abstraction to avoid per-provider rewrites.Core abstraction
Single interface to implement per provider:
OAuth2Handlerhandles the full flow (PKCE, CSRF, code exchange, session/refresh tokens, account linking) generically — callers only implementFetchUserInfo.New files
handler/oauth2.go—OAuth2HandlerwithValidate,Login,Callback,CreateLinkNonce,Link; mirrorsOIDCHandlerfield-for-field. AddsLoginRedirect stringto configure the post-login query param (defaultoauth2_login=1).handler/oauth2_providers.go— built-inGitHubProvider(calls/user+/user/emails, prefixes subjectsgithub:<id>to avoid cross-provider collisions) andGoogleOAuth2Provider(userinfo endpoint fallback).handler/oauth2_common.go— package-level shared helpers extracted fromOIDCHandler:findOrCreateUser,linkSubjectBestEffort,signLinkState,parseLinkState,consumeLinkNonce,handleLinkCallback. Both handlers delegate to these instead of duplicating.handler/oauth2_test.go— full test coverage for all flows.docs/handler/oauth2.md— provider interface docs, subject-prefix convention, built-ins, linking flow, status codes.Refactors / fixes
handler/oidc.go— existing methods become thin wrappers over the shared package-level functions.handler/oidc_test.go— fixed brokenTestOIDCLink_storeErrorthat referenced undefinedh.linkNonces/linkNoncetypes (pre-existing compile error).docs/handler/oidc.md— corrected pre-existing doc error:User not foundis only forErrNotFound; transient DB errors onFindByIDproduceLink verification failed.Usage
Greptile Summary
This PR introduces
OAuth2Handler, a generic OAuth2 login/linking flow that mirrorsOIDCHandlerbut delegates identity resolution to a pluggableOAuth2IdentityProviderinterface, along with built-inGitHubProviderandGoogleOAuth2Providerimplementations. Shared helpers (findOrCreateUser,handleLinkCallback, link-state signing) are extracted intooauth2_common.goso both handlers delegate to the same logic, eliminating duplication.Confidence Score: 5/5
Safe to merge; the single finding is a P2 observability improvement in an edge-case retry path that still returns an error correctly.
No P0 or P1 issues found. The one flagged item (race-retry swallowing non-ErrNotFound DB errors) is P2 — it doesn't change the error outcome (a 500 is still returned), only loses diagnostic context. All slog calls include context per the custom rule, Validate() correctly guards all required fields including Provider, and both providers correctly prefix subjects.
handler/oauth2_common.go — race-retry error handling in findOrCreateUser
Important Files Changed
Sequence Diagram
sequenceDiagram participant Browser participant OAuth2Handler participant Provider as OAuth2IdentityProvider participant OAuth2Server as OAuth2 Provider participant Users as UserStore Note over Browser,Users: Login flow Browser->>OAuth2Handler: GET /login OAuth2Handler->>Browser: Set state+verifier cookies, 302 to OAuth2Server Browser->>OAuth2Server: Authorize (PKCE) OAuth2Server->>Browser: 302 to /callback?state=&code= Browser->>OAuth2Handler: GET /callback?state=&code= OAuth2Handler->>OAuth2Handler: Validate CSRF state cookie OAuth2Handler->>OAuth2Server: Exchange code (PKCE verifier) OAuth2Server-->>OAuth2Handler: access_token OAuth2Handler->>Provider: FetchUserInfo(ctx, token) Provider-->>OAuth2Handler: OAuth2UserInfo{Subject, Email, ...} OAuth2Handler->>Users: FindByOIDCSubject / FindByEmail / CreateOIDCUser Users-->>OAuth2Handler: auth.User OAuth2Handler->>Browser: Set JWT cookie, 302 to /?LoginRedirect Note over Browser,Users: Link flow Browser->>OAuth2Handler: POST /link-nonce (authenticated) OAuth2Handler->>Browser: {nonce: ...} Browser->>OAuth2Handler: GET /link?nonce=... OAuth2Handler->>OAuth2Handler: consumeLinkNonce to userID OAuth2Handler->>OAuth2Handler: signLinkState(JWT, rand, userID) OAuth2Handler->>Browser: Set signed-state cookie, 302 to OAuth2Server OAuth2Server->>Browser: 302 to /callback?state=signedState&code= Browser->>OAuth2Handler: GET /callback (signedState) OAuth2Handler->>OAuth2Handler: parseLinkState to linkUserID OAuth2Handler->>Users: FindByID, FindByOIDCSubject, LinkOIDCSubject OAuth2Handler->>Browser: 302 to /?oauth2_linked=trueComments Outside Diff (1)
handler/oauth2.go, line 293-298 (link)Validate()does not checkProviderfor nilProvideris the single required field unique toOAuth2Handler, yetValidate()skips it. If a caller forgets to setProviderand still callsh.Validate()(which returns nil), the server starts cleanly — then panics with a nil pointer dereference the first timeCallbackreachesh.Provider.FetchUserInfo(r.Context(), token).Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (3): Last reviewed commit: "fix(handler): address PR review comments..." | Re-trigger Greptile