mirror of
https://github.com/discourse/discourse.git
synced 2025-01-18 16:52:45 +08:00
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:
parent
2dec731da3
commit
ff49f72ad9
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -13,6 +13,7 @@ module Jobs
|
|||
ScoreCalculator.new.calculate
|
||||
SchedulerStat.purge_old
|
||||
Draft.cleanup!
|
||||
UserAuthToken.cleanup!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
115
app/models/user_auth_token.rb
Normal file
115
app/models/user_auth_token.rb
Normal 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
|
||||
#
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
|
|
43
db/migrate/20170124181409_add_user_auth_tokens.rb
Normal file
43
db/migrate/20170124181409_add_user_auth_tokens.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
123
spec/models/user_auth_token_spec.rb
Normal file
123
spec/models/user_auth_token_spec.rb
Normal 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
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user