Require card verification for cost-restricted dial codes

What does this MR do and why?

Require card verification for cost-restricted dial codes

Phone verification is billed per SMS and termination cost varies sharply by destination. High-cost prefixes are what SMS pumping targets, and every attempt draws down the global daily transaction budget that gates phone verification for all users.

Add an admin-configurable list of dial codes that must complete credit card verification before phone verification. The check runs before the per-user rate limit, so a redirected user does not lose one of their daily send attempts, and before the Telesign call, so no transaction is spent on a number we are not going to text.

This reuses assume_high_risk!, which already produces the required email -> credit card -> phone ordering. A user already flagged for another reason keeps that reason rather than having it overwritten.

The list is empty by default, so behaviour is unchanged until an administrator populates it.

Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/609536

Changelog: added EE: true

References

Screenshots or screen recordings

How to set up and validate locally

To set it up make sure the right flags and settings are enabled (see below). Register a user and before confirming email update the users arkose risk score to medium. then continue confirming email, when you use the country code you've added to the settings and submit it should refresh the page and force credit card verification first.

How to set up

GDK must run with SaaS simulation, since identity verification is gated on Gitlab::Saas.feature_available?(:identity_verification):

# gdk.yml
env:
  GITLAB_SIMULATE_SAAS: 1

Then gdk restart rails. Use gdk rails console rather than bin/rails console, otherwise gdk.env is not loaded and the instance will not be in SaaS mode.

Configure the setting (92 is used as the example restricted dial code):

ApplicationSetting.current.update!(
  sms_cost_restricted_dial_codes: [92],
  phone_verification_enabled: true,
  credit_card_verification_enabled: true
)
Gitlab::CurrentSettings.expire_current_application_settings

Verify the gate

The gate returns before query_intelligence_api and before the Telesign send, so this needs no Telesign or Arkose credentials.

user = create(:user) # or any user without a credit card

result = PhoneVerification::Users::SendVerificationCodeService.new(
  user,
  ip_address: '127.0.0.1',
  country: 'PK',
  international_dial_code: 92,
  phone_number: '3001234567'
).execute

result.error?    # => true
result.reason    # => :card_verification_required
result.message   # => "Verify a payment method to continue."

Re-fetch the user (the risk profile is memoized on the instance):

user = User.find(user.id)
user.assumed_high_risk?  # => true
user.custom_attributes.find_by(key: 'assumed_high_risk_reason').value
# => "Cost-restricted dial code"
user.required_identity_verification_methods
# => ["email", "credit_card", "phone"]   <- credit card now precedes phone

Confirm nothing was spent:

user.phone_number_validation&.sms_send_count  # => nil or unchanged, no SMS sent
Gitlab::ApplicationRateLimiter.peek(:phone_verification_send_code, scope: user)
# => false, the daily send allowance was not consumed

Verify each guard lets the user through

Each of these should return a non-card_verification_required result. With no Telesign credentials locally they will fail later in the chain, which is the point: the gate is no longer what stops them.

  1. Dial code not on the list — resend with international_dial_code: 1.
  2. User already has a credit cardcreate(:credit_card_validation, user: user), then resend with 92.
  3. Credit card verification disabled instance-wideApplicationSetting.current.update!(credit_card_verification_enabled: false) then resend with 92. This is the lockout case: the user must not be blocked, because there would be no credit card step for them to complete.
  4. User already assumed high riskuser.assume_high_risk!(reason: 'Duplicate phone number'), then resend with 92. The original reason must be preserved and the user must not be blocked a second time.

Verify in the UI

With a user partway through identity verification, enter a phone number on a restricted dial code and submit. Expected: the request returns 400, the page reloads, and the wizard re-renders with the credit card step ahead of phone.

What should not happen

  • No SMS is sent and no Telesign transaction is spent
  • The user's daily phone verification send allowance is not decremented
  • With credit_card_verification_enabled false, the user is neither flagged nor blocked

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #609536

Edited by Jay

Merge request reports

Loading
Loading