FEATURE: per client user tokens

Revamped system for managing authentication tokens.

- Every user has 1 token per client (web browser)
- Tokens are rotated every 10 minutes

New system migrates the old tokens to "legacy" tokens,
so users still remain logged on.

Also introduces weekly job to expire old auth tokens.
This commit is contained in:
Sam 2017-01-31 17:21:37 -05:00
parent 2dec731da3
commit ff49f72ad9
19 changed files with 495 additions and 106 deletions

View File

@ -72,8 +72,7 @@ class Admin::UsersController < Admin::AdminController
def log_out def log_out
if @user if @user
@user.auth_token = nil @user.user_auth_tokens.destroy_all
@user.save!
@user.logged_out @user.logged_out
render json: success_json render json: success_json
else else

View File

@ -417,7 +417,7 @@ class UsersController < ApplicationController
else else
@user.password = params[:password] @user.password = params[:password]
@user.password_required! @user.password_required!
@user.auth_token = nil @user.user_auth_tokens.destroy_all
if @user.save if @user.save
Invite.invalidate_for_email(@user.email) # invite link can't be used to log in anymore Invite.invalidate_for_email(@user.email) # invite link can't be used to log in anymore
secure_session["password-#{token}"] = nil secure_session["password-#{token}"] = nil
@ -701,7 +701,7 @@ class UsersController < ApplicationController
private private
def honeypot_value def honeypot_value
Digest::SHA1::hexdigest("#{Discourse.current_hostname}:#{Discourse::Application.config.secret_token}")[0,15] Digest::SHA1::hexdigest("#{Discourse.current_hostname}:#{GlobalSetting.safe_secret_key_base}")[0,15]
end end
def challenge_value def challenge_value

View File

@ -13,6 +13,7 @@ module Jobs
ScoreCalculator.new.calculate ScoreCalculator.new.calculate
SchedulerStat.purge_old SchedulerStat.purge_old
Draft.cleanup! Draft.cleanup!
UserAuthToken.cleanup!
end end
end end
end end

View File

@ -6,6 +6,35 @@ class GlobalSetting
end end
end end
VALID_SECRET_KEY = /^[0-9a-f]{128}$/
# this is named SECRET_TOKEN as opposed to SECRET_KEY_BASE
# for legacy reasons
REDIS_SECRET_KEY = 'SECRET_TOKEN'
# In Rails secret_key_base is used to encrypt the cookie store
# the cookie store contains session data
# Discourse also uses this secret key to digest user auth tokens
# This method will
# - use existing token if already set in ENV or discourse.conf
# - generate a token on the fly if needed and cache in redis
# - enforce rules about token format falling back to redis if needed
def self.safe_secret_key_base
@safe_secret_key_base ||= begin
token = secret_key_base
if token.blank? || token !~ VALID_SECRET_KEY
token = $redis.without_namespace.get(REDIS_SECRET_KEY)
unless token && token =~ VALID_SECRET_KEY
token = SecureRandom.hex(64)
$redis.without_namespace.set(REDIS_SECRET_KEY,token)
end
end
if !secret_key_base.blank? && token != secret_key_base
STDERR.puts "WARNING: DISCOURSE_SECRET_KEY_BASE is invalid, it was re-generated"
end
token
end
end
def self.load_defaults def self.load_defaults
default_provider = FileProvider.from(File.expand_path('../../../config/discourse_defaults.conf', __FILE__)) default_provider = FileProvider.from(File.expand_path('../../../config/discourse_defaults.conf', __FILE__))
default_provider.keys.concat(@provider.keys).uniq.each do |key| default_provider.keys.concat(@provider.keys).uniq.each do |key|

View File

@ -41,6 +41,7 @@ class User < ActiveRecord::Base
has_many :user_archived_messages, dependent: :destroy has_many :user_archived_messages, dependent: :destroy
has_many :email_change_requests, dependent: :destroy has_many :email_change_requests, dependent: :destroy
has_many :directory_items, dependent: :delete_all has_many :directory_items, dependent: :delete_all
has_many :user_auth_tokens, dependent: :destroy
has_one :user_option, dependent: :destroy has_one :user_option, dependent: :destroy
@ -97,6 +98,7 @@ class User < ActiveRecord::Base
before_save :update_username_lower before_save :update_username_lower
before_save :ensure_password_is_hashed before_save :ensure_password_is_hashed
after_save :expire_tokens_if_password_changed
after_save :automatic_group_membership after_save :automatic_group_membership
after_save :clear_global_notice_if_needed after_save :clear_global_notice_if_needed
after_save :refresh_avatar after_save :refresh_avatar
@ -420,7 +422,6 @@ class User < ActiveRecord::Base
# special case for passwordless accounts # special case for passwordless accounts
unless password.blank? unless password.blank?
@raw_password = password @raw_password = password
self.auth_token = nil
end end
end end
@ -971,6 +972,18 @@ class User < ActiveRecord::Base
end end
end end
def expire_tokens_if_password_changed
# NOTE: setting raw password is the only valid way of changing a password
# the password field in the DB is actually hashed, nobody should be amending direct
if @raw_password
# Association in model may be out-of-sync
UserAuthToken.where(user_id: id).destroy_all
# We should not carry this around after save
@raw_password = nil
end
end
def hash_password(password, salt) def hash_password(password, salt)
raise StandardError.new("password is too long") if password.size > User.max_password_length raise StandardError.new("password is too long") if password.size > User.max_password_length
Pbkdf2.hash_password(password, salt, Rails.configuration.pbkdf2_iterations, Rails.configuration.pbkdf2_algorithm) Pbkdf2.hash_password(password, salt, Rails.configuration.pbkdf2_iterations, Rails.configuration.pbkdf2_algorithm)
@ -1076,7 +1089,7 @@ end
# username :string(60) not null # username :string(60) not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# name :string # name :string(255)
# seen_notification_id :integer default(0), not null # seen_notification_id :integer default(0), not null
# last_posted_at :datetime # last_posted_at :datetime
# email :string(513) not null # email :string(513) not null
@ -1101,7 +1114,7 @@ end
# ip_address :inet # ip_address :inet
# moderator :boolean default(FALSE) # moderator :boolean default(FALSE)
# blocked :boolean default(FALSE) # blocked :boolean default(FALSE)
# title :string # title :string(255)
# uploaded_avatar_id :integer # uploaded_avatar_id :integer
# locale :string(10) # locale :string(10)
# primary_group_id :integer # primary_group_id :integer

View File

@ -0,0 +1,115 @@
# 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
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 created_at > :expire_before",
token: token, unhashed_token: unhashed_token, expire_before: expire_before)
if user_token &&
user_token.auth_token_seen &&
user_token.prev_auth_token == token &&
user_token.prev_auth_token != user_token.auth_token &&
user_token.rotated_at > 1.minute.ago
return nil
end
if mark_seen && user_token && !user_token.auth_token_seen && user_token.auth_token == token
user_token.update_columns(auth_token_seen: true)
end
user_token
end
def self.hash_token(token)
Digest::SHA1.base64digest("#{token}#{GlobalSetting.safe_secret_key_base}")
end
def self.cleanup!
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,
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
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
#

View File

@ -147,3 +147,6 @@ relative_url_root =
# this ensures backlog (ability of channels to catch up are capped) # this ensures backlog (ability of channels to catch up are capped)
# message bus default cap is 1000, we are winding it down to 100 # message bus default cap is 1000, we are winding it down to 100
message_bus_max_backlog_size = 100 message_bus_max_backlog_size = 100
# must be a 64 byte hex string, anything else will be ignored with a warning
secret_key_base =

View File

@ -1,14 +1,4 @@
# We have had lots of config issues with SECRET_TOKEN to avoid this mess we are moving it to redis # Not fussed setting secret_token anymore, that is only required for
# if you feel strongly that it does not belong there use ENV['SECRET_TOKEN'] # backwards support of "seamless" upgrade from Rails 3.
# # Discourse has shipped Rails 3 for a very long time.
token = ENV['SECRET_TOKEN'] Discourse::Application.config.secret_key_base = GlobalSetting.safe_secret_key_base
unless token
token = $redis.get('SECRET_TOKEN')
unless token && token.length == 128
token = SecureRandom.hex(64)
$redis.set('SECRET_TOKEN',token)
end
end
Discourse::Application.config.secret_token = token
Discourse::Application.config.secret_key_base = token

View File

@ -0,0 +1,43 @@
class AddUserAuthTokens < ActiveRecord::Migration
def down
add_column :users, :auth_token, :string
add_column :users, :auth_token_updated_at, :datetime
execute <<SQL
UPDATE users
SET auth_token = user_auth_tokens.auth_token,
auth_token_updated_at = user_auth_tokens.created_at
FROM user_auth_tokens
WHERE legacy AND user_auth_tokens.user_id = users.id
SQL
drop_table :user_auth_tokens
end
def up
create_table :user_auth_tokens do |t|
t.integer :user_id, null: false
t.string :auth_token, null: false
t.string :prev_auth_token, null: false
t.string :user_agent
t.boolean :auth_token_seen, default: false, null: false
t.boolean :legacy, default: false, null: false
t.inet :client_ip
t.datetime :rotated_at, null: false
t.timestamps
end
add_index :user_auth_tokens, [:auth_token]
add_index :user_auth_tokens, [:prev_auth_token]
execute <<SQL
INSERT INTO user_auth_tokens(user_id, auth_token, prev_auth_token, legacy, created_at, rotated_at)
SELECT id, auth_token, auth_token, true, auth_token_updated_at, auth_token_updated_at
FROM users
WHERE auth_token_updated_at IS NOT NULL AND auth_token IS NOT NULL
SQL
remove_column :users, :auth_token
remove_column :users, :auth_token_updated_at
end
end

View File

@ -44,10 +44,8 @@ class Auth::DefaultCurrentUserProvider
limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN ,60) limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN ,60)
if limiter.can_perform? if limiter.can_perform?
current_user = User.where(auth_token: auth_token) @user_token = UserAuthToken.lookup(auth_token, seen: true)
.where('auth_token_updated_at IS NULL OR auth_token_updated_at > ?', current_user = @user_token.try(:user)
SiteSetting.maximum_session_age.hours.ago)
.first
end end
unless current_user unless current_user
@ -105,35 +103,46 @@ class Auth::DefaultCurrentUserProvider
end end
def refresh_session(user, session, cookies) def refresh_session(user, session, cookies)
return if is_api?
if user && (!user.auth_token_updated_at || user.auth_token_updated_at <= 1.hour.ago) # if user was not loaded, no point refreshing session
user.update_column(:auth_token_updated_at, Time.zone.now) # it could be an anonymous path, this would add cost
cookies[TOKEN_COOKIE] = cookie_hash(user) return if is_api? || !@env.key?(CURRENT_USER_KEY)
if @user_token && @user_token.user == user
rotated_at = @user_token.rotated_at
needs_rotation = @user_token.auth_token_seen ? rotated_at < UserAuthToken::ROTATE_TIME.ago : rotated_at < UserAuthToken::URGENT_ROTATE_TIME.ago
if !@user_token.legacy && needs_rotation
if @user_token.rotate!(user_agent: @env['HTTP_USER_AGENT'],
client_ip: @request.ip)
cookies[TOKEN_COOKIE] = cookie_hash(@user_token.unhashed_auth_token)
end end
elsif @user_token.legacy
# make a new token
log_on_user(user, session, cookies)
end
end
if !user && cookies.key?(TOKEN_COOKIE) if !user && cookies.key?(TOKEN_COOKIE)
cookies.delete(TOKEN_COOKIE) cookies.delete(TOKEN_COOKIE)
end end
end end
def log_on_user(user, session, cookies) def log_on_user(user, session, cookies)
legit_token = user.auth_token && user.auth_token.length == 32 @user_token = UserAuthToken.generate!(user_id: user.id,
expired_token = user.auth_token_updated_at && user.auth_token_updated_at < SiteSetting.maximum_session_age.hours.ago user_agent: @env['HTTP_USER_AGENT'],
client_ip: @request.ip)
if !legit_token || expired_token cookies[TOKEN_COOKIE] = cookie_hash(@user_token.unhashed_auth_token)
user.update_columns(auth_token: SecureRandom.hex(16),
auth_token_updated_at: Time.zone.now)
end
cookies[TOKEN_COOKIE] = cookie_hash(user)
make_developer_admin(user) make_developer_admin(user)
enable_bootstrap_mode(user) enable_bootstrap_mode(user)
@env[CURRENT_USER_KEY] = user @env[CURRENT_USER_KEY] = user
end end
def cookie_hash(user) def cookie_hash(unhashed_auth_token)
{ {
value: user.auth_token, value: unhashed_auth_token,
httponly: true, httponly: true,
expires: SiteSetting.maximum_session_age.hours.from_now, expires: SiteSetting.maximum_session_age.hours.from_now,
secure: SiteSetting.force_https secure: SiteSetting.force_https
@ -155,9 +164,9 @@ class Auth::DefaultCurrentUserProvider
end end
def log_off_user(session, cookies) def log_off_user(session, cookies)
if SiteSetting.log_out_strict && (user = current_user) user = current_user
user.auth_token = nil if SiteSetting.log_out_strict && user
user.save! user.user_auth_tokens.destroy_all
if user.admin && defined?(Rack::MiniProfiler) if user.admin && defined?(Rack::MiniProfiler)
# clear the profiling cookie to keep stuff tidy # clear the profiling cookie to keep stuff tidy
@ -165,6 +174,8 @@ class Auth::DefaultCurrentUserProvider
end end
user.logged_out user.logged_out
elsif user && @user_token
@user_token.destroy
end end
cookies.delete(TOKEN_COOKIE) cookies.delete(TOKEN_COOKIE)
end end

View File

@ -1,6 +1,7 @@
require_dependency 'wizard/step' require_dependency 'wizard/step'
require_dependency 'wizard/field' require_dependency 'wizard/field'
require_dependency 'wizard/step_updater' require_dependency 'wizard/step_updater'
require_dependency 'wizard/builder'
class Wizard class Wizard
attr_reader :steps, :user attr_reader :steps, :user
@ -76,11 +77,10 @@ class Wizard
def requires_completion? def requires_completion?
return false unless SiteSetting.wizard_enabled? return false unless SiteSetting.wizard_enabled?
first_admin = User.where(admin: true) first_admin = User.where(admin: true)
.where.not(id: Discourse.system_user.id) .where.not(id: Discourse.system_user.id)
.where.not(auth_token_updated_at: nil) .joins(:user_auth_tokens)
.order(:auth_token_updated_at) .order('user_auth_tokens.created_at')
if @user.present? && first_admin.first == @user && (Topic.count < 15) if @user.present? && first_admin.first == @user && (Topic.count < 15)
!Wizard::Builder.new(@user).build.completed? !Wizard::Builder.new(@user).build.completed?

View File

@ -3,10 +3,17 @@ require_dependency 'auth/default_current_user_provider'
describe Auth::DefaultCurrentUserProvider do describe Auth::DefaultCurrentUserProvider do
class TestProvider < Auth::DefaultCurrentUserProvider
attr_reader :env
def initialize(env)
super(env)
end
end
def provider(url, opts=nil) def provider(url, opts=nil)
opts ||= {method: "GET"} opts ||= {method: "GET"}
env = Rack::MockRequest.env_for(url, opts) env = Rack::MockRequest.env_for(url, opts)
Auth::DefaultCurrentUserProvider.new(env) TestProvider.new(env)
end end
it "raises errors for incorrect api_key" do it "raises errors for incorrect api_key" do
@ -73,21 +80,83 @@ describe Auth::DefaultCurrentUserProvider do
expect(provider("/topic/anything/goes", method: "GET").should_update_last_seen?).to eq(true) expect(provider("/topic/anything/goes", method: "GET").should_update_last_seen?).to eq(true)
end end
it "correctly renews session once an hour" do it "correctly supports legacy tokens" do
user = Fabricate(:user)
token = SecureRandom.hex(16)
user_token = UserAuthToken.create!(user_id: user.id, auth_token: token,
prev_auth_token: token, legacy: true,
rotated_at: Time.zone.now
)
prov = provider("/", "HTTP_COOKIE" => "_t=#{user_token.auth_token}")
expect(prov.current_user.id).to eq(user.id)
# sets a new token up cause it got a global token
cookies = {}
prov.refresh_session(user, {}, cookies)
user.reload
expect(user.user_auth_tokens.count).to eq(2)
expect(cookies["_t"][:value]).not_to eq(token)
end
it "correctly rotates tokens" do
SiteSetting.maximum_session_age = 3 SiteSetting.maximum_session_age = 3
user = Fabricate(:user) user = Fabricate(:user)
provider('/').log_on_user(user, {}, {}) @provider = provider('/')
freeze_time 2.hours.from_now
cookies = {} cookies = {}
provider("/", "HTTP_COOKIE" => "_t=#{user.auth_token}").refresh_session(user, {}, cookies) @provider.log_on_user(user, {}, cookies)
unhashed_token = cookies["_t"][:value]
token = UserAuthToken.find_by(user_id: user.id)
expect(token.auth_token_seen).to eq(false)
expect(token.auth_token).not_to eq(unhashed_token)
expect(token.auth_token).to eq(UserAuthToken.hash_token(unhashed_token))
# at this point we are going to try to rotate token
freeze_time 20.minutes.from_now
provider2 = provider("/", "HTTP_COOKIE" => "_t=#{unhashed_token}")
provider2.current_user
token.reload
expect(token.auth_token_seen).to eq(true)
cookies = {}
provider2.refresh_session(user, {}, cookies)
expect(cookies["_t"][:value]).not_to eq(unhashed_token)
token.reload
expect(token.auth_token_seen).to eq(false)
freeze_time 21.minutes.from_now
old_token = token.prev_auth_token
unverified_token = token.auth_token
# old token should still work
provider2 = provider("/", "HTTP_COOKIE" => "_t=#{unhashed_token}")
expect(provider2.current_user.id).to eq(user.id)
provider2.refresh_session(user, {}, cookies)
token.reload
# because this should cause a rotation since we can safely
# assume it never reached the client
expect(token.prev_auth_token).to eq(old_token)
expect(token.auth_token).not_to eq(unverified_token)
expect(user.auth_token_updated_at - Time.now).to eq(0)
end end
it "can only try 10 bad cookies a minute" do it "can only try 10 bad cookies a minute" do
user = Fabricate(:user) user = Fabricate(:user)
token = UserAuthToken.generate!(user_id: user.id)
provider('/').log_on_user(user, {}, {}) provider('/').log_on_user(user, {}, {})
RateLimiter.stubs(:disabled?).returns(false) RateLimiter.stubs(:disabled?).returns(false)
@ -107,12 +176,15 @@ describe Auth::DefaultCurrentUserProvider do
}.to raise_error(Discourse::InvalidAccess) }.to raise_error(Discourse::InvalidAccess)
expect { expect {
env["HTTP_COOKIE"] = "_t=#{user.auth_token}" env["HTTP_COOKIE"] = "_t=#{token.unhashed_auth_token}"
provider("/", env).current_user provider("/", env).current_user
}.to raise_error(Discourse::InvalidAccess) }.to raise_error(Discourse::InvalidAccess)
env["REMOTE_ADDR"] = "10.0.0.2" env["REMOTE_ADDR"] = "10.0.0.2"
expect {
provider('/', env).current_user provider('/', env).current_user
}.not_to raise_error
end end
it "correctly removes invalid cookies" do it "correctly removes invalid cookies" do
@ -123,35 +195,26 @@ describe Auth::DefaultCurrentUserProvider do
expect(cookies.key?("_t")).to eq(false) expect(cookies.key?("_t")).to eq(false)
end end
it "recycles existing auth_token correctly" do it "logging on user always creates a new token" do
SiteSetting.maximum_session_age = 3
user = Fabricate(:user) user = Fabricate(:user)
provider('/').log_on_user(user, {}, {})
original_auth_token = user.auth_token
freeze_time 2.hours.from_now
provider('/').log_on_user(user, {}, {})
user.reload
expect(user.auth_token).to eq(original_auth_token)
freeze_time 10.hours.from_now
provider('/').log_on_user(user, {}, {}) provider('/').log_on_user(user, {}, {})
user.reload provider('/').log_on_user(user, {}, {})
expect(user.auth_token).not_to eq(original_auth_token)
expect(UserAuthToken.where(user_id: user.id).count).to eq(2)
end end
it "correctly expires session" do it "correctly expires session" do
SiteSetting.maximum_session_age = 2 SiteSetting.maximum_session_age = 2
user = Fabricate(:user) user = Fabricate(:user)
token = UserAuthToken.generate!(user_id: user.id)
provider('/').log_on_user(user, {}, {}) provider('/').log_on_user(user, {}, {})
expect(provider("/", "HTTP_COOKIE" => "_t=#{user.auth_token}").current_user.id).to eq(user.id) expect(provider("/", "HTTP_COOKIE" => "_t=#{token.unhashed_auth_token}").current_user.id).to eq(user.id)
freeze_time 3.hours.from_now freeze_time 3.hours.from_now
expect(provider("/", "HTTP_COOKIE" => "_t=#{user.auth_token}").current_user).to eq(nil) expect(provider("/", "HTTP_COOKIE" => "_t=#{token.unhashed_auth_token}").current_user).to eq(nil)
end end
context "user api" do context "user api" do

View File

@ -3,10 +3,10 @@ require_dependency 'current_user'
describe CurrentUser do describe CurrentUser do
it "allows us to lookup a user from our environment" do it "allows us to lookup a user from our environment" do
user = Fabricate(:user, auth_token: EmailToken.generate_token, active: true) user = Fabricate(:user, active: true)
EmailToken.confirm(user.auth_token) token = UserAuthToken.generate!(user_id: user.id)
env = Rack::MockRequest.env_for("/test", "HTTP_COOKIE" => "_t=#{user.auth_token};") env = Rack::MockRequest.env_for("/test", "HTTP_COOKIE" => "_t=#{token.unhashed_auth_token};")
expect(CurrentUser.lookup_from_env(env)).to eq(user) expect(CurrentUser.lookup_from_env(env)).to eq(user)
end end

View File

@ -122,7 +122,8 @@ describe Wizard do
it "it's true for the first admin who logs in" do it "it's true for the first admin who logs in" do
admin = Fabricate(:admin) admin = Fabricate(:admin)
second_admin = Fabricate(:admin, auth_token_updated_at: Time.now) second_admin = Fabricate(:admin)
UserAuthToken.generate!(user_id: second_admin.id)
expect(build_simple(admin).requires_completion?).to eq(false) expect(build_simple(admin).requires_completion?).to eq(false)
expect(build_simple(second_admin).requires_completion?).to eq(true) expect(build_simple(second_admin).requires_completion?).to eq(true)

View File

@ -505,8 +505,8 @@ describe SessionController do
user.reload user.reload
expect(session[:current_user_id]).to eq(user.id) expect(session[:current_user_id]).to eq(user.id)
expect(user.auth_token).to be_present expect(user.user_auth_tokens.count).to eq(1)
expect(cookies[:_t]).to eq(user.auth_token) expect(UserAuthToken.hash_token(cookies[:_t])).to eq(user.user_auth_tokens.first.auth_token)
end end
end end

View File

@ -155,21 +155,13 @@ describe UsersController do
put :perform_account_activation, token: 'asdfasdf' put :perform_account_activation, token: 'asdfasdf'
end end
it 'returns success' do it 'correctly logs on user' do
expect(response).to be_success expect(response).to be_success
end
it "doesn't set an error" do
expect(flash[:error]).to be_blank expect(flash[:error]).to be_blank
end
it 'logs in as the user' do
expect(session[:current_user_id]).to be_present expect(session[:current_user_id]).to be_present
end
it "doesn't set @needs_approval" do
expect(assigns[:needs_approval]).to be_blank expect(assigns[:needs_approval]).to be_blank
end end
end end
context 'user is not approved' do context 'user is not approved' do
@ -241,7 +233,7 @@ describe UsersController do
render_views render_views
it 'renders referrer never on get requests' do it 'renders referrer never on get requests' do
user = Fabricate(:user, auth_token: SecureRandom.hex(16)) user = Fabricate(:user)
token = user.email_tokens.create(email: user.email).token token = user.email_tokens.create(email: user.email).token
get :password_reset, token: token get :password_reset, token: token
@ -250,25 +242,25 @@ describe UsersController do
end end
it 'returns success' do it 'returns success' do
user = Fabricate(:user, auth_token: SecureRandom.hex(16)) user = Fabricate(:user)
user_auth_token = UserAuthToken.generate!(user_id: user.id)
token = user.email_tokens.create(email: user.email).token token = user.email_tokens.create(email: user.email).token
old_token = user.auth_token
get :password_reset, token: token get :password_reset, token: token
put :password_reset, token: token, password: 'hg9ow8yhg98o' put :password_reset, token: token, password: 'hg9ow8yhg98o'
expect(response).to be_success expect(response).to be_success
expect(assigns[:error]).to be_blank expect(assigns[:error]).to be_blank
user.reload user.reload
expect(user.auth_token).to_not eq old_token
expect(user.auth_token.length).to eq 32
expect(session["password-#{token}"]).to be_blank expect(session["password-#{token}"]).to be_blank
expect(UserAuthToken.where(id: user_auth_token.id).count).to eq(0)
end end
it 'disallows double password reset' do it 'disallows double password reset' do
user = Fabricate(:user)
user = Fabricate(:user, auth_token: SecureRandom.hex(16))
token = user.email_tokens.create(email: user.email).token token = user.email_tokens.create(email: user.email).token
get :password_reset, token: token get :password_reset, token: token
@ -277,10 +269,13 @@ describe UsersController do
user.reload user.reload
expect(user.confirm_password?('hg9ow8yhg98o')).to eq(true) expect(user.confirm_password?('hg9ow8yhg98o')).to eq(true)
# logged in now
expect(user.user_auth_tokens.count).to eq(1)
end end
it "redirects to the wizard if you're the first admin" do it "redirects to the wizard if you're the first admin" do
user = Fabricate(:admin, auth_token: SecureRandom.hex(16), auth_token_updated_at: Time.now) user = Fabricate(:admin)
token = user.email_tokens.create(email: user.email).token token = user.email_tokens.create(email: user.email).token
get :password_reset, token: token get :password_reset, token: token
put :password_reset, token: token, password: 'hg9ow8yhg98oadminlonger' put :password_reset, token: token, password: 'hg9ow8yhg98oadminlonger'
@ -288,13 +283,17 @@ describe UsersController do
end end
it "doesn't invalidate the token when loading the page" do it "doesn't invalidate the token when loading the page" do
user = Fabricate(:user, auth_token: SecureRandom.hex(16)) user = Fabricate(:user)
user_token = UserAuthToken.generate!(user_id: user.id)
email_token = user.email_tokens.create(email: user.email) email_token = user.email_tokens.create(email: user.email)
get :password_reset, token: email_token.token get :password_reset, token: email_token.token
email_token.reload email_token.reload
expect(email_token.confirmed).to eq(false) expect(email_token.confirmed).to eq(false)
expect(UserAuthToken.where(id: user_token.id).count).to eq(1)
end end
end end

View File

@ -0,0 +1,123 @@
require 'rails_helper'
describe UserAuthToken do
it "can remove old expired tokens" do
freeze_time Time.zone.now
SiteSetting.maximum_session_age = 1
user = Fabricate(:user)
token = UserAuthToken.generate!(user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3")
freeze_time 1.hour.from_now
UserAuthToken.cleanup!
expect(UserAuthToken.where(id: token.id).count).to eq(1)
freeze_time 1.second.from_now
UserAuthToken.cleanup!
expect(UserAuthToken.where(id: token.id).count).to eq(1)
freeze_time UserAuthToken::ROTATE_TIME.from_now
UserAuthToken.cleanup!
expect(UserAuthToken.where(id: token.id).count).to eq(0)
end
it "can lookup both hashed and unhashed" do
user = Fabricate(:user)
token = UserAuthToken.generate!(user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3")
lookup_token = UserAuthToken.lookup(token.unhashed_auth_token)
expect(user.id).to eq(lookup_token.user.id)
lookup_token = UserAuthToken.lookup(token.auth_token)
expect(lookup_token).to eq(nil)
token.update_columns(legacy: true)
lookup_token = UserAuthToken.lookup(token.auth_token)
expect(user.id).to eq(lookup_token.user.id)
end
it "can validate token was seen at lookup time" do
user = Fabricate(:user)
user_token = UserAuthToken.generate!(user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3")
expect(user_token.auth_token_seen).to eq(false)
UserAuthToken.lookup(user_token.unhashed_auth_token, seen: true)
user_token.reload
expect(user_token.auth_token_seen).to eq(true)
end
it "can rotate with no params maintaining data" do
user = Fabricate(:user)
user_token = UserAuthToken.generate!(user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3")
user_token.update_columns(auth_token_seen: true)
expect(user_token.rotate!).to eq(true)
user_token.reload
expect(user_token.client_ip.to_s).to eq("1.1.2.3")
expect(user_token.user_agent).to eq("some user agent 2")
end
it "can properly rotate tokens" do
user = Fabricate(:user)
user_token = UserAuthToken.generate!(user_id: user.id,
user_agent: "some user agent 2",
client_ip: "1.1.2.3")
prev_auth_token = user_token.auth_token
unhashed_prev = user_token.unhashed_auth_token
rotated = user_token.rotate!(user_agent: "a new user agent", client_ip: "1.1.2.4")
expect(rotated).to eq(false)
user_token.update_columns(auth_token_seen: true)
rotated = user_token.rotate!(user_agent: "a new user agent", client_ip: "1.1.2.4")
expect(rotated).to eq(true)
user_token.reload
expect(user_token.rotated_at).to be_within(5.second).of(Time.zone.now)
expect(user_token.client_ip).to eq("1.1.2.4")
expect(user_token.user_agent).to eq("a new user agent")
expect(user_token.auth_token_seen).to eq(false)
expect(user_token.prev_auth_token).to eq(prev_auth_token)
# ability to auth using an old token
looked_up = UserAuthToken.lookup(unhashed_prev)
expect(looked_up.id).to eq(user_token.id)
freeze_time(2.minute.from_now) do
looked_up = UserAuthToken.lookup(unhashed_prev)
expect(looked_up).to eq(nil)
end
end
end

View File

@ -592,21 +592,18 @@ describe User do
@user.password = "ilovepasta" @user.password = "ilovepasta"
@user.save! @user.save!
@user.auth_token = SecureRandom.hex(16)
@user.save!
expect(@user.active).to eq(false) expect(@user.active).to eq(false)
expect(@user.confirm_password?("ilovepasta")).to eq(true) expect(@user.confirm_password?("ilovepasta")).to eq(true)
email_token = @user.email_tokens.create(email: 'pasta@delicious.com') email_token = @user.email_tokens.create(email: 'pasta@delicious.com')
old_token = @user.auth_token UserAuthToken.generate!(user_id: @user.id)
@user.password = "passwordT" @user.password = "passwordT"
@user.save! @user.save!
# must expire old token on password change # must expire old token on password change
expect(@user.auth_token).to_not eq(old_token) expect(@user.user_auth_tokens.count).to eq(0)
email_token.reload email_token.reload
expect(email_token.expired).to eq(true) expect(email_token.expired).to eq(true)

View File

@ -4,7 +4,7 @@ describe UserAnonymizer do
describe "make_anonymous" do describe "make_anonymous" do
let(:admin) { Fabricate(:admin) } let(:admin) { Fabricate(:admin) }
let(:user) { Fabricate(:user, username: "edward", auth_token: "mysecretauthtoken") } let(:user) { Fabricate(:user, username: "edward") }
subject(:make_anonymous) { described_class.make_anonymous(user, admin) } subject(:make_anonymous) { described_class.make_anonymous(user, admin) }
@ -51,6 +51,8 @@ describe UserAnonymizer do
prev_username = user.username prev_username = user.username
UserAuthToken.generate!(user_id: user.id)
make_anonymous make_anonymous
user.reload user.reload
@ -58,7 +60,7 @@ describe UserAnonymizer do
expect(user.name).not_to be_present expect(user.name).not_to be_present
expect(user.date_of_birth).to eq(nil) expect(user.date_of_birth).to eq(nil)
expect(user.title).not_to be_present expect(user.title).not_to be_present
expect(user.auth_token).to eq(nil) expect(user.user_auth_tokens.count).to eq(0)
profile = user.user_profile(true) profile = user.user_profile(true)
expect(profile.location).to eq(nil) expect(profile.location).to eq(nil)