FEATURE: Compute distance between logins to generate login alerts. (#6562)
This commit is contained in:
parent
f6fb079129
commit
fce0a0ccc8
|
@ -30,23 +30,23 @@ class UserAuthToken < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the login location as it will be used by the the system to detect
|
RAD_PER_DEG = Math::PI / 180
|
||||||
# suspicious login.
|
EARTH_RADIUS_KM = 6371 # kilometers
|
||||||
#
|
|
||||||
# This should not be very specific because small variations in location
|
|
||||||
# (i.e. changes of network, small trips, etc) will be detected as suspicious
|
|
||||||
# logins.
|
|
||||||
#
|
|
||||||
# On the other hand, if this is too broad it will not report any suspicious
|
|
||||||
# logins at all.
|
|
||||||
#
|
|
||||||
# For example, let's choose the country as the only component in login
|
|
||||||
# locations. In general, this should be a pretty good choce with the
|
|
||||||
# exception that for users from huge countries it might not be specific
|
|
||||||
# enoguh. For US users where the real user and the malicious one could
|
|
||||||
# happen to live both in USA, this will not detect any suspicious activity.
|
|
||||||
def self.login_location(ip)
|
def self.login_location(ip)
|
||||||
DiscourseIpInfo.get(ip)[:country]
|
ipinfo = DiscourseIpInfo.get(ip)
|
||||||
|
|
||||||
|
ipinfo['latitude'] && ipinfo['longitude'] ? [ipinfo['latitude'], ipinfo['longitude']] : nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.distance(loc1, loc2)
|
||||||
|
lat1_rad, lon1_rad = loc1[0] * RAD_PER_DEG, loc1[1] * RAD_PER_DEG
|
||||||
|
lat2_rad, lon2_rad = loc2[0] * RAD_PER_DEG, loc2[1] * RAD_PER_DEG
|
||||||
|
|
||||||
|
a = Math.sin((lat2_rad - lat1_rad) / 2)**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin((lon2_rad - lon1_rad) / 2)**2
|
||||||
|
c = 2 * Math::atan2(Math::sqrt(a), Math::sqrt(1 - a))
|
||||||
|
|
||||||
|
c * EARTH_RADIUS_KM
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.is_suspicious(user_id, user_ip)
|
def self.is_suspicious(user_id, user_ip)
|
||||||
|
@ -57,8 +57,13 @@ class UserAuthToken < ActiveRecord::Base
|
||||||
ips.uniq!
|
ips.uniq!
|
||||||
return false if ips.empty? # first login is never suspicious
|
return false if ips.empty? # first login is never suspicious
|
||||||
|
|
||||||
user_location = login_location(user_ip)
|
if user_location = login_location(user_ip)
|
||||||
ips.none? { |ip| user_location == login_location(ip) }
|
ips.none? do |ip|
|
||||||
|
if location = login_location(ip)
|
||||||
|
distance(user_location, location) < SiteSetting.max_suspicious_distance_km
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.generate!(user_id: , user_agent: nil, client_ip: nil, path: nil, staff: nil, impersonate: false)
|
def self.generate!(user_id: , user_agent: nil, client_ip: nil, path: nil, staff: nil, impersonate: false)
|
||||||
|
|
|
@ -381,6 +381,9 @@ login:
|
||||||
verbose_auth_token_logging:
|
verbose_auth_token_logging:
|
||||||
hidden: true
|
hidden: true
|
||||||
default: true
|
default: true
|
||||||
|
max_suspicious_distance_km:
|
||||||
|
hidden: true
|
||||||
|
default: 500
|
||||||
sso_url:
|
sso_url:
|
||||||
default: ''
|
default: ''
|
||||||
regex: '^https?:\/\/.+[^\/]$'
|
regex: '^https?:\/\/.+[^\/]$'
|
||||||
|
|
|
@ -4,10 +4,25 @@ describe Jobs::SuspiciousLogin do
|
||||||
|
|
||||||
let(:user) { Fabricate(:moderator) }
|
let(:user) { Fabricate(:moderator) }
|
||||||
|
|
||||||
|
let(:zurich) { [47.3686498, 8.5391825] } # Zurich, Switzerland
|
||||||
|
let(:bern) { [46.947922, 7.444608] } # Bern, Switzerland
|
||||||
|
let(:london) { [51.5073509, -0.1277583] } # London, United Kingdom
|
||||||
|
|
||||||
before do
|
before do
|
||||||
UserAuthToken.stubs(:login_location).with("1.1.1.1").returns("Location 1")
|
UserAuthToken.stubs(:login_location).with("1.1.1.1").returns(zurich)
|
||||||
UserAuthToken.stubs(:login_location).with("1.1.1.2").returns("Location 1")
|
UserAuthToken.stubs(:login_location).with("1.1.1.2").returns(bern)
|
||||||
UserAuthToken.stubs(:login_location).with("1.1.2.1").returns("Location 2")
|
UserAuthToken.stubs(:login_location).with("1.1.2.1").returns(london)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "will correctly compute distance" do
|
||||||
|
def expect_distance(from, to, distance)
|
||||||
|
expect(UserAuthToken.distance(from, to).to_i).to eq(distance)
|
||||||
|
expect(UserAuthToken.distance(to, from).to_i).to eq(distance)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect_distance(zurich, bern, 95)
|
||||||
|
expect_distance(zurich, london, 776)
|
||||||
|
expect_distance(bern, london, 747)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "will not send an email on first login" do
|
it "will not send an email on first login" do
|
||||||
|
|
Loading…
Reference in New Issue