PSR 2017 reg 100 makes SCA mandatory and non-waivable for customer-initiated stored-credential charges; a merchant-side 2FA check cannot legally substitute for it (authorising a token-less charge via 2FA leaves the MERCHANT liable for ECI 7 / SLI 210 chargebacks and reg 77(6) compensation regardless of consent). - payments/twofa.go: the homegrown 2FA fallback for token-less saved-card charges is REMOVED ENTIRELY. requireTwoFactorForCardAccess is now SCA-only: a non-empty Square verification_token (charge surfaces, token forwarded to Square) skips the gate; anything else is refused 402 verification_required. enforceSCAFallbackConsent is a compile-compatible no-op (fallback never runs). - New requireTwoFactorForCardAccessWithTokenValidation distinguishes surfaces where the token IS forwarded to Square (charge — Square validates it) from card-SAVE surfaces (token client-asserted, never forwarded: a non-empty token must NOT skip the save gate, auth-F1). - SCA tokenize-result wire contract (C1): a saved card charged with a fresh one-time tokenize-result sends the token as the charge SOURCE (new_card_token -> source_id) alongside saved_card_id, never a separate verification_token. resolveChargeSource resolves the saved-card branch FIRST (customer from the card row, token as source) so combined token+card requests are SCA-clean. - C6 consent fields (consent_version / consent_accepted) added to the booking/ tip/till/gift-card charge requests, enforced server-side before any fallback charge could reach Square and recorded on the 2fa_fallback_charge audit row; logVerificationTokenProvenance traces minted tokens to their charge. - user 2FA issuance gate refactored into pure build-agnostic functions (twoFAPepperConfigured / twoFADeliveryChannelConfigured / twoFAEnsureIssueAllowedStrict) shared with the payments re-issue path and exercised directly by the test,dev suite; TWO_FACTOR_FALLBACK switch and .env.example entry removed; startup posture notes updated. - Test coverage: fail-closed 2FA production gates (pepper/delivery), token validation on save vs charge surfaces, completion idempotency, idempotency key determinism, refund-policy 72h/24h epsilon boundaries, VAT parity.
61 lines
2.6 KiB
Go
61 lines
2.6 KiB
Go
//go:build test
|
|
|
|
package payments
|
|
|
|
// M17 follow-up (ITEM 2): twofa_delivery_prod.go is excluded from the test,dev
|
|
// suite (`!dev && !test`), so its fail-closed re-issue branches were never
|
|
// exercised in CI — the old twofa_delivery_prod_test.go only runs its
|
|
// assertions in a genuine production build and skips under the test tag. The
|
|
// pure decision logic now lives build-agnostically in twofa.go
|
|
// (twoFAReissueIssueAllowedStrict / twoFAPepperConfigured /
|
|
// twoFADeliveryChannelConfigured); these tests exercise those branches in the
|
|
// STANDARD test,dev run, so a regression in the prod fail-closed behaviour is
|
|
// CI-visible even though the prod file itself is only compiled in a genuine
|
|
// production build.
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestTwoFAReissueIssueAllowedStrict_FailClosed pins the production-style
|
|
// re-issue gate that twofa_delivery_prod.go's twoFAReissueIssueAllowed
|
|
// delegates to (reissueTwoFACodeAfterFailedCharge): (a) pepper unset →
|
|
// re-issue refused (errTwoFAPepperRequired — an unsalted digest in the 1M code
|
|
// space would be offline-brute-forceable); (b) delivery channel absent →
|
|
// re-issue refused (errTwoFADeliveryUnavailable — the 503-style error);
|
|
// (c) both configured → re-issue succeeds.
|
|
func TestTwoFAReissueIssueAllowedStrict_FailClosed(t *testing.T) {
|
|
t.Run("pepper_unset_refuses_reissue", func(t *testing.T) {
|
|
t.Setenv("TWO_FACTOR_PEPPER", "")
|
|
t.Setenv("TWO_FACTOR_ALLOW_LOG_DELIVERY", "true")
|
|
require.ErrorIs(t, twoFAReissueIssueAllowedStrict(), errTwoFAPepperRequired)
|
|
})
|
|
|
|
t.Run("delivery_channel_absent_refuses_reissue", func(t *testing.T) {
|
|
t.Setenv("TWO_FACTOR_PEPPER", "test-pepper")
|
|
t.Setenv("TWO_FACTOR_ALLOW_LOG_DELIVERY", "")
|
|
require.ErrorIs(t, twoFAReissueIssueAllowedStrict(), errTwoFADeliveryUnavailable)
|
|
})
|
|
|
|
t.Run("pepper_and_channel_present_allows_reissue", func(t *testing.T) {
|
|
t.Setenv("TWO_FACTOR_PEPPER", "test-pepper")
|
|
t.Setenv("TWO_FACTOR_ALLOW_LOG_DELIVERY", "true")
|
|
require.NoError(t, twoFAReissueIssueAllowedStrict())
|
|
})
|
|
}
|
|
|
|
// TestPaymentsTwoFADeliveryChannelConfigured pins the pure delivery-channel
|
|
// predicate behind the payments 503 refusal: only the exact value "true" opens
|
|
// the channel.
|
|
func TestPaymentsTwoFADeliveryChannelConfigured(t *testing.T) {
|
|
t.Setenv("TWO_FACTOR_PEPPER", "test-pepper")
|
|
for _, v := range []string{"", "1", "yes", "on", "True", "TRUE", "false"} {
|
|
t.Setenv("TWO_FACTOR_ALLOW_LOG_DELIVERY", v)
|
|
require.False(t, twoFADeliveryChannelConfigured(), "value %q must NOT open the delivery channel (exact 'true' only)", v)
|
|
}
|
|
t.Setenv("TWO_FACTOR_ALLOW_LOG_DELIVERY", "true")
|
|
require.True(t, twoFADeliveryChannelConfigured())
|
|
}
|