mirror of
https://github.com/discourse/discourse.git
synced 2025-02-22 12:09:56 +08:00

This relaxes our security in the following way - prev auth token is always accepted as long as rotation date is within our window of SiteSetting.maximum_session_age.hours (previously old token expired within a minute of new one being seen) - new auth token is marked unseen if we are presented with an old token after we already saw new one This attempts to fix an issue where ios webkit is not committing new cookies
191 lines
5.4 KiB
Ruby
191 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.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
|
|
|
|
if SiteSetting.verbose_auth_token_logging
|
|
UserAuthTokenLog.create!(
|
|
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],
|
|
auth_token: hashed_token
|
|
)
|
|
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 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
|
|
|
|
if SiteSetting.verbose_auth_token_logging
|
|
UserAuthTokenLog.create(
|
|
action: "miss token",
|
|
user_id: user_token&.user_id,
|
|
auth_token: token,
|
|
user_agent: opts && opts[:user_agent],
|
|
client_ip: opts && opts[:client_ip]
|
|
)
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
if user_token.prev_auth_token == token && user_token.auth_token_seen
|
|
changed_rows = UserAuthToken
|
|
.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
|
|
UserAuthTokenLog.create(
|
|
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],
|
|
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
|
|
|
|
if SiteSetting.verbose_auth_token_logging
|
|
UserAuthTokenLog.create(
|
|
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],
|
|
client_ip: opts && opts[:client_ip]
|
|
)
|
|
end
|
|
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
|
|
|
|
if SiteSetting.verbose_auth_token_logging
|
|
UserAuthTokenLog.create(
|
|
action: "rotate",
|
|
user_auth_token_id: id,
|
|
user_id: user_id,
|
|
auth_token: auth_token,
|
|
user_agent: user_agent,
|
|
client_ip: client_ip
|
|
)
|
|
end
|
|
|
|
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
|
|
#
|