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.
80 lines
3.6 KiB
Go
80 lines
3.6 KiB
Go
//go:build !dev
|
|
|
|
package user
|
|
|
|
// Tests for the PRODUCTION 2FA issuance gate (twofa_prod.go).
|
|
//
|
|
// LIMITATION (documented — M17): twofa_prod.go is compiled only in a genuine
|
|
// production build (`!dev && !test`). Under BOTH required test runs — the
|
|
// "test,dev" run and the "test,!dev" prod-shape run — the dev/test variant
|
|
// (twofa_dev.go, build tag `dev || test`) is the compiled function and its
|
|
// fail-closed branches (no TWO_FACTOR_PEPPER → refuse; no delivery channel →
|
|
// 503) are unreachable. These tests compile in every `!dev` build and run
|
|
// their assertions ONLY when the prod variant marker reports the real prod
|
|
// functions are live; under the test tag they skip with the same documented
|
|
// rationale the payments package uses (twofa_delivery_prod_test.go).
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
// twoFAAllowLogDeliveryEnv is only defined in twofa_prod.go (!dev && !test);
|
|
// use the literal env name so this test also compiles under `test,!dev`.
|
|
const allowLogDeliveryEnv = "TWO_FACTOR_ALLOW_LOG_DELIVERY"
|
|
|
|
// TestTwoFAEnsureIssueAllowed_ProdPredicate pins the production issuance gate:
|
|
// it fails closed without TWO_FACTOR_PEPPER (an unsalted digest in the 1M code
|
|
// space would be offline-brute-forceable) or without a delivery channel, and
|
|
// allows issuance only when both are configured.
|
|
func TestTwoFAEnsureIssueAllowed_ProdPredicate(t *testing.T) {
|
|
if !twofaProdVariant {
|
|
t.Skip("twoFAEnsureIssueAllowed() is the dev/test build's always-allowed variant (twofa_dev.go, `dev || test`); the prod fail-closed branches are unreachable under the test tag — see the file header for the documented limitation")
|
|
}
|
|
|
|
os.Unsetenv(twoFAPepperEnv)
|
|
os.Unsetenv(allowLogDeliveryEnv)
|
|
if err := twoFAEnsureIssueAllowed(); err == nil {
|
|
t.Error("expected issuance refused without TWO_FACTOR_PEPPER in a production build")
|
|
} else if err.Error() != "TWO_FACTOR_PEPPER is not set; refusing to issue a 2FA code (an unsalted digest would be offline-brute-forceable)" {
|
|
t.Errorf("expected the pepper-required error without the pepper, got %v", err)
|
|
}
|
|
|
|
os.Setenv(twoFAPepperEnv, "test-pepper")
|
|
os.Unsetenv(allowLogDeliveryEnv)
|
|
if err := twoFAEnsureIssueAllowed(); err == nil {
|
|
t.Error("expected issuance refused without a delivery channel in a production build")
|
|
} else if err != errTwoFADeliveryUnavailable {
|
|
t.Errorf("expected errTwoFADeliveryUnavailable without a channel, got %v", err)
|
|
}
|
|
|
|
os.Setenv(allowLogDeliveryEnv, "true")
|
|
if err := twoFAEnsureIssueAllowed(); err != nil {
|
|
t.Errorf("expected issuance allowed with both the pepper and a delivery channel, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestTwoFADeliveryAvailable_ProdPredicate pins the production delivery
|
|
// predicate: TWO_FACTOR_ALLOW_LOG_DELIVERY unset → no channel (false), exactly
|
|
// "true" → channel (true), any other value → no channel.
|
|
func TestTwoFADeliveryAvailable_ProdPredicate(t *testing.T) {
|
|
if !twofaProdVariant {
|
|
t.Skip("twoFADeliveryAvailable() is the dev/test build's trivially-true variant (twofa_dev.go, `dev || test`); the 503 delivery-unavailable branch is unreachable under the test tag — see the file header for the documented limitation")
|
|
}
|
|
|
|
os.Unsetenv(allowLogDeliveryEnv)
|
|
if twoFADeliveryAvailable() {
|
|
t.Error("production without the explicit opt-in must have NO 2FA delivery channel")
|
|
}
|
|
for _, v := range []string{"", "1", "yes", "on", "True", "TRUE", "false"} {
|
|
os.Setenv(allowLogDeliveryEnv, v)
|
|
if twoFADeliveryAvailable() {
|
|
t.Errorf("value %q must NOT open the delivery channel (exact 'true' only)", v)
|
|
}
|
|
}
|
|
os.Setenv(allowLogDeliveryEnv, "true")
|
|
if !twoFADeliveryAvailable() {
|
|
t.Error("the explicit insecure log-delivery opt-in must open the channel")
|
|
}
|
|
}
|