mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 19:05:18 +08:00
59097b207f
Over the years we accrued many spelling mistakes in the code base. This PR attempts to fix spelling mistakes and typos in all areas of the code that are extremely safe to change - comments - test descriptions - other low risk areas
257 lines
7.6 KiB
Ruby
257 lines
7.6 KiB
Ruby
# frozen_string_literal: true
|
|
require 'digest/sha1'
|
|
|
|
class UserAuthToken < ActiveRecord::Base
|
|
belongs_to :user
|
|
|
|
ROTATE_TIME = 10.minutes
|
|
# used when token did not arrive at client
|
|
URGENT_ROTATE_TIME = 1.minute
|
|
|
|
MAX_SESSION_COUNT = 60
|
|
|
|
USER_ACTIONS = ['generate']
|
|
|
|
attr_accessor :unhashed_auth_token
|
|
|
|
before_destroy do
|
|
UserAuthToken.log(action: 'destroy',
|
|
user_auth_token_id: self.id,
|
|
user_id: self.user_id,
|
|
user_agent: self.user_agent,
|
|
client_ip: self.client_ip,
|
|
auth_token: self.auth_token)
|
|
end
|
|
|
|
def self.log(info)
|
|
if SiteSetting.verbose_auth_token_logging
|
|
UserAuthTokenLog.create!(info)
|
|
end
|
|
end
|
|
|
|
RAD_PER_DEG = Math::PI / 180
|
|
EARTH_RADIUS_KM = 6371 # kilometers
|
|
|
|
def self.login_location(ip)
|
|
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
|
|
|
|
def self.is_suspicious(user_id, user_ip)
|
|
return false unless User.find_by(id: user_id)&.staff?
|
|
|
|
ips = UserAuthTokenLog.where(user_id: user_id).pluck(:client_ip)
|
|
ips.delete_at(ips.index(user_ip) || ips.length) # delete one occurrence (current)
|
|
ips.uniq!
|
|
return false if ips.empty? # first login is never suspicious
|
|
|
|
if user_location = login_location(user_ip)
|
|
ips.none? do |ip|
|
|
if location = login_location(ip)
|
|
distance(user_location, location) < SiteSetting.max_suspicious_distance_km
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.generate!(user_id: , user_agent: nil, client_ip: nil, path: nil, staff: nil, impersonate: false)
|
|
token = SecureRandom.hex(16)
|
|
hashed_token = hash_token(token)
|
|
user_auth_token = UserAuthToken.create!(
|
|
user_id: user_id,
|
|
user_agent: user_agent,
|
|
client_ip: client_ip,
|
|
auth_token: hashed_token,
|
|
prev_auth_token: hashed_token,
|
|
rotated_at: Time.zone.now
|
|
)
|
|
user_auth_token.unhashed_auth_token = token
|
|
|
|
log(action: 'generate',
|
|
user_auth_token_id: user_auth_token.id,
|
|
user_id: user_id,
|
|
user_agent: user_agent,
|
|
client_ip: client_ip,
|
|
path: path,
|
|
auth_token: hashed_token)
|
|
|
|
if staff && !impersonate
|
|
Jobs.enqueue(:suspicious_login,
|
|
user_id: user_id,
|
|
client_ip: client_ip,
|
|
user_agent: user_agent)
|
|
end
|
|
|
|
user_auth_token
|
|
end
|
|
|
|
def self.lookup(unhashed_token, opts = nil)
|
|
mark_seen = opts && opts[:seen]
|
|
|
|
token = hash_token(unhashed_token)
|
|
expire_before = SiteSetting.maximum_session_age.hours.ago
|
|
|
|
user_token = find_by("(auth_token = :token OR
|
|
prev_auth_token = :token) AND rotated_at > :expire_before",
|
|
token: token, expire_before: expire_before)
|
|
|
|
if !user_token
|
|
|
|
log(action: "miss token",
|
|
user_id: user_token&.user_id,
|
|
auth_token: token,
|
|
user_agent: opts && opts[:user_agent],
|
|
path: opts && opts[:path],
|
|
client_ip: opts && opts[:client_ip])
|
|
|
|
return nil
|
|
end
|
|
|
|
if user_token.auth_token != token && user_token.prev_auth_token == token && user_token.auth_token_seen
|
|
changed_rows = UserAuthToken
|
|
.where("rotated_at < ?", 1.minute.ago)
|
|
.where(id: user_token.id, prev_auth_token: token)
|
|
.update_all(auth_token_seen: false)
|
|
|
|
# not updating AR model cause we want to give it one more req
|
|
# with wrong cookie
|
|
UserAuthToken.log(
|
|
action: changed_rows == 0 ? "prev seen token unchanged" : "prev seen token",
|
|
user_auth_token_id: user_token.id,
|
|
user_id: user_token.user_id,
|
|
auth_token: user_token.auth_token,
|
|
user_agent: opts && opts[:user_agent],
|
|
path: opts && opts[:path],
|
|
client_ip: opts && opts[:client_ip]
|
|
)
|
|
end
|
|
|
|
if mark_seen && user_token && !user_token.auth_token_seen && user_token.auth_token == token
|
|
# we must protect against concurrency issues here
|
|
changed_rows = UserAuthToken
|
|
.where(id: user_token.id, auth_token: token)
|
|
.update_all(auth_token_seen: true, seen_at: Time.zone.now)
|
|
|
|
if changed_rows == 1
|
|
# not doing a reload so we don't risk loading a rotated token
|
|
user_token.auth_token_seen = true
|
|
user_token.seen_at = Time.zone.now
|
|
end
|
|
|
|
log(action: changed_rows == 0 ? "seen wrong token" : "seen token",
|
|
user_auth_token_id: user_token.id,
|
|
user_id: user_token.user_id,
|
|
auth_token: user_token.auth_token,
|
|
user_agent: opts && opts[:user_agent],
|
|
path: opts && opts[:path],
|
|
client_ip: opts && opts[:client_ip])
|
|
end
|
|
|
|
user_token
|
|
end
|
|
|
|
def self.hash_token(token)
|
|
Digest::SHA1.base64digest("#{token}#{GlobalSetting.safe_secret_key_base}")
|
|
end
|
|
|
|
def self.cleanup!
|
|
|
|
if SiteSetting.verbose_auth_token_logging
|
|
UserAuthTokenLog.where('created_at < :time',
|
|
time: SiteSetting.maximum_session_age.hours.ago - ROTATE_TIME).delete_all
|
|
end
|
|
|
|
where('rotated_at < :time',
|
|
time: SiteSetting.maximum_session_age.hours.ago - ROTATE_TIME).delete_all
|
|
|
|
end
|
|
|
|
def rotate!(info = nil)
|
|
user_agent = (info && info[:user_agent] || self.user_agent)
|
|
client_ip = (info && info[:client_ip] || self.client_ip)
|
|
|
|
token = SecureRandom.hex(16)
|
|
|
|
result = DB.exec("
|
|
UPDATE user_auth_tokens
|
|
SET
|
|
auth_token_seen = false,
|
|
seen_at = null,
|
|
user_agent = :user_agent,
|
|
client_ip = :client_ip,
|
|
prev_auth_token = case when auth_token_seen then auth_token else prev_auth_token end,
|
|
auth_token = :new_token,
|
|
rotated_at = :now
|
|
WHERE id = :id AND (auth_token_seen or rotated_at < :safeguard_time)
|
|
", id: self.id,
|
|
user_agent: user_agent,
|
|
client_ip: client_ip&.to_s,
|
|
now: Time.zone.now,
|
|
new_token: UserAuthToken.hash_token(token),
|
|
safeguard_time: 30.seconds.ago
|
|
)
|
|
|
|
if result > 0
|
|
reload
|
|
self.unhashed_auth_token = token
|
|
|
|
UserAuthToken.log(
|
|
action: "rotate",
|
|
user_auth_token_id: id,
|
|
user_id: user_id,
|
|
auth_token: auth_token,
|
|
user_agent: user_agent,
|
|
client_ip: client_ip,
|
|
path: info && info[:path]
|
|
)
|
|
|
|
true
|
|
else
|
|
false
|
|
end
|
|
|
|
end
|
|
|
|
def self.enforce_session_count_limit!(user_id)
|
|
tokens_to_destroy = where(user_id: user_id).
|
|
where('rotated_at > ?', SiteSetting.maximum_session_age.hours.ago).
|
|
order("rotated_at DESC").offset(MAX_SESSION_COUNT)
|
|
|
|
tokens_to_destroy.delete_all # Returns the number of deleted rows
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: user_auth_tokens
|
|
#
|
|
# id :integer not null, primary key
|
|
# user_id :integer not null
|
|
# auth_token :string not null
|
|
# prev_auth_token :string not null
|
|
# user_agent :string
|
|
# auth_token_seen :boolean default(FALSE), not null
|
|
# client_ip :inet
|
|
# rotated_at :datetime not null
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# seen_at :datetime
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_user_auth_tokens_on_auth_token (auth_token) UNIQUE
|
|
# index_user_auth_tokens_on_prev_auth_token (prev_auth_token) UNIQUE
|
|
# index_user_auth_tokens_on_user_id (user_id)
|
|
#
|