package payments import ( "net/http" "os" "strings" ) // require2FADisabled reports whether REQUIRE_2FA explicitly disables 2FA // enforcement. The parse is case-insensitive and alias-tolerant (false/0/off/no), // so a value like "False", "OFF" or "off" never silently leaves the gate ON. // Any other value — including empty or unknown — keeps enforcement ON // (fail-closed). func require2FADisabled() bool { switch strings.ToLower(strings.TrimSpace(os.Getenv("REQUIRE_2FA"))) { case "false", "0", "off", "no": return true default: return false } } // twoFactorEnforced reports whether 2FA is required for online card payments. // It is fail-closed: enforcement is ON unless 2FA has been explicitly disabled // (REQUIRE_2FA=false/0/off/no, case-insensitive — see require2FADisabled) or // SQUARE_ENVIRONMENT explicitly selects the dev/mock stack // (mock/dev/development/test — see IsExplicitDevOrMockEnv in // idempotency_helpers.go). Empty or unknown SQUARE_ENVIRONMENT values are // treated as production-enforced, so a mistyped env var can never silently // disarm the gate — main.go logs a startup warning for that misconfiguration. func twoFactorEnforced() bool { return !require2FADisabled() && !IsExplicitDevOrMockEnv() } // TwoFactorEnforced is the exported form of twoFactorEnforced, so the user // package (settings endpoints) and the profile handler can report whether 2FA // is currently required without re-implementing the env logic. func (s *PaymentService) TwoFactorEnforced() bool { return twoFactorEnforced() } // requireTwoFactorForCardAccess gates the saved-card online payment paths. // It returns (allowed, fallbackUsed): allowed is true when the request may // proceed; fallbackUsed is ALWAYS false — the homegrown 2FA fallback for // token-less saved-card charges was REMOVED ENTIRELY, so no charge is ever // authorized by a 2FA code and no fallback audit row is ever written. // // It is a thin wrapper over requireTwoFactorForCardAccessWithTokenValidation // that passes tokenForwardedToSquare=true — the saved-card CHARGE surfaces // (booking, tip, gift-card buy, till, terminal) forward the verification_token // to Square in CreatePaymentReq.VerificationToken, so a non-empty token there // IS Square-validated SCA and legitimately skips the gate. The card-SAVE // surfaces call the WithTokenValidation variant directly with false (see that // helper for the auth-F1 rationale). // // The decision model, in order: // // - 2FA enforcement is not active (dev/mock, or REQUIRE_2FA disabled) → // allowed. The dev mock simulates SCA (SimulateSavedCardVerificationRequired // + cnon:sca-... tokenize-results), so development has full parity with the // SCA-only production posture. // // - The request carries a Square verification_token (SCA performed — the // issuer has already authenticated the buyer) AND the token is forwarded to // Square on this surface (tokenForwardedToSquare=true, the charge paths): // SKIP the gate entirely. SCA is PRIMARY; a charge that carries a token // passes even for a user who has not enabled 2FA. // // - Otherwise the charge is token-less, and there is NO homegrown 2FA // fallback anymore. PSR 2017 reg 100 makes Strong Customer Authentication // mandatory and NON-WAIVABLE for customer-initiated stored-credential // charges, and a merchant-side 2FA check with no bank involvement cannot // legally act as an SCA substitute: authorising a token-less charge via 2FA // would leave the MERCHANT liable for ECI 7 / SLI 210 chargebacks and PSR // 2017 reg 77(6) compensation, and customer consent does not cure that. On // genuine sca-unavailable the charge is therefore REFUSED 402 // verification_required and the customer is invited to pay online later. // // On any denial an error JSON is written (parseable by the frontend via // extractErrorMessage) and allowed=false is returned — the caller must abort // the charge. func requireTwoFactorForCardAccess(w http.ResponseWriter, r *http.Request, service *PaymentService, userID, verificationToken string, consume bool) (allowed, fallbackUsed bool) { _ = consume // TODO: remove when callers updated — the SCA-only gate never reads verification codes return requireTwoFactorForCardAccessWithTokenValidation(w, r, service, userID, verificationToken, consume, true) } // requireTwoFactorForCardAccessWithTokenValidation is the real gate: // requireTwoFactorForCardAccess above is the charge-surface wrapper that passes // tokenForwardedToSquare=true. The extra flag distinguishes surfaces where the // verification_token WILL be forwarded to Square (charge — Square validates it // server-side and rejects a forged token, so a non-empty token legitimately // proves SCA) from surfaces where it is client-asserted and NEVER forwarded // (card SAVE — the card is persisted via CreateCardOnFile, which takes no // verification_token, so Square never validates it). // // auth-F1: on a SAVE surface a forged non-empty verification_token must NOT // skip the gate — with tokenForwardedToSquare=false the token is ignored and // the token-less refusal below applies. An authenticated client can therefore // no longer persist a card to the account with `verification_token: "anything"` // and no SCA. The legitimate SAVE skips are handled by the call site before // this helper is ever reached: a genuine Square token-like card token is // SCA-proven (the STORE-intent SCA performed at tokenization IS the // verification — see CreatePaymentMethod's save gate) and the // scaTokenizedSavedCard flow (new_card_token + saved_card_id) never persists a // card. // // The verification_code parameter that used to flow through this gate is GONE: // the gate never read it (SCA-only), the request structs no longer carry it, // and there is no homegrown fallback to authorise anything with it. func requireTwoFactorForCardAccessWithTokenValidation(w http.ResponseWriter, r *http.Request, service *PaymentService, userID, verificationToken string, consume bool, tokenForwardedToSquare bool) (allowed, fallbackUsed bool) { _ = consume // TODO: remove when callers updated — the SCA-only gate never reads verification codes if !twoFactorEnforced() { return true, false } // SCA-primary: a Square verification_token means the issuer already // completed Strong Customer Authentication — the gate is skipped and no // fallback applies. This skip is only valid when the token WILL be // forwarded to Square (tokenForwardedToSquare=true, the charge surfaces): // Square validates it and rejects a forged value. On a SAVE surface the // token is client-asserted and never reaches Square, so a forged non-empty // token must not skip the gate (auth-F1) — the token-less refusal below // applies instead. if verificationToken != "" && tokenForwardedToSquare { return true, false } // SCA-only: a token-less saved-card charge has NO 2FA fallback (the // homegrown fallback was REMOVED — see the gate doc above for the PSR 2017 // legal rationale). It is refused 402 verification_required; the customer is // invited to pay online later (or through Square's buyer-verification flow // on a retry). fallbackUsed stays false. writeVerificationRequiredResponse(w) return false, false }