Skip to content

feat: replace custom OAuth with goth provider adapter#55

Merged
itsLeonB merged 1 commit into
mainfrom
dev
Jun 12, 2026
Merged

feat: replace custom OAuth with goth provider adapter#55
itsLeonB merged 1 commit into
mainfrom
dev

Conversation

@itsLeonB

@itsLeonB itsLeonB commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

Replace the custom google_provider_service.go (~100 LOC of manual token exchange + userinfo fetch) with a goth-based provider adapter.

Changes

  • Add github.com/markbates/goth dependency
  • Implement gothProviderAdapter wrapping goth.Provider with internal provider map
  • Update StateStore interface to store/return goth session value alongside state
  • Update ProviderService interface to accept provider name as parameter
  • Simplify OAuthService to use single ProviderService instead of manual map lookup
  • Remove http.Client dependency from OAuthService
  • Delete google_provider_service.go

What was tested

  • make lint — 0 issues
  • make build-all — all 3 binaries compile
  • make test — all tests pass

Summary by CodeRabbit

  • Refactor
    • Refactored OAuth authentication system to improve session handling and provider support. Session data is now properly stored and retrieved during the OAuth callback process for more reliable authentication flows.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@itsLeonB, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 26 minutes and 7 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5142dd79-109f-40d0-a397-51656155806d

📥 Commits

Reviewing files that changed from the base of the PR and between 5962f4b and ea45157.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • go.mod
  • internal/adapters/core/service/store/in_memory_state_store.go
  • internal/adapters/core/service/store/nats_kv_state_store.go
  • internal/adapters/core/service/store/nats_kv_state_store_test.go
  • internal/core/service/store/state_store.go
  • internal/domain/service/oauth/google_provider_service.go
  • internal/domain/service/oauth/provider_service.go
  • internal/domain/service/oauth_service.go
  • internal/provider/service_provider.go
📝 Walkthrough

Walkthrough

This PR refactors OAuth provider handling to use the goth library with dynamic provider selection. The StateStore interface now persists session values alongside state keys, enabling stateless OAuth session reconstruction. A new goth-based adapter replaces per-provider implementations and eliminates HTTP client injection from service wiring.

Changes

OAuth Provider Abstraction and State Store Refactoring

Layer / File(s) Summary
State Store Contract and Implementations
internal/core/service/store/state_store.go, internal/adapters/core/service/store/in_memory_state_store.go, internal/adapters/core/service/store/nats_kv_state_store.go, internal/adapters/core/service/store/nats_kv_state_store_test.go
StateStore interface updated to persist values alongside state keys via Store(ctx, state, value, expiry) and retrieve them via VerifyAndDelete(ctx, state) (string, error). In-memory and NATS KV implementations updated with tests validating session value round-trip.
OAuth Provider Service with Goth Adapter
go.mod, internal/domain/service/oauth/provider_service.go
Added goth v1.82.0 dependency and refactored provider service to use goth-based adapter pattern. NewProviderService constructs a single adapter that dynamically validates providers at runtime, marshals OAuth sessions from GetAuthCodeURL, and unmarshals stored sessions in HandleCallback for state reconstruction.
OAuth Service Integration and Wiring
internal/domain/service/oauth_service.go, internal/provider/service_provider.go
OAuthService refactored to use new single ProviderService instead of provider map and removed HTTP client injection. OAuth flow updated to persist session strings via enriched StateStore contract, enabling stateless session reconstruction across callback handling and provider trust validation.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant OAuthService
  participant ProviderService
  participant StateStore
  participant GothProvider
  
  Client->>OAuthService: GetOAuthURL(provider, state)
  OAuthService->>ProviderService: GetAuthCodeURL(provider, state)
  ProviderService->>GothProvider: BeginAuth(state)
  GothProvider-->>ProviderService: AuthURL, Session
  ProviderService->>StateStore: Marshal session
  ProviderService-->>OAuthService: AuthURL, SessionString
  OAuthService->>StateStore: Store(state, sessionString, expiry)
  OAuthService-->>Client: redirectURL
  
  Client->>OAuthService: HandleOAuthCallback(provider, code, state)
  OAuthService->>StateStore: VerifyAndDelete(state)
  StateStore-->>OAuthService: sessionString, err
  OAuthService->>ProviderService: HandleCallback(provider, code, sessionString)
  ProviderService->>StateStore: Unmarshal sessionString
  ProviderService->>GothProvider: Authorize(code)
  GothProvider->>GothProvider: FetchUser()
  GothProvider-->>ProviderService: UserInfo
  ProviderService-->>OAuthService: UserInfo
  OAuthService-->>Client: UserInfo
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • itsLeonB/cashback#51: Introduced the NATS KV–backed state store adapter with the same method surface; this PR extends that contract to persist and return values alongside state keys for OAuth session reconstruction.

Poem

🐰 Goth comes to town with session flair,
State now holds values everywhere,
No HTTP client to carry around,
OAuth flows stateless, safe and sound!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'feat: replace custom OAuth with goth provider adapter' clearly and concisely summarizes the main objective of the changeset: replacing a custom OAuth implementation with a goth-based adapter.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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.

Caution

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

⚠️ Outside diff range comments (1)
internal/adapters/core/service/store/nats_kv_state_store_test.go (1)

50-103: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Test file should use testify/assert for assertions.

Per coding guidelines, test files must use testify/assert for assertions. The current tests use manual t.Fatal and t.Fatalf calls instead.

Example refactor for one test:

+import (
+	"github.com/stretchr/testify/assert"
+)

 func TestNATSKVStateStore_Store(t *testing.T) {
 	kv := newMockKV()
 	s := NewNATSKVStateStore(kv)

 	err := s.Store(context.Background(), "abc123", "session-data", 5*time.Minute)
-	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
-	}
+	assert.NoError(t, err)

-	if _, ok := kv.entries["state.abc123"]; !ok {
-		t.Fatal("expected key to be stored")
-	}
+	assert.Contains(t, kv.entries, "state.abc123")
 }
🤖 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/adapters/core/service/store/nats_kv_state_store_test.go` around
lines 50 - 103, Replace manual t.Fatal/t.Fatalf assertions in
TestNATSKVStateStore_Store, TestNATSKVStateStore_Store_Duplicate,
TestNATSKVStateStore_VerifyAndDelete and
TestNATSKVStateStore_VerifyAndDelete_NotFound with testify/assert helpers: add
import "github.com/stretchr/testify/assert", use assert.NoError(t, err) after
Store/VerifyAndDelete calls, use assert.Contains(t, kv.entries, "state.abc123")
to check stored key and assert.NotContains(t, kv.entries, "state.abc123") to
check deletion, use assert.Equal(t, "session-data", value) for value equality,
and use assert.Error(t, err) when expecting failures (duplicate or not found);
reference the Store and VerifyAndDelete methods and the mockKV entries map when
making replacements.

Source: Coding guidelines

🧹 Nitpick comments (3)
internal/adapters/core/service/store/nats_kv_state_store.go (1)

47-48: 💤 Low value

Consider logging infrastructure errors before masking them.

When Delete fails due to infrastructure issues (not revision mismatch), the error is silently converted to a generic BadRequestError("invalid state"). While this is secure (no information leakage), it may hinder debugging transient NATS failures.

Consider logging the underlying error at debug/warn level before returning the masked error, or distinguishing revision conflicts from other failures if jetstream provides that granularity.

🤖 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/adapters/core/service/store/nats_kv_state_store.go` around lines 47
- 48, The Delete call in s.kv.Delete(ctx, key,
jetstream.LastRevision(entry.Revision())) currently masks all errors by
returning ungerr.BadRequestError("invalid state"); update the error handling in
the method containing that call to log the underlying error (use process or
package logger at debug/warn) before returning the generic BadRequestError, and
if jetstream exposes a specific revision-conflict error type/value, branch to
return the BadRequestError only for revision conflicts and log/return a
different wrapper for infra errors; reference s.kv.Delete,
jetstream.LastRevision(entry.Revision()) and the current return of
ungerr.BadRequestError("invalid state") to locate where to add the logging and
optional error-type check.
internal/domain/service/oauth/provider_service.go (1)

19-37: 💤 Low value

Naming convention note: adapter vs service implementation.

The struct gothProviderAdapter doesn't follow the <name>ServiceImpl naming convention specified in coding guidelines for internal/domain/service/**/*.go. However, this is intentionally an adapter pattern wrapping goth.Provider, which is semantically different from a service implementation.

If strict adherence is required, consider renaming to providerServiceImpl. Otherwise, the current name clearly conveys intent.

🤖 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/domain/service/oauth/provider_service.go` around lines 19 - 37, The
type name gothProviderAdapter conflicts with the repository's <name>ServiceImpl
convention; rename gothProviderAdapter to providerServiceImpl and
gothProviderEntry to providerEntry, update NewProviderService to return
&providerServiceImpl{...} and replace all references/usages of
gothProviderAdapter and gothProviderEntry in this package (including any tests)
to the new names so the adapter keeps its behavior but follows the ServiceImpl
naming convention; ensure constructor signature (NewProviderService) and the map
key ("google") initialization remain unchanged.

Source: Coding guidelines

internal/domain/service/oauth_service.go (1)

36-42: ⚖️ Poor tradeoff

Consider injecting ProviderService for better testability.

The providerSvc is constructed directly from config.Global.OAuthProviders inside the constructor (line 37), making it difficult to mock in unit tests. Consider accepting oauth.ProviderService as a parameter:

 func NewOAuthService(
 	transactor crud.Transactor,
+	providerSvc oauth.ProviderService,
 	oauthAccountRepo crud.Repository[users.OAuthAccount],
 	stateStore store.StateStore,
 	userSvc UserService,
 	sessionSvc SessionService,
 ) OAuthService {
 	return &oauthServiceImpl{
 		transactor:       transactor,
-		providerSvc:      oauth.NewProviderService(config.Global.OAuthProviders),
+		providerSvc:      providerSvc,
 		oauthAccountRepo: oauthAccountRepo,
 		// ...
 	}
 }

This would require updating service_provider.go to construct and inject the provider service.

🤖 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/domain/service/oauth_service.go` around lines 36 - 42, Change the
oauth service constructor to accept a oauth.ProviderService parameter instead of
calling oauth.NewProviderService inside the constructor: replace the direct
construction (NewProviderService(config.Global.OAuthProviders)) with a
providerSvc parameter and assign it to the providerSvc field in the struct; then
update service_provider.go so it constructs the ProviderService via
oauth.NewProviderService(config.Global.OAuthProviders) and passes that instance
into the oauth service factory when wiring dependencies. Also update any call
sites/tests to pass a mock or real oauth.ProviderService accordingly.
🤖 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.

Outside diff comments:
In `@internal/adapters/core/service/store/nats_kv_state_store_test.go`:
- Around line 50-103: Replace manual t.Fatal/t.Fatalf assertions in
TestNATSKVStateStore_Store, TestNATSKVStateStore_Store_Duplicate,
TestNATSKVStateStore_VerifyAndDelete and
TestNATSKVStateStore_VerifyAndDelete_NotFound with testify/assert helpers: add
import "github.com/stretchr/testify/assert", use assert.NoError(t, err) after
Store/VerifyAndDelete calls, use assert.Contains(t, kv.entries, "state.abc123")
to check stored key and assert.NotContains(t, kv.entries, "state.abc123") to
check deletion, use assert.Equal(t, "session-data", value) for value equality,
and use assert.Error(t, err) when expecting failures (duplicate or not found);
reference the Store and VerifyAndDelete methods and the mockKV entries map when
making replacements.

---

Nitpick comments:
In `@internal/adapters/core/service/store/nats_kv_state_store.go`:
- Around line 47-48: The Delete call in s.kv.Delete(ctx, key,
jetstream.LastRevision(entry.Revision())) currently masks all errors by
returning ungerr.BadRequestError("invalid state"); update the error handling in
the method containing that call to log the underlying error (use process or
package logger at debug/warn) before returning the generic BadRequestError, and
if jetstream exposes a specific revision-conflict error type/value, branch to
return the BadRequestError only for revision conflicts and log/return a
different wrapper for infra errors; reference s.kv.Delete,
jetstream.LastRevision(entry.Revision()) and the current return of
ungerr.BadRequestError("invalid state") to locate where to add the logging and
optional error-type check.

In `@internal/domain/service/oauth_service.go`:
- Around line 36-42: Change the oauth service constructor to accept a
oauth.ProviderService parameter instead of calling oauth.NewProviderService
inside the constructor: replace the direct construction
(NewProviderService(config.Global.OAuthProviders)) with a providerSvc parameter
and assign it to the providerSvc field in the struct; then update
service_provider.go so it constructs the ProviderService via
oauth.NewProviderService(config.Global.OAuthProviders) and passes that instance
into the oauth service factory when wiring dependencies. Also update any call
sites/tests to pass a mock or real oauth.ProviderService accordingly.

In `@internal/domain/service/oauth/provider_service.go`:
- Around line 19-37: The type name gothProviderAdapter conflicts with the
repository's <name>ServiceImpl convention; rename gothProviderAdapter to
providerServiceImpl and gothProviderEntry to providerEntry, update
NewProviderService to return &providerServiceImpl{...} and replace all
references/usages of gothProviderAdapter and gothProviderEntry in this package
(including any tests) to the new names so the adapter keeps its behavior but
follows the ServiceImpl naming convention; ensure constructor signature
(NewProviderService) and the map key ("google") initialization remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 37aa19bd-c36d-4e42-bddd-bb3e0931402e

📥 Commits

Reviewing files that changed from the base of the PR and between 5617e13 and 5962f4b.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • go.mod
  • internal/adapters/core/service/store/in_memory_state_store.go
  • internal/adapters/core/service/store/nats_kv_state_store.go
  • internal/adapters/core/service/store/nats_kv_state_store_test.go
  • internal/core/service/store/state_store.go
  • internal/domain/service/oauth/google_provider_service.go
  • internal/domain/service/oauth/provider_service.go
  • internal/domain/service/oauth_service.go
  • internal/provider/service_provider.go
💤 Files with no reviewable changes (1)
  • internal/domain/service/oauth/google_provider_service.go

- Add github.com/markbates/goth dependency
- Implement gothProviderAdapter wrapping goth.Provider
- Update StateStore interface to store/return session value
- Update ProviderService to accept provider name parameter
- Remove custom google_provider_service.go
- Remove http.Client dependency from OAuthService
@railway-app railway-app Bot temporarily deployed to cashus-backend / development June 12, 2026 11:57 Inactive
@itsLeonB itsLeonB merged commit baa4d50 into main Jun 12, 2026
8 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.

1 participant