Skip to content

Commit 3de39c5

Browse files
authored
Merge pull request #997 from meridianhub/stripe-identity-kyc--4--config-factory-wiring
feat: wire Stripe Identity provider into config, factory, and HTTP routes
2 parents 3f9b34a + 14ac18a commit 3de39c5

6 files changed

Lines changed: 367 additions & 13 deletions

File tree

services/party/cmd/main.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ func run(logger *slog.Logger) error {
223223
VerificationService: verificationSvc,
224224
HMACSecrets: map[string][]byte{
225225
"default": []byte(verificationCfg.WebhookSecret),
226+
"stripe": []byte(verificationCfg.WebhookSecret),
226227
},
227228
Logger: logger,
228229
},
@@ -231,6 +232,25 @@ func run(logger *slog.Logger) error {
231232
return fmt.Errorf("failed to create webhook handler: %w", err)
232233
}
233234

235+
// Register provider-specific webhook routes before the generic catch-all.
236+
// For Stripe, we use the StripeWebhookAdapter which validates the Stripe-Signature
237+
// header (using the Stripe endpoint signing secret) and translates the Stripe event
238+
// format to our generic webhook format (signed with the generic HMAC secret).
239+
if strings.ToLower(verificationCfg.Provider) == "stripe" {
240+
stripeAdapter, err := httpAdapter.NewStripeWebhookAdapter(
241+
httpAdapter.StripeWebhookAdapterConfig{
242+
InnerHandler: webhookHandler,
243+
WebhookSecret: []byte(verificationCfg.StripeWebhookSecret),
244+
InnerHMACSecret: []byte(verificationCfg.WebhookSecret),
245+
Logger: logger,
246+
},
247+
)
248+
if err != nil {
249+
return bootstrap.Permanent(fmt.Errorf("failed to create stripe webhook adapter: %w", err))
250+
}
251+
httpMux.Handle("/webhooks/verification/stripe", stripeAdapter)
252+
}
253+
234254
httpMux.HandleFunc("/webhooks/verification/", webhookHandler.HandleWebhook)
235255
httpMux.HandleFunc("/health", newHTTPHealthHandler(verificationCfg))
236256

services/party/config/verification.go

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,39 @@ import (
1313
// - "mock": Mock provider for testing (always available)
1414
// - "jumio": Jumio identity verification
1515
// - "onfido": Onfido identity verification
16+
// - "stripe": Stripe Identity verification
1617
//
1718
// ProviderConfig contains provider-specific settings like API keys and endpoints.
1819
// Required keys vary by provider.
1920
//
20-
// WebhookSecret is the HMAC secret used to verify incoming webhook signatures.
21-
// This prevents malicious actors from spoofing verification callbacks.
21+
// WebhookSecret is the HMAC secret used to verify incoming webhook signatures
22+
// for the generic webhook handler and as the inner HMAC secret for the Stripe
23+
// adapter. This prevents malicious actors from spoofing verification callbacks.
24+
//
25+
// StripeWebhookSecret is the Stripe endpoint signing secret (whsec_ prefixed)
26+
// used exclusively to validate the Stripe-Signature header on inbound Stripe
27+
// webhooks. Required when provider is "stripe".
2228
//
2329
// WebhookURL is the publicly accessible URL where providers send verification
2430
// callbacks. Required for production deployments.
2531
type VerificationConfig struct {
26-
// Provider is the verification provider to use ("mock", "jumio", "onfido").
32+
// Provider is the verification provider to use ("mock", "jumio", "onfido", "stripe").
2733
Provider string
2834

2935
// ProviderConfig contains provider-specific configuration.
3036
// For jumio/onfido: "api_key", "api_secret", "base_url" (optional).
37+
// For stripe: "api_key", "base_url" (optional), "stripe_account" (optional).
3138
ProviderConfig map[string]string
3239

33-
// WebhookSecret is the HMAC secret for validating webhook signatures.
40+
// WebhookSecret is the HMAC secret for validating webhook signatures on the
41+
// generic handler and as the inner HMAC secret for the Stripe adapter.
3442
WebhookSecret string
3543

44+
// StripeWebhookSecret is the Stripe endpoint signing secret (whsec_ prefixed)
45+
// used to validate inbound Stripe webhook signatures.
46+
// Loaded from STRIPE_WEBHOOK_SECRET. Required when provider is "stripe".
47+
StripeWebhookSecret string
48+
3649
// WebhookURL is the public URL for provider callbacks (e.g., "https://api.example.com/webhooks/verification").
3750
WebhookURL string
3851
}
@@ -45,27 +58,32 @@ var (
4558
ErrEmptyWebhookURLForNonMock = errors.New("webhook URL is required for non-mock providers")
4659
ErrMissingProviderAPIKey = errors.New("api_key is required in provider config")
4760
ErrMissingProviderAPISecret = errors.New("api_secret is required in provider config")
61+
ErrMissingStripeWebhookSecret = errors.New("stripe_webhook_secret is required when provider is stripe (set STRIPE_WEBHOOK_SECRET)")
4862
ErrMockProviderInProduction = errors.New("mock provider not allowed in production")
4963
ErrWebhookHTTPSRequired = errors.New("webhook URL must use HTTPS in production")
5064
ErrWebhookSecretTooShort = errors.New("webhook secret must be at least 32 characters in production")
5165
)
5266

5367
// SupportedProviders lists all supported verification provider names.
54-
var SupportedProviders = []string{"mock", "jumio", "onfido"}
68+
var SupportedProviders = []string{"mock", "jumio", "onfido", "stripe"}
5569

5670
// LoadVerificationConfig loads verification configuration from environment variables.
5771
//
5872
// Required environment variables:
5973
// - VERIFICATION_PROVIDER: The provider to use (required)
6074
//
6175
// Conditional environment variables (required for non-mock providers):
62-
// - VERIFICATION_WEBHOOK_SECRET: HMAC secret for webhook validation
76+
// - VERIFICATION_WEBHOOK_SECRET: HMAC secret for generic webhook validation
6377
// - VERIFICATION_WEBHOOK_URL: Public webhook callback URL
6478
//
6579
// Provider-specific environment variables:
6680
// - VERIFICATION_API_KEY: Provider API key
67-
// - VERIFICATION_API_SECRET: Provider API secret
81+
// - VERIFICATION_API_SECRET: Provider API secret (not required for Stripe)
6882
// - VERIFICATION_BASE_URL: Provider base URL (optional, provider default used if not set)
83+
//
84+
// Stripe-specific environment variables:
85+
// - STRIPE_WEBHOOK_SECRET: Stripe endpoint signing secret (whsec_ prefixed).
86+
// Required when VERIFICATION_PROVIDER=stripe.
6987
func LoadVerificationConfig() (*VerificationConfig, error) {
7088
providerConfig := make(map[string]string)
7189

@@ -81,10 +99,11 @@ func LoadVerificationConfig() (*VerificationConfig, error) {
8199
}
82100

83101
cfg := &VerificationConfig{
84-
Provider: strings.TrimSpace(os.Getenv("VERIFICATION_PROVIDER")),
85-
ProviderConfig: providerConfig,
86-
WebhookSecret: strings.TrimSpace(os.Getenv("VERIFICATION_WEBHOOK_SECRET")),
87-
WebhookURL: strings.TrimSpace(os.Getenv("VERIFICATION_WEBHOOK_URL")),
102+
Provider: strings.TrimSpace(os.Getenv("VERIFICATION_PROVIDER")),
103+
ProviderConfig: providerConfig,
104+
WebhookSecret: strings.TrimSpace(os.Getenv("VERIFICATION_WEBHOOK_SECRET")),
105+
StripeWebhookSecret: strings.TrimSpace(os.Getenv("STRIPE_WEBHOOK_SECRET")),
106+
WebhookURL: strings.TrimSpace(os.Getenv("VERIFICATION_WEBHOOK_URL")),
88107
}
89108

90109
if err := cfg.Validate(); err != nil {
@@ -122,10 +141,16 @@ func (c *VerificationConfig) Validate() error {
122141
if c.ProviderConfig["api_key"] == "" {
123142
return ErrMissingProviderAPIKey
124143
}
125-
if c.ProviderConfig["api_secret"] == "" {
144+
// Stripe only needs api_key; other providers require api_secret as well
145+
if strings.ToLower(c.Provider) != "stripe" && c.ProviderConfig["api_secret"] == "" {
126146
return ErrMissingProviderAPISecret
127147
}
128148

149+
// Stripe requires its own endpoint signing secret (distinct from the generic HMAC secret)
150+
if strings.ToLower(c.Provider) == "stripe" && c.StripeWebhookSecret == "" {
151+
return ErrMissingStripeWebhookSecret
152+
}
153+
129154
return nil
130155
}
131156

services/party/config/verification_test.go

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func clearVerificationEnv(t *testing.T) {
1717
"VERIFICATION_API_KEY",
1818
"VERIFICATION_API_SECRET",
1919
"VERIFICATION_BASE_URL",
20+
"STRIPE_WEBHOOK_SECRET",
2021
}
2122
for _, key := range envVars {
2223
_ = os.Unsetenv(key)
@@ -261,7 +262,82 @@ func TestVerificationConfig_SupportedProviders(t *testing.T) {
261262
assert.Contains(t, SupportedProviders, "mock")
262263
assert.Contains(t, SupportedProviders, "jumio")
263264
assert.Contains(t, SupportedProviders, "onfido")
264-
assert.Len(t, SupportedProviders, 3)
265+
assert.Contains(t, SupportedProviders, "stripe")
266+
assert.Len(t, SupportedProviders, 4)
267+
}
268+
269+
func TestLoadVerificationConfig_StripeProvider_OnlyNeedsAPIKey(t *testing.T) {
270+
clearVerificationEnv(t)
271+
t.Setenv("VERIFICATION_PROVIDER", "stripe")
272+
t.Setenv("VERIFICATION_WEBHOOK_SECRET", "webhook-secret")
273+
t.Setenv("VERIFICATION_WEBHOOK_URL", "https://api.example.com/webhooks/verification")
274+
t.Setenv("VERIFICATION_API_KEY", "sk_test_my-stripe-key")
275+
t.Setenv("STRIPE_WEBHOOK_SECRET", "whsec_test_stripe_endpoint_secret")
276+
// No VERIFICATION_API_SECRET — Stripe does not need it
277+
278+
cfg, err := LoadVerificationConfig()
279+
280+
require.NoError(t, err)
281+
assert.Equal(t, "stripe", cfg.Provider)
282+
assert.Equal(t, "sk_test_my-stripe-key", cfg.ProviderConfig["api_key"])
283+
assert.Empty(t, cfg.ProviderConfig["api_secret"])
284+
assert.Equal(t, "whsec_test_stripe_endpoint_secret", cfg.StripeWebhookSecret)
285+
}
286+
287+
func TestLoadVerificationConfig_StripeProvider_MissingStripeWebhookSecret(t *testing.T) {
288+
clearVerificationEnv(t)
289+
t.Setenv("VERIFICATION_PROVIDER", "stripe")
290+
t.Setenv("VERIFICATION_WEBHOOK_SECRET", "webhook-secret")
291+
t.Setenv("VERIFICATION_WEBHOOK_URL", "https://api.example.com/webhooks/verification")
292+
t.Setenv("VERIFICATION_API_KEY", "sk_test_my-stripe-key")
293+
// No STRIPE_WEBHOOK_SECRET — required for Stripe
294+
295+
_, err := LoadVerificationConfig()
296+
297+
require.Error(t, err)
298+
assert.ErrorIs(t, err, ErrMissingStripeWebhookSecret)
299+
}
300+
301+
func TestVerificationConfig_Validate_StripeRequiresWebhookConfig(t *testing.T) {
302+
cfg := &VerificationConfig{
303+
Provider: "stripe",
304+
ProviderConfig: map[string]string{"api_key": "sk_test_key"},
305+
// Missing WebhookSecret and WebhookURL
306+
}
307+
308+
err := cfg.Validate()
309+
310+
require.Error(t, err)
311+
assert.ErrorIs(t, err, ErrEmptyWebhookSecretForNonMock)
312+
}
313+
314+
func TestVerificationConfig_Validate_StripeWithoutAPISecretPasses(t *testing.T) {
315+
cfg := &VerificationConfig{
316+
Provider: "stripe",
317+
WebhookSecret: "webhook-secret",
318+
StripeWebhookSecret: "whsec_test_endpoint_secret",
319+
WebhookURL: "https://example.com/webhooks/verification",
320+
ProviderConfig: map[string]string{"api_key": "sk_test_key"},
321+
}
322+
323+
err := cfg.Validate()
324+
325+
assert.NoError(t, err)
326+
}
327+
328+
func TestVerificationConfig_Validate_StripeMissingStripeWebhookSecret(t *testing.T) {
329+
cfg := &VerificationConfig{
330+
Provider: "stripe",
331+
WebhookSecret: "webhook-secret",
332+
StripeWebhookSecret: "", // missing
333+
WebhookURL: "https://example.com/webhooks/verification",
334+
ProviderConfig: map[string]string{"api_key": "sk_test_key"},
335+
}
336+
337+
err := cfg.Validate()
338+
339+
require.Error(t, err)
340+
assert.ErrorIs(t, err, ErrMissingStripeWebhookSecret)
265341
}
266342

267343
func TestVerificationConfig_ValidateForEnvironment_ProductionRejectsMock(t *testing.T) {

services/party/verification/factory.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var (
2020
// Currently supported providers:
2121
// - "mock": Returns a MockProvider for testing and development
2222
// - "onfido": Onfido identity verification
23+
// - "stripe": Stripe Identity verification
2324
//
2425
// Future providers (stubs, not yet implemented):
2526
// - "jumio": Jumio identity verification
@@ -40,6 +41,8 @@ func NewProvider(cfg *config.VerificationConfig) (Provider, error) {
4041
return nil, ErrUnsupportedProvider
4142
case "onfido":
4243
return NewOnfidoProvider(cfg, slog.Default())
44+
case "stripe":
45+
return NewStripeIdentityProvider(cfg, slog.Default())
4346
default:
4447
return nil, ErrUnsupportedProvider
4548
}
@@ -78,6 +81,8 @@ func NewProviderWithOptions(cfg *config.VerificationConfig, opts ProviderOptions
7881
return nil, ErrUnsupportedProvider
7982
case "onfido":
8083
return NewOnfidoProvider(cfg, slog.Default())
84+
case "stripe":
85+
return NewStripeIdentityProvider(cfg, slog.Default())
8186
default:
8287
return nil, ErrUnsupportedProvider
8388
}

services/party/verification/factory_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,42 @@ func TestNewProviderWithOptions_NonMockProvider_ReturnsError(t *testing.T) {
193193
}
194194
}
195195

196+
func TestNewProvider_StripeProvider(t *testing.T) {
197+
cfg := &config.VerificationConfig{
198+
Provider: "stripe",
199+
WebhookSecret: "webhook-secret",
200+
StripeWebhookSecret: "whsec_test_endpoint_secret",
201+
WebhookURL: "https://example.com/webhook",
202+
ProviderConfig: map[string]string{"api_key": "sk_test_key"},
203+
}
204+
205+
provider, err := NewProvider(cfg)
206+
207+
require.NoError(t, err)
208+
require.NotNil(t, provider)
209+
210+
_, ok := provider.(*StripeIdentityProvider)
211+
assert.True(t, ok, "expected *StripeIdentityProvider type")
212+
}
213+
214+
func TestNewProviderWithOptions_StripeProvider(t *testing.T) {
215+
cfg := &config.VerificationConfig{
216+
Provider: "stripe",
217+
WebhookSecret: "webhook-secret",
218+
StripeWebhookSecret: "whsec_test_endpoint_secret",
219+
WebhookURL: "https://example.com/webhook",
220+
ProviderConfig: map[string]string{"api_key": "sk_test_key"},
221+
}
222+
223+
provider, err := NewProviderWithOptions(cfg, DefaultProviderOptions())
224+
225+
require.NoError(t, err)
226+
require.NotNil(t, provider)
227+
228+
_, ok := provider.(*StripeIdentityProvider)
229+
assert.True(t, ok, "expected *StripeIdentityProvider type")
230+
}
231+
196232
func TestNewProviderWithOptions_OnfidoProvider(t *testing.T) {
197233
cfg := &config.VerificationConfig{
198234
Provider: "onfido",

0 commit comments

Comments
 (0)