mirror of
https://github.com/discourse/discourse.git
synced 2024-12-01 03:33:39 +08:00
5f64fd0a21
Introduce new patterns for direct sql that are safe and fast. MiniSql is not prone to memory bloat that can happen with direct PG usage. It also has an extremely fast materializer and very a convenient API - DB.exec(sql, *params) => runs sql returns row count - DB.query(sql, *params) => runs sql returns usable objects (not a hash) - DB.query_hash(sql, *params) => runs sql returns an array of hashes - DB.query_single(sql, *params) => runs sql and returns a flat one dimensional array - DB.build(sql) => returns a sql builder See more at: https://github.com/discourse/mini_sql
196 lines
5.5 KiB
Ruby
196 lines
5.5 KiB
Ruby
# frozen_string_literal: true
|
|
require 'digest/sha1'
|
|
|
|
class UserAuthToken < ActiveRecord::Base
|
|
belongs_to :user
|
|
|
|
# TODO 2019: remove this line
|
|
self.ignored_columns = ["legacy"]
|
|
|
|
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) 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
|
|
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
|
|
#
|