discourse/app/models/user_auth_token.rb
Sam 99f4d5082b FIX: Improve token rotation and increase logging
- avoid access denied on bad cookie, instead just nuke it
- avoid marking a token unseen for first minute post rotation
- log path in user auth token logs
2017-03-07 13:27:43 -05:00

189 lines
5.4 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
# user_agent :string
# auth_token_seen :boolean default(FALSE), not null
# legacy :boolean default(FALSE), not null
# client_ip :inet
# rotated_at :datetime
# created_at :datetime
# updated_at :datetime
#