Commit 607b5569 authored by Marcos Rocha's avatar Marcos Rocha Committed by Stan Hu

Add ArkoseLabs verify request service

This MR adds a service to check if the user was verified by ArkoseLabs

Changelog: added
MR:
parent 95018e5b
---
name: arkose_labs_login_challenge
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82751
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356171
milestone: '14.10'
type: development
group: group::antiabuse
default_enabled: false
......@@ -989,6 +989,14 @@ Settings['prometheus'] ||= Settingslogic.new({})
Settings.prometheus['enabled'] ||= false
Settings.prometheus['server_address'] ||= nil
#
# Arkose settings
#
Settings['arkose'] ||= Settingslogic.new({})
Settings.arkose['public_key'] ||= ENV['ARKOSE_LABS_PUBLIC_KEY']
Settings.arkose['private_key'] ||= ENV['ARKOSE_LABS_PRIVATE_KEY']
Settings.arkose['verify_url'] ||= ENV['ARKOSE_LABS_VERIFY_URL']
#
# Shutdown settings
#
......
......@@ -69,5 +69,35 @@ module EE
super
end
override :check_captcha
def check_captcha
if ::Feature.enabled?(:arkose_labs_login_challenge, default_enabled: :yaml)
check_arkose_captcha
else
super
end
end
def check_arkose_captcha
return unless user_params[:password].present?
return unless params[:arkose_labs_token].present?
user = ::User.find_by_username(user_params[:login])
return unless user.present?
if Arkose::UserVerificationService.new(session_token: params[:arkose_labs_token], userid: user.id).execute
increment_successful_login_captcha_counter
else
increment_failed_login_captcha_counter
self.resource = resource_class.new
flash[:alert] = 'Login failed. Please retry from your primary device and network'
flash.delete :recaptcha_error
respond_with_navigational(resource) { render :new }
end
end
end
end
# frozen_string_literal: true
module Arkose
class UserVerificationService
attr_reader :url, :session_token, :userid
VERIFY_URL = 'http://verify-api.arkoselabs.com/api/v4/verify'
def initialize(session_token:, userid:)
@session_token = session_token
@userid = userid
end
def execute
response = Gitlab::HTTP.perform_request(Net::HTTP::Post, VERIFY_URL, body: body)
logger.info(build_message("Arkose verify response: #{response.parsed_response}"))
return false if invalid_token(response)
challenge_solved?(response) && low_risk?(response)
rescue StandardError => error
payload = { session_token: session_token, log_data: userid }
Gitlab::ExceptionLogFormatter.format!(error, payload)
Gitlab::ErrorTracking.track_exception(error)
logger.error(build_message("Error verifying user on Arkose: #{payload}"))
true
end
private
def body
{
private_key: Settings.arkose['private_key'],
session_token: session_token,
log_data: userid
}
end
def logger
Gitlab::AppLogger
end
def build_message(message)
Gitlab::ApplicationContext.current.merge(message: message)
end
def invalid_token(response)
response.parsed_response&.key?('error')
end
def challenge_solved?(response)
solved = response.parsed_response&.dig('session_details', 'solved')
solved.nil? ? true : solved
end
def low_risk?(response)
risk_band = response.parsed_response&.dig('session_risk', 'risk_band')
risk_band.present? ? risk_band != 'High' : true
end
end
end
......@@ -140,5 +140,40 @@ RSpec.describe SessionsController, :geo do
expect(flash[:alert]).to include('You are not allowed to log in using password')
end
end
context 'with Arkose reCAPTCHA' do
before do
stub_feature_flags(arkose_labs_login_challenge: true)
end
let(:user) { create(:user) }
let(:session_token) { '22612c147bb418c8.2570749403' }
let(:user_params) { { login: user.username, password: user.password } }
let(:params) { { arkose_labs_token: session_token, user: user_params } }
context 'when the user was verified by Arkose' do
it 'successfully logs in a user when reCAPTCHA is solved' do
allow_next_instance_of(Arkose::UserVerificationService) do |instance|
allow(instance).to receive(:execute).and_return(true)
end
post(:create, params: params, session: {})
expect(subject.current_user).to eq user
end
end
context 'when the user was not verified by Arkose' do
it 'successfully logs in a user when reCAPTCHA is solved' do
allow_next_instance_of(Arkose::UserVerificationService) do |instance|
allow(instance).to receive(:execute).and_return(false)
end
post(:create, params: params, session: {})
expect(response).to render_template(:new)
expect(flash[:alert]).to include 'Login failed. Please retry from your primary device and network'
expect(subject.current_user).to be_nil
end
end
end
end
end
{
"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": [
"eng-1362",
"eng-1362-game3-py-0."
],
"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"
}
}
{
"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": [
"eng-1362",
"eng-1362-game3-py-0."
],
"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"
}
}
{
"session_details": {
"solved": true,
"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": [
"eng-1362",
"eng-1362-game3-py-0."
],
"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"
},
"session_risk": {
"risk_band": "Low",
"global": {
"score": "0",
"telltales": []
},
"custom": {
"score": "0",
"telltales": []
}
}
}
{
"session_details": {
"solved": true,
"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": [
"eng-1362",
"eng-1362-game3-py-0."
],
"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"
},
"session_risk": {
"risk_category": "BOT-STD",
"risk_band": "High",
"global": {
"score": "15",
"telltales": [
{
"name": "g-h-cfp-1000000000",
"weight": "7"
},
{
"name": "g-os-impersonation-win",
"weight": "8"
}
]
},
"custom": {
"score": "100",
"telltales": [
{
"name": "outdated-browser-customer-2",
"weight": "100"
},
{
"name": "outdated-os-customer",
"weight": "100"
}
]
}
}
}
{
"session_details": {
"solved": true,
"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": [
"eng-1362",
"eng-1362-game3-py-0."
],
"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"
}
}
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Arkose::UserVerificationService do
let(:session_token) { '22612c147bb418c8.2570749403' }
let(:userid) { '1999' }
let(:service) { Arkose::UserVerificationService.new(session_token: session_token, userid: userid) }
let(:response) { instance_double(HTTParty::Response, success?: true, code: 200, parsed_response: arkose_ec_response) }
subject { service.execute }
describe '#execute' do
context 'when the user did not solve the challenge' do
let(:arkose_ec_response) { Gitlab::Json.parse(File.read(Rails.root.join('ee/spec/fixtures/arkose/failed_ec_response.json'))) }
it 'returns false' do
allow(Gitlab::HTTP).to receive(:perform_request).and_return(response)
expect(subject).to be_falsey
end
end
context 'when the user solved the challenge' do
context 'when the risk score is not high' do
let(:arkose_ec_response) { Gitlab::Json.parse(File.read(Rails.root.join('ee/spec/fixtures/arkose/successfully_solved_ec_response.json'))) }
it 'returns true' do
allow(Gitlab::HTTP).to receive(:perform_request).and_return(response)
expect(subject).to be_truthy
end
context 'when the risk score is high' do
let(:arkose_ec_response) { Gitlab::Json.parse(File.read(Rails.root.join('ee/spec/fixtures/arkose/successfully_solved_ec_response_high_risk.json'))) }
it 'returns false' do
allow(Gitlab::HTTP).to receive(:perform_request).and_return(response)
expect(subject).to be_falsey
end
end
end
end
context 'when the response does not include the risk session' do
context 'when the user solved the challenge' do
let(:arkose_ec_response) { Gitlab::Json.parse(File.read(Rails.root.join('ee/spec/fixtures/arkose/successfully_solved_ec_response_without_session_risk.json'))) }
it 'returns true' do
allow(Gitlab::HTTP).to receive(:perform_request).and_return(response)
expect(subject).to be_truthy
end
end
context 'when the user did not solve the challenge' do
let(:arkose_ec_response) { Gitlab::Json.parse(File.read(Rails.root.join('ee/spec/fixtures/arkose/failed_ec_response_without_risk_session.json'))) }
it 'returns false' do
allow(Gitlab::HTTP).to receive(:perform_request).and_return(response)
expect(subject).to be_falsey
end
end
end
context 'when an error occurs during the Arkose request' do
it 'returns true' do
allow(Gitlab::HTTP).to receive(:perform_request).and_raise(Gitlab::HTTP::BlockedUrlError)
expect(subject).to be_truthy
end
end
end
end
......@@ -193,6 +193,10 @@ RSpec.describe SessionsController do
end
context 'with reCAPTCHA' do
before do
stub_feature_flags(arkose_labs_login_challenge: false)
end
def unsuccesful_login(user_params, sesion_params: {})
# Without this, `verify_recaptcha` arbitrarily returns true in test env
Recaptcha.configuration.skip_verify_env.delete('test')
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment