Merge branch 'new_user_auth'

This commit is contained in:
Sam 2017-02-07 09:23:02 -05:00
commit 634755113a
19 changed files with 495 additions and 106 deletions

View File

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

View File

@ -417,7 +417,7 @@ class UsersController < ApplicationController
else
@user.password = params[:password]
@user.password_required!
@user.auth_token = nil
@user.user_auth_tokens.destroy_all
if @user.save
Invite.invalidate_for_email(@user.email) # invite link can't be used to log in anymore
secure_session["password-#{token}"] = nil
@ -701,7 +701,7 @@ class UsersController < ApplicationController
private
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
def challenge_value

View File

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

View File

@ -6,6 +6,35 @@ class GlobalSetting
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
default_provider = FileProvider.from(File.expand_path('../../../config/discourse_defaults.conf', __FILE__))
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 :email_change_requests, dependent: :destroy
has_many :directory_items, dependent: :delete_all
has_many :user_auth_tokens, dependent: :destroy
has_one :user_option, dependent: :destroy
@ -97,6 +98,7 @@ class User < ActiveRecord::Base
before_save :update_username_lower
before_save :ensure_password_is_hashed
after_save :expire_tokens_if_password_changed
after_save :automatic_group_membership
after_save :clear_global_notice_if_needed
after_save :refresh_avatar
@ -420,7 +422,6 @@ class User < ActiveRecord::Base
# special case for passwordless accounts
unless password.blank?
@raw_password = password
self.auth_token = nil
end
end
@ -971,6 +972,18 @@ class User < ActiveRecord::Base
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)
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)
@ -1076,7 +1089,7 @@ end
# username :string(60) not null
# created_at :datetime not null
# updated_at :datetime not null
# name :string
# name :string(255)
# seen_notification_id :integer default(0), not null
# last_posted_at :datetime
# email :string(513) not null
@ -1101,7 +1114,7 @@ end
# ip_address :inet
# moderator :boolean default(FALSE)
# blocked :boolean default(FALSE)
# title :string
# title :string(255)
# uploaded_avatar_id :integer
# locale :string(10)
# 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)
# message bus default cap is 1000, we are winding it down to 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
# if you feel strongly that it does not belong there use ENV['SECRET_TOKEN']
#
token = ENV['SECRET_TOKEN']
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
# Not fussed setting secret_token anymore, that is only required for
# backwards support of "seamless" upgrade from Rails 3.
# Discourse has shipped Rails 3 for a very long time.
Discourse::Application.config.secret_key_base = GlobalSetting.safe_secret_key_base

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

View File

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

View File

@ -3,10 +3,17 @@ require_dependency 'auth/default_current_user_provider'
describe Auth::DefaultCurrentUserProvider do
class TestProvider < Auth::DefaultCurrentUserProvider
attr_reader :env
def initialize(env)
super(env)
end
end
def provider(url, opts=nil)
opts ||= {method: "GET"}
env = Rack::MockRequest.env_for(url, opts)
Auth::DefaultCurrentUserProvider.new(env)
TestProvider.new(env)
end
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)
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
user = Fabricate(:user)
provider('/').log_on_user(user, {}, {})
freeze_time 2.hours.from_now
@provider = provider('/')
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
it "can only try 10 bad cookies a minute" do
user = Fabricate(:user)
token = UserAuthToken.generate!(user_id: user.id)
provider('/').log_on_user(user, {}, {})
RateLimiter.stubs(:disabled?).returns(false)
@ -107,12 +176,15 @@ describe Auth::DefaultCurrentUserProvider do
}.to raise_error(Discourse::InvalidAccess)
expect {
env["HTTP_COOKIE"] = "_t=#{user.auth_token}"
env["HTTP_COOKIE"] = "_t=#{token.unhashed_auth_token}"
provider("/", env).current_user
}.to raise_error(Discourse::InvalidAccess)
env["REMOTE_ADDR"] = "10.0.0.2"
provider('/', env).current_user
expect {
provider('/', env).current_user
}.not_to raise_error
end
it "correctly removes invalid cookies" do
@ -123,35 +195,26 @@ describe Auth::DefaultCurrentUserProvider do
expect(cookies.key?("_t")).to eq(false)
end
it "recycles existing auth_token correctly" do
SiteSetting.maximum_session_age = 3
it "logging on user always creates a new token" do
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, {}, {})
user.reload
expect(user.auth_token).not_to eq(original_auth_token)
provider('/').log_on_user(user, {}, {})
expect(UserAuthToken.where(user_id: user.id).count).to eq(2)
end
it "correctly expires session" do
SiteSetting.maximum_session_age = 2
user = Fabricate(:user)
token = UserAuthToken.generate!(user_id: user.id)
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
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
context "user api" do

View File

@ -3,10 +3,10 @@ require_dependency 'current_user'
describe CurrentUser do
it "allows us to lookup a user from our environment" do
user = Fabricate(:user, auth_token: EmailToken.generate_token, active: true)
EmailToken.confirm(user.auth_token)
user = Fabricate(:user, active: true)
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)
end

View File

@ -122,7 +122,8 @@ describe Wizard do
it "it's true for the first admin who logs in" do
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(second_admin).requires_completion?).to eq(true)

View File

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

View File

@ -155,21 +155,13 @@ describe UsersController do
put :perform_account_activation, token: 'asdfasdf'
end
it 'returns success' do
it 'correctly logs on user' do
expect(response).to be_success
end
it "doesn't set an error" do
expect(flash[:error]).to be_blank
end
it 'logs in as the user' do
expect(session[:current_user_id]).to be_present
end
it "doesn't set @needs_approval" do
expect(assigns[:needs_approval]).to be_blank
end
end
context 'user is not approved' do
@ -241,7 +233,7 @@ describe UsersController do
render_views
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
get :password_reset, token: token
@ -250,25 +242,25 @@ describe UsersController do
end
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
old_token = user.auth_token
get :password_reset, token: token
put :password_reset, token: token, password: 'hg9ow8yhg98o'
expect(response).to be_success
expect(assigns[:error]).to be_blank
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(UserAuthToken.where(id: user_auth_token.id).count).to eq(0)
end
it 'disallows double password reset' do
user = Fabricate(:user, auth_token: SecureRandom.hex(16))
user = Fabricate(:user)
token = user.email_tokens.create(email: user.email).token
get :password_reset, token: token
@ -277,10 +269,13 @@ describe UsersController do
user.reload
expect(user.confirm_password?('hg9ow8yhg98o')).to eq(true)
# logged in now
expect(user.user_auth_tokens.count).to eq(1)
end
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
get :password_reset, token: token
put :password_reset, token: token, password: 'hg9ow8yhg98oadminlonger'
@ -288,13 +283,17 @@ describe UsersController do
end
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)
get :password_reset, token: email_token.token
email_token.reload
expect(email_token.confirmed).to eq(false)
expect(UserAuthToken.where(id: user_token.id).count).to eq(1)
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.save!
@user.auth_token = SecureRandom.hex(16)
@user.save!
expect(@user.active).to eq(false)
expect(@user.confirm_password?("ilovepasta")).to eq(true)
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.save!
# 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
expect(email_token.expired).to eq(true)

View File

@ -4,7 +4,7 @@ describe UserAnonymizer do
describe "make_anonymous" do
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) }
@ -51,6 +51,8 @@ describe UserAnonymizer do
prev_username = user.username
UserAuthToken.generate!(user_id: user.id)
make_anonymous
user.reload
@ -58,7 +60,7 @@ describe UserAnonymizer do
expect(user.name).not_to be_present
expect(user.date_of_birth).to eq(nil)
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)
expect(profile.location).to eq(nil)