diff --git a/ee/app/services/arkose/user_verification_service.rb b/ee/app/services/arkose/user_verification_service.rb
index 82aff20c37b1bb5529e1458f69ea46ce0261ad4f..fbb777131c60f946bc6832be975f81f61fa82e3e 100644
--- a/ee/app/services/arkose/user_verification_service.rb
+++ b/ee/app/services/arkose/user_verification_service.rb
@@ -1,11 +1,10 @@
 # frozen_string_literal: true
 module Arkose
   class UserVerificationService
-    attr_reader :url, :session_token, :user
+    attr_reader :url, :session_token, :user, :response
 
     ARKOSE_LABS_DEFAULT_NAMESPACE = 'client'
     ARKOSE_LABS_DEFAULT_SUBDOMAIN = 'verify-api'
-    ALLOWLIST_TELLTALE = 'gitlab1-whitelist-qa-team'
 
     def initialize(session_token:, user:)
       @session_token = session_token
@@ -13,14 +12,16 @@ def initialize(session_token:, user:)
     end
 
     def execute
-      response = Gitlab::HTTP.perform_request(Net::HTTP::Post, arkose_verify_url, body: body).parsed_response
-      logger.info(build_message(response))
+      json_response = Gitlab::HTTP.perform_request(Net::HTTP::Post, arkose_verify_url, body: body).parsed_response
+      @response = VerifyResponse.new(json_response)
 
-      return false if invalid_token(response)
+      logger.info(build_message)
 
-      add_or_update_arkose_attributes(response)
+      return false if response.invalid_token?
 
-      allowlisted?(response) || (challenge_solved?(response) && low_risk?(response))
+      add_or_update_arkose_attributes
+
+      response.allowlisted? || (response.challenge_solved? && response.low_risk?)
     rescue StandardError => error
       payload = { session_token: session_token, log_data: user.id }
       Gitlab::ExceptionLogFormatter.format!(error, payload)
@@ -32,53 +33,23 @@ def execute
 
     private
 
-    def add_or_update_arkose_attributes(response)
+    def add_or_update_arkose_attributes
       return if Gitlab::Database.read_only?
 
-      custom_attributes = custom_attributes(response)
-
       UserCustomAttribute.upsert_custom_attributes(custom_attributes)
     end
 
-    def custom_attributes(response)
+    def custom_attributes
       custom_attributes = []
-      custom_attributes.push({ key: 'arkose_session', value: session_id(response) })
-      custom_attributes.push({ key: 'arkose_risk_band', value: risk_band(response) })
-      custom_attributes.push({ key: 'arkose_global_score', value: global_score(response) })
-      custom_attributes.push({ key: 'arkose_custom_score', value: custom_score(response) })
+      custom_attributes.push({ key: 'arkose_session', value: response.session_id })
+      custom_attributes.push({ key: 'arkose_risk_band', value: response.risk_band })
+      custom_attributes.push({ key: 'arkose_global_score', value: response.global_score })
+      custom_attributes.push({ key: 'arkose_custom_score', value: response.custom_score })
 
       custom_attributes.map! { |custom_attribute| custom_attribute.merge({ user_id: user.id }) }
       custom_attributes
     end
 
-    def custom_score(response)
-      response&.dig('session_risk', 'custom', 'score') || 0
-    end
-
-    def global_score(response)
-      response&.dig('session_risk', 'global', 'score') || 0
-    end
-
-    def risk_band(response)
-      response&.dig('session_risk', 'risk_band') || 'Unavailable'
-    end
-
-    def session_id(response)
-      response&.dig('session_details', 'session') || 'Unavailable'
-    end
-
-    def risk_category(response)
-      response&.dig('session_risk', 'risk_category') || 'Unavailable'
-    end
-
-    def global_telltale_list(response)
-      response&.dig('session_risk', 'global', 'telltales') || 'Unavailable'
-    end
-
-    def custom_telltale_list(response)
-      response&.dig('session_risk', 'custom', 'telltales') || 'Unavailable'
-    end
-
     def body
       {
         private_key: Settings.arkose_private_api_key,
@@ -91,49 +62,28 @@ def logger
       Gitlab::AppLogger
     end
 
-    def build_message(response)
+    def build_message
       Gitlab::ApplicationContext.current.symbolize_keys.merge(
         {
           message: 'Arkose verify response',
-          response: response,
+          response: response.response,
           username: user.username
-        }.merge(arkose_payload(response))
+        }.merge(arkose_payload)
       )
     end
 
-    def arkose_payload(response)
+    def arkose_payload
       {
-        'arkose.session_id': session_id(response),
-        'arkose.global_score': global_score(response),
-        'arkose.global_telltale_list': global_telltale_list(response),
-        'arkose.custom_score': custom_score(response),
-        'arkose.custom_telltale_list': custom_telltale_list(response),
-        'arkose.risk_band': risk_band(response),
-        'arkose.risk_category': risk_category(response)
+        'arkose.session_id': response.session_id,
+        'arkose.global_score': response.global_score,
+        'arkose.global_telltale_list': response.global_telltale_list,
+        'arkose.custom_score': response.custom_score,
+        'arkose.custom_telltale_list': response.custom_telltale_list,
+        'arkose.risk_band': response.risk_band,
+        'arkose.risk_category': response.risk_category
       }
     end
 
-    def invalid_token(response)
-      response&.key?('error')
-    end
-
-    def challenge_solved?(response)
-      solved = response&.dig('session_details', 'solved')
-      solved.nil? ? true : solved
-    end
-
-    def low_risk?(response)
-      return true unless Feature.enabled?(:arkose_labs_prevent_login)
-
-      risk_band = risk_band(response)
-      risk_band.present? ? risk_band != 'High' : true
-    end
-
-    def allowlisted?(response)
-      telltale_list = response&.dig('session_details', 'telltale_list') || []
-      telltale_list.include?(ALLOWLIST_TELLTALE)
-    end
-
     def arkose_verify_url
       arkose_labs_namespace = ::Gitlab::CurrentSettings.arkose_labs_namespace
       subdomain = if arkose_labs_namespace == ARKOSE_LABS_DEFAULT_NAMESPACE
diff --git a/ee/lib/arkose/verify_response.rb b/ee/lib/arkose/verify_response.rb
new file mode 100644
index 0000000000000000000000000000000000000000..60e7fc05d9ac67a78f6ebfdffe7a081d3a50ae75
--- /dev/null
+++ b/ee/lib/arkose/verify_response.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Arkose
+  class VerifyResponse
+    attr_reader :response
+
+    ALLOWLIST_TELLTALE = 'gitlab1-whitelist-qa-team'
+
+    def initialize(response)
+      @response = response
+    end
+
+    def invalid_token?
+      response&.key?('error')
+    end
+
+    def error
+      response["error"]
+    end
+
+    def challenge_solved?
+      solved = response&.dig('session_details', 'solved')
+      solved.nil? ? true : solved
+    end
+
+    def low_risk?
+      return true unless Feature.enabled?(:arkose_labs_prevent_login)
+
+      risk_band.present? ? risk_band != 'High' : true
+    end
+
+    def allowlisted?
+      telltale_list = response&.dig('session_details', 'telltale_list') || []
+      telltale_list.include?(ALLOWLIST_TELLTALE)
+    end
+
+    def custom_score
+      response&.dig('session_risk', 'custom', 'score') || 0
+    end
+
+    def global_score
+      response&.dig('session_risk', 'global', 'score') || 0
+    end
+
+    def risk_band
+      response&.dig('session_risk', 'risk_band') || 'Unavailable'
+    end
+
+    def session_id
+      response&.dig('session_details', 'session') || 'Unavailable'
+    end
+
+    def risk_category
+      response&.dig('session_risk', 'risk_category') || 'Unavailable'
+    end
+
+    def global_telltale_list
+      response&.dig('session_risk', 'global', 'telltales') || 'Unavailable'
+    end
+
+    def custom_telltale_list
+      response&.dig('session_risk', 'custom', 'telltales') || 'Unavailable'
+    end
+  end
+end
diff --git a/ee/spec/fixtures/arkose/allowlisted_response.json b/ee/spec/fixtures/arkose/allowlisted_response.json
new file mode 100644
index 0000000000000000000000000000000000000000..a29551805f68485455bcb4a52312de027943e37b
--- /dev/null
+++ b/ee/spec/fixtures/arkose/allowlisted_response.json
@@ -0,0 +1,76 @@
+{
+  "session_details": {
+    "solved": false,
+    "session": "22612c147bb418c8.2570749403",
+    "session_created": "2021-08-29T23:13:03+00:00",
+    "check_answer": "2021-08-29T23:13:16+00:00",
+    "verified": "2021-08-30T00:19:32+00:00",
+    "attempted": true,
+    "security_level": 30,
+    "session_is_legit": false,
+    "previously_verified": true,
+    "session_timed_out": true,
+    "suppress_limited": false,
+    "theme_arg_invalid": false,
+    "suppressed": false,
+    "punishable_actioned": false,
+    "telltale_user": "eng-1362-game3-py-0.",
+    "failed_low_sec_validation": false,
+    "lowsec_error": null,
+    "lowsec_level_denied": null,
+    "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36",
+    "ip_rep_list": null,
+    "game_number_limit_reached": false,
+    "user_language_shown": "en",
+    "telltale_list": [
+      "gitlab1-whitelist-qa-team"
+    ],
+    "optional": null
+  },
+  "fingerprint": {
+    "browser_characteristics": {
+      "browser_name": "Chrome",
+      "browser_version": "92.0.4515.159",
+      "color_depth": 24,
+      "session_storage": false,
+      "indexed_database": false,
+      "canvas_fingerprint": 1652956012
+    },
+    "device_characteristics": {
+      "operating_system": null,
+      "operating_system_version": null,
+      "screen_resolution": [
+        1920,
+        1080
+      ],
+      "max_resolution_supported": [
+        1920,
+        1057
+      ],
+      "behavior": false,
+      "cpu_class": "unknown",
+      "platform": "MacIntel",
+      "touch_support": false,
+      "hardware_concurrency": 8
+    },
+    "user_preferences": {
+      "timezone_offset": -600
+    }
+  },
+  "ip_intelligence": {
+    "user_ip": "10.211.121.196",
+    "is_tor": false,
+    "is_vpn": true,
+    "is_proxy": true,
+    "is_bot": true,
+    "country": "AU",
+    "region": "New South Wales",
+    "city": "Sydney",
+    "isp": "Amazon.com",
+    "public_access_point": false,
+    "connection_type": "Data Center",
+    "latitude": "-38.85120035",
+    "longitude": "106.21220398",
+    "timezone": "Australia/Sydney"
+  }
+}
diff --git a/ee/spec/fixtures/arkose/invalid_token.json b/ee/spec/fixtures/arkose/invalid_token.json
new file mode 100644
index 0000000000000000000000000000000000000000..e4004ba76814f2f3b69bc8f086306a27aa684bc9
--- /dev/null
+++ b/ee/spec/fixtures/arkose/invalid_token.json
@@ -0,0 +1,4 @@
+{
+  "error": "DENIED ACCESS",
+  "verified": "2022-08-12T07:57:31Z"
+}
\ No newline at end of file
diff --git a/ee/spec/lib/arkose/verify_response_spec.rb b/ee/spec/lib/arkose/verify_response_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..42814c422f22a331a9072c076b73c207b22d94c8
--- /dev/null
+++ b/ee/spec/lib/arkose/verify_response_spec.rb
@@ -0,0 +1,183 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Arkose::VerifyResponse do
+  def parse_json(file_path)
+    Gitlab::Json.parse(File.read(Rails.root.join(file_path)))
+  end
+
+  let(:invalid_token_response) do
+    parse_json('ee/spec/fixtures/arkose/invalid_token.json')
+  end
+
+  let(:unsolved_challenge_response) do
+    parse_json('ee/spec/fixtures/arkose/failed_ec_response.json')
+  end
+
+  let(:low_risk_response) do
+    parse_json('ee/spec/fixtures/arkose/successfully_solved_ec_response.json')
+  end
+
+  let(:high_risk_response) do
+    parse_json('ee/spec/fixtures/arkose/successfully_solved_ec_response_high_risk.json')
+  end
+
+  let(:allowlisted_response) do
+    parse_json('ee/spec/fixtures/arkose/allowlisted_response.json')
+  end
+
+  describe '#invalid_token?' do
+    subject { described_class.new(json_response).invalid_token? }
+
+    context 'when token is invalid' do
+      let(:json_response) { invalid_token_response }
+
+      it { is_expected.to eq true }
+    end
+
+    context 'when token is valid' do
+      let(:json_response) { unsolved_challenge_response }
+
+      it { is_expected.to eq false }
+    end
+  end
+
+  describe '#error' do
+    let(:json_response) { invalid_token_response }
+
+    subject { described_class.new(json_response).error }
+
+    it { is_expected.to eq 'DENIED ACCESS' }
+  end
+
+  describe '#challenge_solved?' do
+    subject { described_class.new(json_response).challenge_solved? }
+
+    context 'when response does not contain solved data' do
+      let(:json_response) { Gitlab::Json.parse("{}") }
+
+      it { is_expected.to eq true }
+    end
+
+    context 'when response contains solved data' do
+      let(:json_response) { unsolved_challenge_response }
+
+      it { is_expected.to eq false }
+    end
+  end
+
+  describe '#low_risk?' do
+    subject { described_class.new(json_response).low_risk? }
+
+    context 'when arkose_labs_prevent_login feature flag is disabled' do
+      let(:json_response) { Gitlab::Json.parse("{}") }
+
+      before do
+        stub_feature_flags(arkose_labs_prevent_login: false)
+      end
+
+      it { is_expected.to eq true }
+    end
+
+    context 'when response does not contain session_risk.risk_band data' do
+      let(:json_response) { Gitlab::Json.parse("{}") }
+
+      it { is_expected.to eq true }
+    end
+
+    context 'when response contains session_risk.risk_band != "High"' do
+      let(:json_response) { low_risk_response }
+
+      it { is_expected.to eq true }
+    end
+
+    context 'when response contains session_risk.risk_band == "High"' do
+      let(:json_response) { high_risk_response }
+
+      it { is_expected.to eq false }
+    end
+  end
+
+  describe '#allowlisted?' do
+    subject { described_class.new(json_response).allowlisted? }
+
+    context 'when session_details.telltale_list data includes ALLOWLIST_TELLTALE' do
+      let(:json_response) { allowlisted_response }
+
+      it { is_expected.to eq true }
+    end
+
+    context 'when session_details.telltale_list data does not include ALLOWLIST_TELLTALE' do
+      let(:json_response) { high_risk_response }
+
+      it { is_expected.to eq false }
+    end
+
+    context 'when response does not include session_details.telltale_list data' do
+      let(:json_response) { Gitlab::Json.parse("{}") }
+
+      it { is_expected.to eq false }
+    end
+  end
+
+  describe 'other methods' do
+    using RSpec::Parameterized::TableSyntax
+
+    subject(:response) { described_class.new(json_response) }
+
+    context 'when response has the correct data' do
+      let(:global_telltale_list) do
+        [
+          { "name" => "g-h-cfp-1000000000", "weight" => "7" },
+          { "name" => "g-os-impersonation-win", "weight" => "8" }
+        ]
+      end
+
+      let(:custom_telltale_list) do
+        [
+          { "name" => "outdated-browser-customer-2", "weight" => "100" },
+          { "name" => "outdated-os-customer", "weight" => "100" }
+        ]
+      end
+
+      where(:method, :expected_value) do
+        :custom_score         | "100"
+        :global_score         | "15"
+        :risk_band            | "High"
+        :session_id           | "22612c147bb418c8.2570749403"
+        :risk_category        | "BOT-STD"
+        :global_telltale_list | lazy { global_telltale_list }
+        :custom_telltale_list | lazy { custom_telltale_list }
+      end
+
+      with_them do
+        let(:json_response) { high_risk_response }
+
+        it 'succeeds' do
+          expect(response.public_send(method)).to eq expected_value
+        end
+      end
+    end
+
+    context 'when response does not have the correct data' do
+      where(:method, :expected_value) do
+        :custom_score         | 0
+        :global_score         | 0
+        :risk_band            | 'Unavailable'
+        :session_id           | 'Unavailable'
+        :risk_category        | 'Unavailable'
+        :global_telltale_list | 'Unavailable'
+        :custom_telltale_list | 'Unavailable'
+      end
+
+      with_them do
+        let(:json_response) { Gitlab::Json.parse("{}") }
+
+        it 'succeeds' do
+          expect(response.public_send(method)).to eq expected_value
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/services/arkose/user_verification_service_spec.rb b/ee/spec/services/arkose/user_verification_service_spec.rb
index e5c901aad6cdac87f945be42142e10b5fda2c3a1..fbd4d9475e0ca4ce7cc040932cdbe7bceef3e008 100644
--- a/ee/spec/services/arkose/user_verification_service_spec.rb
+++ b/ee/spec/services/arkose/user_verification_service_spec.rb
@@ -91,7 +91,7 @@
               context 'when the session is allowlisted' do
                 let(:arkose_ec_response) do
                   json = Gitlab::Json.parse(File.read(Rails.root.join('ee/spec/fixtures/arkose/successfully_solved_ec_response.json')))
-                  json['session_details']['telltale_list'].push(Arkose::UserVerificationService::ALLOWLIST_TELLTALE)
+                  json['session_details']['telltale_list'].push(Arkose::VerifyResponse::ALLOWLIST_TELLTALE)
                   json
                 end