From ea45157d71427a6b3cb9a388b598118ba1f8b7b7 Mon Sep 17 00:00:00 2001 From: itsLeonB Date: Fri, 12 Jun 2026 18:21:51 +0700 Subject: [PATCH] feat: replace custom OAuth with goth provider adapter - 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 --- go.mod | 1 + go.sum | 2 + .../service/store/in_memory_state_store.go | 18 +-- .../core/service/store/nats_kv_state_store.go | 16 +-- .../service/store/nats_kv_state_store_test.go | 49 ++++---- internal/core/service/store/state_store.go | 4 +- .../service/oauth/google_provider_service.go | 107 ------------------ .../domain/service/oauth/provider_service.go | 100 ++++++++++++++-- internal/domain/service/oauth_service.go | 45 +++----- internal/provider/service_provider.go | 5 +- 10 files changed, 154 insertions(+), 193 deletions(-) delete mode 100644 internal/domain/service/oauth/google_provider_service.go diff --git a/go.mod b/go.mod index d253e9d..77d3e01 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/kroma-labs/sentinel-go v0.3.4 + github.com/markbates/goth v1.82.0 github.com/midtrans/midtrans-go v1.3.8 github.com/nats-io/nats.go v1.52.0 github.com/openai/openai-go/v2 v2.7.1 diff --git a/go.sum b/go.sum index 6fa6be5..61f3375 100644 --- a/go.sum +++ b/go.sum @@ -368,6 +368,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ= +github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= diff --git a/internal/adapters/core/service/store/in_memory_state_store.go b/internal/adapters/core/service/store/in_memory_state_store.go index c6ba72b..6def705 100644 --- a/internal/adapters/core/service/store/in_memory_state_store.go +++ b/internal/adapters/core/service/store/in_memory_state_store.go @@ -29,36 +29,36 @@ func NewInMemoryStateStore() *inMemoryStateStore { return store } -func (vss *inMemoryStateStore) Store(ctx context.Context, state string, expiry time.Duration) error { +func (vss *inMemoryStateStore) Store(ctx context.Context, state string, value string, expiry time.Duration) error { key := vss.constructKey(state) entry := stateEntry{ - value: state, + value: value, expiresAt: time.Now().Add(expiry), } vss.data.Store(key, entry) return nil } -func (vss *inMemoryStateStore) VerifyAndDelete(ctx context.Context, state string) error { +func (vss *inMemoryStateStore) VerifyAndDelete(ctx context.Context, state string) (string, error) { key := vss.constructKey(state) - value, loaded := vss.data.LoadAndDelete(key) + raw, loaded := vss.data.LoadAndDelete(key) if !loaded { - return ungerr.BadRequestError("invalid state") + return "", ungerr.BadRequestError("invalid state") } - entry := value.(stateEntry) + entry := raw.(stateEntry) if time.Now().After(entry.expiresAt) { - return ungerr.BadRequestError("invalid state") + return "", ungerr.BadRequestError("invalid state") } - return nil + return entry.value, nil } func (vss *inMemoryStateStore) startCleanup() { vss.wg.Add(1) go func() { defer vss.wg.Done() - ticker := time.NewTicker(1 * time.Minute) // Adjust interval as needed + ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() for { diff --git a/internal/adapters/core/service/store/nats_kv_state_store.go b/internal/adapters/core/service/store/nats_kv_state_store.go index 1fb8666..86f7f62 100644 --- a/internal/adapters/core/service/store/nats_kv_state_store.go +++ b/internal/adapters/core/service/store/nats_kv_state_store.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/itsLeonB/cashback/internal/core/logger" "github.com/itsLeonB/cashback/internal/core/otel" "github.com/itsLeonB/ungerr" "github.com/nats-io/nats.go/jetstream" @@ -19,11 +20,11 @@ func NewNATSKVStateStore(kv jetstream.KeyValue) *natsKVStateStore { return &natsKVStateStore{kv: kv} } -func (s *natsKVStateStore) Store(ctx context.Context, state string, expiry time.Duration) error { +func (s *natsKVStateStore) Store(ctx context.Context, state string, value string, expiry time.Duration) error { ctx, span := otel.Tracer.Start(ctx, "natsKVStateStore.Store") defer span.End() - _, err := s.kv.Create(ctx, s.constructKey(state), []byte(state), jetstream.KeyTTL(expiry)) + _, err := s.kv.Create(ctx, s.constructKey(state), []byte(value), jetstream.KeyTTL(expiry)) if err != nil { return ungerr.Wrap(err, "error storing state in NATS KV") } @@ -31,7 +32,7 @@ func (s *natsKVStateStore) Store(ctx context.Context, state string, expiry time. return nil } -func (s *natsKVStateStore) VerifyAndDelete(ctx context.Context, state string) error { +func (s *natsKVStateStore) VerifyAndDelete(ctx context.Context, state string) (string, error) { ctx, span := otel.Tracer.Start(ctx, "natsKVStateStore.VerifyAndDelete") defer span.End() @@ -39,16 +40,17 @@ func (s *natsKVStateStore) VerifyAndDelete(ctx context.Context, state string) er entry, err := s.kv.Get(ctx, key) if err != nil { if errors.Is(err, jetstream.ErrKeyNotFound) { - return ungerr.BadRequestError("invalid state") + return "", ungerr.BadRequestError("invalid state") } - return ungerr.Wrap(err, "error verifying state in NATS KV") + return "", ungerr.Wrap(err, "error verifying state in NATS KV") } if err := s.kv.Delete(ctx, key, jetstream.LastRevision(entry.Revision())); err != nil { - return ungerr.BadRequestError("invalid state") + logger.Warnf("error deleting state from NATS KV: %v", err) + return "", ungerr.BadRequestError("invalid state") } - return nil + return string(entry.Value()), nil } func (s *natsKVStateStore) Shutdown() error { diff --git a/internal/adapters/core/service/store/nats_kv_state_store_test.go b/internal/adapters/core/service/store/nats_kv_state_store_test.go index 2c84b2f..385b3db 100644 --- a/internal/adapters/core/service/store/nats_kv_state_store_test.go +++ b/internal/adapters/core/service/store/nats_kv_state_store_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/nats-io/nats.go/jetstream" + "github.com/stretchr/testify/assert" ) type mockKV struct { @@ -26,10 +27,11 @@ func (m *mockKV) Create(ctx context.Context, key string, value []byte, opts ...j } func (m *mockKV) Get(ctx context.Context, key string) (jetstream.KeyValueEntry, error) { - if _, ok := m.entries[key]; !ok { + v, ok := m.entries[key] + if !ok { return nil, jetstream.ErrKeyNotFound } - return &mockEntry{revision: 1}, nil + return &mockEntry{revision: 1, value: v}, nil } func (m *mockKV) Delete(ctx context.Context, key string, opts ...jetstream.KVDeleteOpt) error { @@ -40,66 +42,55 @@ func (m *mockKV) Delete(ctx context.Context, key string, opts ...jetstream.KVDel type mockEntry struct { jetstream.KeyValueEntry revision uint64 + value []byte } func (e *mockEntry) Revision() uint64 { return e.revision } +func (e *mockEntry) Value() []byte { return e.value } func TestNATSKVStateStore_Store(t *testing.T) { kv := newMockKV() s := NewNATSKVStateStore(kv) - err := s.Store(context.Background(), "abc123", 5*time.Minute) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + err := s.Store(context.Background(), "abc123", "session-data", 5*time.Minute) + 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") } func TestNATSKVStateStore_Store_Duplicate(t *testing.T) { kv := newMockKV() s := NewNATSKVStateStore(kv) - _ = s.Store(context.Background(), "abc123", 5*time.Minute) - err := s.Store(context.Background(), "abc123", 5*time.Minute) - if err == nil { - t.Fatal("expected error on duplicate store") - } + _ = s.Store(context.Background(), "abc123", "session-data", 5*time.Minute) + err := s.Store(context.Background(), "abc123", "session-data", 5*time.Minute) + assert.Error(t, err) } func TestNATSKVStateStore_VerifyAndDelete(t *testing.T) { kv := newMockKV() s := NewNATSKVStateStore(kv) - _ = s.Store(context.Background(), "abc123", 5*time.Minute) + _ = s.Store(context.Background(), "abc123", "session-data", 5*time.Minute) - err := s.VerifyAndDelete(context.Background(), "abc123") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + value, err := s.VerifyAndDelete(context.Background(), "abc123") + assert.NoError(t, err) + assert.Equal(t, "session-data", value) - if _, ok := kv.entries["state.abc123"]; ok { - t.Fatal("expected key to be deleted") - } + assert.NotContains(t, kv.entries, "state.abc123") } func TestNATSKVStateStore_VerifyAndDelete_NotFound(t *testing.T) { kv := newMockKV() s := NewNATSKVStateStore(kv) - err := s.VerifyAndDelete(context.Background(), "nonexistent") - if err == nil { - t.Fatal("expected error for nonexistent state") - } + _, err := s.VerifyAndDelete(context.Background(), "nonexistent") + assert.Error(t, err) } func TestNATSKVStateStore_Shutdown(t *testing.T) { kv := newMockKV() s := NewNATSKVStateStore(kv) - if err := s.Shutdown(); err != nil { - t.Fatalf("unexpected error: %v", err) - } + assert.NoError(t, s.Shutdown()) } diff --git a/internal/core/service/store/state_store.go b/internal/core/service/store/state_store.go index 675bebe..15954e0 100644 --- a/internal/core/service/store/state_store.go +++ b/internal/core/service/store/state_store.go @@ -11,8 +11,8 @@ import ( ) type StateStore interface { - Store(ctx context.Context, state string, expiry time.Duration) error - VerifyAndDelete(ctx context.Context, state string) error + Store(ctx context.Context, state string, value string, expiry time.Duration) error + VerifyAndDelete(ctx context.Context, state string) (string, error) Shutdown() error } diff --git a/internal/domain/service/oauth/google_provider_service.go b/internal/domain/service/oauth/google_provider_service.go deleted file mode 100644 index 9f67dfd..0000000 --- a/internal/domain/service/oauth/google_provider_service.go +++ /dev/null @@ -1,107 +0,0 @@ -package oauth - -import ( - "context" - "io" - "net/http" - - "github.com/itsLeonB/cashback/internal/core/config" - "github.com/itsLeonB/cashback/internal/core/logger" - "github.com/itsLeonB/cashback/internal/core/otel" - "github.com/itsLeonB/ezutil/v2" - "github.com/itsLeonB/ungerr" - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" -) - -type googleUserInfo struct { - ID string `json:"id"` - Email string `json:"email"` - Name string `json:"name"` - Picture string `json:"picture"` -} - -type googleProviderService struct { - userInfoURL string - cfg *oauth2.Config - httpClient *http.Client -} - -func newGoogleProviderService( - oauthConfig config.OAuthProvider, - httpClient *http.Client, -) ProviderService { - return &googleProviderService{ - userInfoURL: "https://www.googleapis.com/oauth2/v2/userinfo", - cfg: &oauth2.Config{ - ClientID: oauthConfig.ClientID, - ClientSecret: oauthConfig.ClientSecret, - RedirectURL: oauthConfig.RedirectUrl, - Endpoint: google.Endpoint, - Scopes: []string{ - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/userinfo.profile", - }, - }, - httpClient: httpClient, - } -} - -func (*googleProviderService) IsTrusted() bool { - return true -} - -func (gps *googleProviderService) GetAuthCodeURL(state string) (string, error) { - url := gps.cfg.AuthCodeURL(state, oauth2.AccessTypeOffline) - if url == "" { - return "", ungerr.Unknownf("OAuth2 google provider returns empty string for auth code URL") - } - return url, nil -} - -func (gps *googleProviderService) HandleCallback(ctx context.Context, code string) (UserInfo, error) { - ctx, span := otel.Tracer.Start(ctx, "googleProviderService.HandleCallback") - defer span.End() - - token, err := gps.cfg.Exchange(ctx, code) - if err != nil { - return UserInfo{}, ungerr.Wrap(err, "error exchange OAuth2 token at callback") - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, gps.userInfoURL, nil) - if err != nil { - return UserInfo{}, ungerr.Wrap(err, "error creating new HTTP request") - } - req.Header.Set("Authorization", "Bearer "+token.AccessToken) - - resp, err := gps.httpClient.Do(req) - if err != nil { - return UserInfo{}, ungerr.Wrap(err, "error making HTTP request") - } - defer func() { - if err := resp.Body.Close(); err != nil { - logger.Error(ungerr.Wrap(err, "error closing HTTP response body")) - } - }() - body, err := io.ReadAll(resp.Body) - if err != nil { - return UserInfo{}, ungerr.Wrap(err, "error reading response body") - } - if resp.StatusCode != http.StatusOK { - return UserInfo{}, ungerr.Unknownf("error getting user info: %s", string(body)) - } - - userInfo, err := ezutil.Unmarshal[googleUserInfo](body) - if err != nil { - return UserInfo{}, err - } - - return UserInfo{ - Provider: "google", - ProviderID: userInfo.ID, - Email: userInfo.Email, - Name: userInfo.Name, - Avatar: userInfo.Picture, - AccessToken: token.AccessToken, - }, nil -} diff --git a/internal/domain/service/oauth/provider_service.go b/internal/domain/service/oauth/provider_service.go index 638b266..d493419 100644 --- a/internal/domain/service/oauth/provider_service.go +++ b/internal/domain/service/oauth/provider_service.go @@ -2,22 +2,102 @@ package oauth import ( "context" - "net/http" + "net/url" "github.com/itsLeonB/cashback/internal/core/config" + "github.com/itsLeonB/ungerr" + "github.com/markbates/goth" + "github.com/markbates/goth/providers/google" ) type ProviderService interface { - IsTrusted() bool - GetAuthCodeURL(state string) (string, error) - HandleCallback(ctx context.Context, code string) (UserInfo, error) + IsTrusted(provider string) (bool, error) + GetAuthCodeURL(provider string, state string) (url string, session string, err error) + HandleCallback(ctx context.Context, provider string, code string, session string) (UserInfo, error) } -func NewOAuthProviderServices( - cfgs config.OAuthProviders, - httpClient *http.Client, -) map[string]ProviderService { - return map[string]ProviderService{ - "google": newGoogleProviderService(cfgs.Google, httpClient), +type providerServiceImpl struct { + providers map[string]providerEntry +} + +type providerEntry struct { + provider goth.Provider + trusted bool +} + +func NewProviderService(cfgs config.OAuthProviders) ProviderService { + return &providerServiceImpl{ + providers: map[string]providerEntry{ + "google": { + provider: google.New(cfgs.Google.ClientID, cfgs.Google.ClientSecret, cfgs.Google.RedirectUrl, "email", "profile"), + trusted: true, + }, + }, + } +} + +func (a *providerServiceImpl) get(provider string) (providerEntry, error) { + entry, ok := a.providers[provider] + if !ok { + return providerEntry{}, ungerr.BadRequestError("unsupported oauth provider: " + provider) + } + return entry, nil +} + +func (a *providerServiceImpl) IsTrusted(provider string) (bool, error) { + entry, err := a.get(provider) + if err != nil { + return false, err + } + return entry.trusted, nil +} + +func (a *providerServiceImpl) GetAuthCodeURL(provider string, state string) (string, string, error) { + entry, err := a.get(provider) + if err != nil { + return "", "", err } + + session, err := entry.provider.BeginAuth(state) + if err != nil { + return "", "", ungerr.Wrap(err, "error beginning oauth auth") + } + authURL, err := session.GetAuthURL() + if err != nil { + return "", "", ungerr.Wrap(err, "error getting oauth auth URL") + } + return authURL, session.Marshal(), nil +} + +// TODO: goth's Authorize and FetchUser don't accept context, so these HTTP calls +// won't respect request cancellation or deadlines. This is a goth limitation. +func (a *providerServiceImpl) HandleCallback(ctx context.Context, provider string, code string, sessionStr string) (UserInfo, error) { + entry, err := a.get(provider) + if err != nil { + return UserInfo{}, err + } + + session, err := entry.provider.UnmarshalSession(sessionStr) + if err != nil { + return UserInfo{}, ungerr.Wrap(err, "error unmarshalling oauth session") + } + + _, err = session.Authorize(entry.provider, url.Values{"code": {code}}) + if err != nil { + return UserInfo{}, ungerr.Wrap(err, "error authorizing oauth session") + } + + user, err := entry.provider.FetchUser(session) + if err != nil { + return UserInfo{}, ungerr.Wrap(err, "error fetching oauth user") + } + + return UserInfo{ + Provider: user.Provider, + ProviderID: user.UserID, + Email: user.Email, + Name: user.Name, + Avatar: user.AvatarURL, + AccessToken: user.AccessToken, + }, nil } diff --git a/internal/domain/service/oauth_service.go b/internal/domain/service/oauth_service.go index e7b2c19..d57b9ed 100644 --- a/internal/domain/service/oauth_service.go +++ b/internal/domain/service/oauth_service.go @@ -4,10 +4,8 @@ import ( "context" "crypto/rand" "encoding/base64" - "net/http" "time" - "github.com/itsLeonB/cashback/internal/core/config" "github.com/itsLeonB/cashback/internal/core/otel" "github.com/itsLeonB/cashback/internal/core/service/store" "github.com/itsLeonB/cashback/internal/domain/dto" @@ -19,7 +17,7 @@ import ( type oauthServiceImpl struct { transactor crud.Transactor - oauthProviders map[string]oauth.ProviderService + providerSvc oauth.ProviderService oauthAccountRepo crud.Repository[users.OAuthAccount] stateStore store.StateStore userSvc UserService @@ -28,19 +26,19 @@ type oauthServiceImpl struct { func NewOAuthService( transactor crud.Transactor, + providerSvc oauth.ProviderService, oauthAccountRepo crud.Repository[users.OAuthAccount], stateStore store.StateStore, userSvc UserService, - httpClient *http.Client, sessionSvc SessionService, ) OAuthService { return &oauthServiceImpl{ - transactor, - oauth.NewOAuthProviderServices(config.Global.OAuthProviders, httpClient), - oauthAccountRepo, - stateStore, - userSvc, - sessionSvc, + transactor: transactor, + providerSvc: providerSvc, + oauthAccountRepo: oauthAccountRepo, + stateStore: stateStore, + userSvc: userSvc, + sessionSvc: sessionSvc, } } @@ -48,22 +46,17 @@ func (as *oauthServiceImpl) GetOAuthURL(ctx context.Context, provider string) (s ctx, span := otel.Tracer.Start(ctx, "OAuthService.GetOAuthURL") defer span.End() - oauthProvider, ok := as.oauthProviders[provider] - if !ok { - return "", ungerr.Unknownf("unsupported oauth provider: %s", provider) - } - state, err := as.generateState() if err != nil { return "", err } - url, err := oauthProvider.GetAuthCodeURL(state) + url, sessionStr, err := as.providerSvc.GetAuthCodeURL(provider, state) if err != nil { return "", err } - if err = as.stateStore.Store(ctx, state, 5*time.Minute); err != nil { + if err = as.stateStore.Store(ctx, state, sessionStr, 5*time.Minute); err != nil { return "", err } @@ -76,16 +69,12 @@ func (as *oauthServiceImpl) HandleOAuthCallback(ctx context.Context, data dto.OA var response dto.TokenResponse err := as.transactor.WithinTransaction(ctx, func(ctx context.Context) error { - oauthProvider, ok := as.oauthProviders[data.Provider] - if !ok { - return ungerr.Unknownf("unsupported oauth provider: %s", data.Provider) - } - - if err := as.stateStore.VerifyAndDelete(ctx, data.State); err != nil { + sessionStr, err := as.stateStore.VerifyAndDelete(ctx, data.State) + if err != nil { return err } - userInfo, err := oauthProvider.HandleCallback(ctx, data.Code) + userInfo, err := as.providerSvc.HandleCallback(ctx, data.Provider, data.Code, sessionStr) if err != nil { return err } @@ -125,7 +114,6 @@ func (as *oauthServiceImpl) createNewUserOAuth(ctx context.Context, userInfo oau return users.User{}, err } if user.IsZero() { - // New user newUser := dto.NewUserRequest{ Email: userInfo.Email, Name: userInfo.Name, @@ -138,11 +126,14 @@ func (as *oauthServiceImpl) createNewUserOAuth(ctx context.Context, userInfo oau } } - if !as.oauthProviders[userInfo.Provider].IsTrusted() { + trusted, err := as.providerSvc.IsTrusted(userInfo.Provider) + if err != nil { + return users.User{}, err + } + if !trusted { return users.User{}, ungerr.Unknown("provider temporarily disabled") } - // New oauth method newOAuthAccount := users.OAuthAccount{ UserID: user.ID, Provider: userInfo.Provider, diff --git a/internal/provider/service_provider.go b/internal/provider/service_provider.go index bbb2159..4a30e56 100644 --- a/internal/provider/service_provider.go +++ b/internal/provider/service_provider.go @@ -2,7 +2,6 @@ package provider import ( "errors" - "net/http" "github.com/google/uuid" appembed "github.com/itsLeonB/cashback" @@ -13,6 +12,7 @@ import ( "github.com/itsLeonB/cashback/internal/domain/service/fee" "github.com/itsLeonB/cashback/internal/domain/service/monetization" "github.com/itsLeonB/cashback/internal/domain/service/monetization/payment" + "github.com/itsLeonB/cashback/internal/domain/service/oauth" "github.com/itsLeonB/sekure" ) @@ -89,10 +89,11 @@ func ProvideServices( pushNotification := service.NewPushNotificationService(repos.PushSubscription, repos.Notification, repos.Transactor, coreSvc.WebPush) sessionCache := cache.NewInMemoryCache[uuid.UUID](authConfig.TokenDuration) + providerSvc := oauth.NewProviderService(config.Global.OAuthProviders) return &Services{ Auth: service.NewAuthService(jwt, repos.Transactor, user, coreSvc.Mail, appConfig.RegisterVerificationUrl, appConfig.ResetPasswordUrl, authConfig.HashCost, pushNotification, session, profile, friendship, sessionCache), - OAuth: service.NewOAuthService(repos.Transactor, repos.OAuthAccount, coreSvc.State, user, http.DefaultClient, session), + OAuth: service.NewOAuthService(repos.Transactor, providerSvc, repos.OAuthAccount, coreSvc.State, user, session), Session: session, Captcha: service.NewTurnstileService(authConfig.TurnstileSecretKey),