feat: SCA-only saved-card charges — 2FA charge fallback removed (C6), versioned consent fields, token provenance
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.
This commit is contained in:
+19
-17
@@ -191,11 +191,11 @@ func initSquare() {
|
||||
// 2FA enforcement is fail-closed (see payments.twoFactorEnforced): it is
|
||||
// OFF only when REQUIRE_2FA explicitly disables it (false/0/off/no,
|
||||
// case-insensitive) or SQUARE_ENVIRONMENT explicitly selects the dev/mock
|
||||
// stack. Warn loudly when a non-dev env (empty/unknown — a likely
|
||||
// misconfiguration) leaves the gate disabled, so saved-card charges can
|
||||
// never silently ship without this merchant-level authorization gate (an
|
||||
// additional fraud control; NOT PSD2 SCA — Square buyer verification is the
|
||||
// SCA mechanism, wired for new-card charges).
|
||||
// stack. In an enforced env a token-less saved-card charge is refused 402
|
||||
// verification_required (SCA-only — see the posture note below); in a
|
||||
// dev/mock env the gate lifts and the mock simulates SCA. Warn loudly when
|
||||
// a non-dev env (empty/unknown — a likely misconfiguration) leaves the gate
|
||||
// disabled.
|
||||
enforced := payments.NewPaymentService().TwoFactorEnforced()
|
||||
if enforced {
|
||||
// Delivery is build-dependent (handlers/user/twofa_dev.go /
|
||||
@@ -206,24 +206,26 @@ func initSquare() {
|
||||
// misled into thinking codes are reaching users when issuance is
|
||||
// actually failing closed.
|
||||
if os.Getenv("TWO_FACTOR_ALLOW_LOG_DELIVERY") == "true" {
|
||||
log.Printf("WARNING: 2FA codes are delivered in PLAINTEXT via the server log ([2FA] prefix) — anyone with backend log access can defeat the 2FA gate. Restrict log access and relay codes out-of-band; replace this loose-fake delivery with email/SMS (P6) before launch.")
|
||||
log.Printf("WARNING: 2FA codes are delivered in PLAINTEXT via the server log ([2FA] prefix) — anyone with backend log access can defeat the account 2FA gate. Restrict log access and relay codes out-of-band; replace this loose-fake delivery with email/SMS (P6) before launch.")
|
||||
} else {
|
||||
log.Printf("WARNING: 2FA enforcement is ON but TWO_FACTOR_ALLOW_LOG_DELIVERY is unset: in a production build there is NO code-delivery channel (email/SMS is not wired — P6), so 2FA code issuance FAILS CLOSED and no user can complete setup or disable. Every enforced saved-card online payment for a user without 2FA will 403 with no way to enable it. Set TWO_FACTOR_ALLOW_LOG_DELIVERY=true to opt into the insecure [2FA] log-delivery channel (plaintext codes in the server log — restrict log access), or wire email/SMS (P6).")
|
||||
log.Printf("WARNING: 2FA enforcement is ON but TWO_FACTOR_ALLOW_LOG_DELIVERY is unset: in a production build there is NO code-delivery channel (email/SMS is not wired — P6), so 2FA code issuance FAILS CLOSED and no user can complete 2FA setup or disable. Set TWO_FACTOR_ALLOW_LOG_DELIVERY=true to opt into the insecure [2FA] log-delivery channel (plaintext codes in the server log — restrict log access), or wire email/SMS (P6).")
|
||||
}
|
||||
}
|
||||
if !enforced && !payments.IsExplicitDevOrMockEnv() {
|
||||
log.Printf("WARNING: 2FA enforcement is OFF (REQUIRE_2FA=%q) with SQUARE_ENVIRONMENT=%q (not an explicit mock/dev value). Online saved-card payments will NOT require 2FA.", os.Getenv("REQUIRE_2FA"), env)
|
||||
}
|
||||
// SCA-primary / 2FA-backup posture (TWO_FACTOR_FALLBACK, default "true"):
|
||||
// the homegrown 2FA gate is the BACKUP authorization for saved-card charges
|
||||
// that carry no Square verification_token. TWO_FACTOR_FALLBACK=false makes
|
||||
// the deployment SCA-only: a token-less saved-card charge is then rejected
|
||||
// 402 verification_required (the frontend shows the 3DS challenge; if the
|
||||
// bank cannot do SCA the payment cannot proceed). Warn when the operator
|
||||
// opted out so the SCA-only posture is a deliberate, visible choice.
|
||||
if !payments.NewPaymentService().TwoFactorFallbackEnabled() && !payments.IsExplicitDevOrMockEnv() {
|
||||
log.Printf("WARNING: TWO_FACTOR_FALLBACK=false with SQUARE_ENVIRONMENT=%q — the homegrown 2FA is DISABLED as a saved-card charge fallback. Only charges carrying a Square verification_token (SCA performed) can proceed; a token-less charge is rejected 402 verification_required.", env)
|
||||
}
|
||||
// SCA-only posture (permanent — no switch): saved-card charges are
|
||||
// authorised exclusively by Square PSD2 SCA. PSR 2017 reg 100 makes SCA
|
||||
// mandatory and non-waivable for customer-initiated stored-credential
|
||||
// charges, and the homegrown 2FA cannot legally act as an SCA fallback: a
|
||||
// token-less saved-card charge is refused 402 verification_required ("pay
|
||||
// online later") — a fallback-authorised charge would leave the MERCHANT
|
||||
// liable for ECI 7 / SLI 210 chargebacks and PSR 2017 reg 77(6)
|
||||
// compensation, and consent does not cure it. The dev mock simulates SCA
|
||||
// (SimulateSavedCardVerificationRequired + cnon:sca-... tokenize-results),
|
||||
// so development has full parity. There is no TWO_FACTOR_FALLBACK switch to
|
||||
// warn about — it was removed; a token-less charge can never be authorised
|
||||
// by 2FA in any environment.
|
||||
// The mirror-image confusion: enforcement is ON but the Square client fell
|
||||
// back to the in-memory mock (internal/square.NewDevClient only picks the
|
||||
// real API for sandbox/production) because SQUARE_ENVIRONMENT is empty or
|
||||
|
||||
Reference in New Issue
Block a user