mirror of
https://github.com/discourse/discourse.git
synced 2024-12-16 05:33:54 +08:00
195 lines
5.6 KiB
Ruby
195 lines
5.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
|
|
|
|
attr_accessor :unhashed_auth_token
|
|
|
|
def self.log(info)
|
|
if SiteSetting.verbose_auth_token_logging
|
|
UserAuthTokenLog.create!(info)
|
|
end
|
|
end
|
|
|
|
def self.generate!(info)
|
|
token = SecureRandom.hex(16)
|
|
hashed_token = hash_token(token)
|
|
user_auth_token = UserAuthToken.create!(
|
|
user_id: info[:user_id],
|
|
user_agent: info[:user_agent],
|
|
client_ip: info[: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: info[:user_id],
|
|
user_agent: info[:user_agent],
|
|
client_ip: info[:client_ip],
|
|
path: info[:path],
|
|
auth_token: hashed_token)
|
|
|
|
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 OR
|
|
(auth_token = :unhashed_token AND legacy)) AND rotated_at > :expire_before",
|
|
token: token, unhashed_token: unhashed_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 = UserAuthToken.exec_sql("
|
|
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.cmdtuples > 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
|
|
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
|
|
# legacy :boolean default(FALSE), not null
|
|
# client_ip :inet
|
|
# rotated_at :datetime not null
|
|
# created_at :datetime
|
|
# updated_at :datetime
|
|
# 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
|
|
#
|