users: refactor login and authentication logic.
* Make authentication methods into User instance methods instead of class methods. * Fix API key authentication to use a secure string comparison. Fixes a hypothetical (unlikely to be exploitable) timing attack. * Move login logic from SessionCreator to SessionLoader.
This commit is contained in:
@@ -14,7 +14,7 @@ class EmailsController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
@user = authorize User.find(params[:user_id]), policy_class: EmailAddressPolicy
|
@user = authorize User.find(params[:user_id]), policy_class: EmailAddressPolicy
|
||||||
|
|
||||||
if User.authenticate(@user.name, params[:user][:password])
|
if @user.authenticate_password(params[:user][:password])
|
||||||
@user.update(email_address_attributes: { address: params[:user][:email] })
|
@user.update(email_address_attributes: { address: params[:user][:email] })
|
||||||
else
|
else
|
||||||
@user.errors[:base] << "Password was incorrect"
|
@user.errors[:base] << "Password was incorrect"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ module Maintenance
|
|||||||
end
|
end
|
||||||
|
|
||||||
def authenticate!
|
def authenticate!
|
||||||
if ::User.authenticate(CurrentUser.user.name, params[:user][:password]) == CurrentUser.user
|
if CurrentUser.user.authenticate_password(params[:user][:password])
|
||||||
@api_key = CurrentUser.user.api_key || ApiKey.generate!(CurrentUser.user)
|
@api_key = CurrentUser.user.api_key || ApiKey.generate!(CurrentUser.user)
|
||||||
@password = params[:user][:password]
|
@password = params[:user][:password]
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ class PasswordsController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
@user = authorize User.find(params[:user_id]), policy_class: PasswordPolicy
|
@user = authorize User.find(params[:user_id]), policy_class: PasswordPolicy
|
||||||
|
|
||||||
if User.authenticate(@user.name, params[:user][:old_password])
|
if @user.authenticate_password(params[:user][:old_password]) || @user.authenticate_login_key(params[:user][:signed_user_id])
|
||||||
@user.update(password: params[:user][:password], password_confirmation: params[:user][:password_confirmation])
|
|
||||||
elsif @user.authenticate_login_key(params[:user][:signed_user_id])
|
|
||||||
@user.update(password: params[:user][:password], password_confirmation: params[:user][:password_confirmation])
|
@user.update(password: params[:user][:password], password_confirmation: params[:user][:password_confirmation])
|
||||||
else
|
else
|
||||||
@user.errors[:base] << "Incorrect password"
|
@user.errors[:base] << "Incorrect password"
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ class SessionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
session_params = params[:session].presence || params
|
name, password, url = params.fetch(:session, params).slice(:name, :password, :url).values
|
||||||
session_creator = SessionCreator.new(session, session_params[:name], session_params[:password], request.remote_ip)
|
user = SessionLoader.new(request).login(name, password)
|
||||||
|
|
||||||
if session_creator.authenticate
|
if user
|
||||||
url = session_params[:url]
|
url = posts_path unless url&.start_with?("/")
|
||||||
url = posts_path if !url&.start_with?("/")
|
respond_with(user, location: url, methods: [:api_token])
|
||||||
respond_with(session_creator.user, location: url, methods: [:api_token])
|
|
||||||
else
|
else
|
||||||
flash.now[:notice] = "Password was incorrect"
|
flash.now[:notice] = "Password was incorrect"
|
||||||
raise SessionLoader::AuthenticationFailure
|
raise SessionLoader::AuthenticationFailure
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
class SessionCreator
|
|
||||||
attr_reader :session, :name, :password, :ip_addr
|
|
||||||
attr_reader :user
|
|
||||||
|
|
||||||
def initialize(session, name, password, ip_addr)
|
|
||||||
@session = session
|
|
||||||
@name = name
|
|
||||||
@password = password
|
|
||||||
@ip_addr = ip_addr
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate
|
|
||||||
if User.authenticate(name, password)
|
|
||||||
@user = User.find_by_name(name)
|
|
||||||
|
|
||||||
session[:user_id] = @user.id
|
|
||||||
@user.update_column(:last_ip_addr, ip_addr)
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -9,6 +9,15 @@ class SessionLoader
|
|||||||
@params = request.parameters
|
@params = request.parameters
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def login(name, password)
|
||||||
|
user = User.find_by_name(name)&.authenticate_password(password)
|
||||||
|
return nil unless user
|
||||||
|
|
||||||
|
session[:user_id] = user.id
|
||||||
|
user.update_column(:last_ip_addr, request.remote_ip)
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
def load
|
def load
|
||||||
CurrentUser.user = User.anonymous
|
CurrentUser.user = User.anonymous
|
||||||
CurrentUser.ip_addr = request.remote_ip
|
CurrentUser.ip_addr = request.remote_ip
|
||||||
@@ -50,8 +59,6 @@ class SessionLoader
|
|||||||
authenticate_api_key(params[:login], params[:api_key])
|
authenticate_api_key(params[:login], params[:api_key])
|
||||||
elsif params[:login].present? && params[:password_hash].present?
|
elsif params[:login].present? && params[:password_hash].present?
|
||||||
authenticate_legacy_api_key(params[:login], params[:password_hash])
|
authenticate_legacy_api_key(params[:login], params[:password_hash])
|
||||||
else
|
|
||||||
raise AuthenticationFailure
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -63,11 +70,8 @@ class SessionLoader
|
|||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_api_key(name, api_key)
|
def authenticate_api_key(name, api_key)
|
||||||
CurrentUser.user = User.authenticate_api_key(name, api_key)
|
CurrentUser.user = User.find_by_name(name)&.authenticate_api_key(api_key)
|
||||||
|
raise AuthenticationFailure unless Currentuser.user.present?
|
||||||
if CurrentUser.user.nil?
|
|
||||||
raise AuthenticationFailure.new
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_legacy_api_key(name, password_hash)
|
def authenticate_legacy_api_key(name, password_hash)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class UserDeletion
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate
|
def validate
|
||||||
if !User.authenticate(user.name, password)
|
if !user.authenticate_password(password)
|
||||||
raise ValidationError.new("Password is incorrect")
|
raise ValidationError.new("Password is incorrect")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -160,38 +160,29 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module PasswordMethods
|
concerning :AuthenticationMethods do
|
||||||
def bcrypt_password
|
|
||||||
BCrypt::Password.new(bcrypt_password_hash)
|
|
||||||
end
|
|
||||||
|
|
||||||
def password=(new_password)
|
def password=(new_password)
|
||||||
@password = new_password
|
@password = new_password
|
||||||
self.bcrypt_password_hash = User.bcrypt(new_password)
|
self.bcrypt_password_hash = BCrypt::Password.create(hash_password(new_password))
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
module AuthenticationMethods
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
def authenticate_login_key(signed_user_id)
|
def authenticate_login_key(signed_user_id)
|
||||||
signed_user_id.present? && id == Danbooru::MessageVerifier.new(:login).verify(signed_user_id)
|
signed_user_id.present? && id == Danbooru::MessageVerifier.new(:login).verify(signed_user_id) && self
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_api_key(key)
|
||||||
|
api_key.present? && ActiveSupport::SecurityUtils.secure_compare(api_key.key, key) && self
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_password(password)
|
||||||
|
BCrypt::Password.new(bcrypt_password_hash) == hash_password(password) && self
|
||||||
|
end
|
||||||
|
|
||||||
|
def hash_password(password)
|
||||||
|
Digest::SHA1.hexdigest("choujin-steiner--#{password}--")
|
||||||
end
|
end
|
||||||
|
|
||||||
module ClassMethods
|
module ClassMethods
|
||||||
def authenticate(name, pass)
|
|
||||||
authenticate_hash(name, sha1(pass))
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate_api_key(name, api_key)
|
|
||||||
key = ApiKey.where(:key => api_key).first
|
|
||||||
return nil if key.nil?
|
|
||||||
user = find_by_name(name)
|
|
||||||
return nil if user.nil?
|
|
||||||
return user if key.user_id == user.id
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate_hash(name, hash)
|
def authenticate_hash(name, hash)
|
||||||
user = find_by_name(name)
|
user = find_by_name(name)
|
||||||
if user && user.bcrypt_password == hash
|
if user && user.bcrypt_password == hash
|
||||||
@@ -200,15 +191,6 @@ class User < ApplicationRecord
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def bcrypt(pass)
|
|
||||||
BCrypt::Password.create(sha1(pass))
|
|
||||||
end
|
|
||||||
|
|
||||||
def sha1(pass)
|
|
||||||
salt = "choujin-steiner"
|
|
||||||
Digest::SHA1.hexdigest("#{salt}--#{pass}--")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -610,8 +592,6 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
include BanMethods
|
include BanMethods
|
||||||
include PasswordMethods
|
|
||||||
include AuthenticationMethods
|
|
||||||
include LevelMethods
|
include LevelMethods
|
||||||
include EmailMethods
|
include EmailMethods
|
||||||
include BlacklistMethods
|
include BlacklistMethods
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
put_auth user_password_path(@user), @user, params: { user: { old_password: "12345", password: "abcde", password_confirmation: "abcde" } }
|
put_auth user_password_path(@user), @user, params: { user: { old_password: "12345", password: "abcde", password_confirmation: "abcde" } }
|
||||||
|
|
||||||
assert_redirected_to @user
|
assert_redirected_to @user
|
||||||
assert_equal(nil, User.authenticate(@user.name, "12345"))
|
assert_equal(false, @user.reload.authenticate_password("12345"))
|
||||||
assert_equal(@user, User.authenticate(@user.name, "abcde"))
|
assert_equal(@user, @user.authenticate_password("abcde"))
|
||||||
end
|
end
|
||||||
|
|
||||||
should "update the password when given a valid login key" do
|
should "update the password when given a valid login key" do
|
||||||
@@ -27,24 +27,24 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
put_auth user_password_path(@user), @user, params: { user: { password: "abcde", password_confirmation: "abcde", signed_user_id: signed_user_id } }
|
put_auth user_password_path(@user), @user, params: { user: { password: "abcde", password_confirmation: "abcde", signed_user_id: signed_user_id } }
|
||||||
|
|
||||||
assert_redirected_to @user
|
assert_redirected_to @user
|
||||||
assert_equal(nil, User.authenticate(@user.name, "12345"))
|
assert_equal(false, @user.reload.authenticate_password("12345"))
|
||||||
assert_equal(@user, User.authenticate(@user.name, "abcde"))
|
assert_equal(@user, @user.authenticate_password("abcde"))
|
||||||
end
|
end
|
||||||
|
|
||||||
should "not update the password when given an invalid old password" do
|
should "not update the password when given an invalid old password" do
|
||||||
put_auth user_password_path(@user), @user, params: { user: { old_password: "3qoirjqe", password: "abcde", password_confirmation: "abcde" } }
|
put_auth user_password_path(@user), @user, params: { user: { old_password: "3qoirjqe", password: "abcde", password_confirmation: "abcde" } }
|
||||||
|
|
||||||
assert_response :success
|
assert_response :success
|
||||||
assert_equal(@user, User.authenticate(@user.name, "12345"))
|
assert_equal(@user, @user.reload.authenticate_password("12345"))
|
||||||
assert_equal(nil, User.authenticate(@user.name, "abcde"))
|
assert_equal(false, @user.authenticate_password("abcde"))
|
||||||
end
|
end
|
||||||
|
|
||||||
should "not update the password when password confirmation fails for the new password" do
|
should "not update the password when password confirmation fails for the new password" do
|
||||||
put_auth user_password_path(@user), @user, params: { user: { old_password: "12345", password: "abcde", password_confirmation: "qerogijqe" } }
|
put_auth user_password_path(@user), @user, params: { user: { old_password: "12345", password: "abcde", password_confirmation: "qerogijqe" } }
|
||||||
|
|
||||||
assert_response :success
|
assert_response :success
|
||||||
assert_equal(@user, User.authenticate(@user.name, "12345"))
|
assert_equal(@user, @user.reload.authenticate_password("12345"))
|
||||||
assert_equal(nil, User.authenticate(@user.name, "abcde"))
|
assert_equal(false, @user.authenticate_password("abcde"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ require 'test_helper'
|
|||||||
class SessionsControllerTest < ActionDispatch::IntegrationTest
|
class SessionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
context "the sessions controller" do
|
context "the sessions controller" do
|
||||||
setup do
|
setup do
|
||||||
@user = create(:user)
|
@user = create(:user, password: "password")
|
||||||
end
|
end
|
||||||
|
|
||||||
context "new action" do
|
context "new action" do
|
||||||
@@ -14,15 +14,27 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
context "create action" do
|
context "create action" do
|
||||||
should "create a new session" do
|
should "log the user in when given the correct password" do
|
||||||
post session_path, params: {:name => @user.name, :password => "password"}
|
post session_path, params: { name: @user.name, password: "password" }
|
||||||
|
|
||||||
assert_redirected_to posts_path
|
assert_redirected_to posts_path
|
||||||
assert_equal(@user.id, session[:user_id])
|
assert_equal(@user.id, session[:user_id])
|
||||||
assert_not_nil(@user.reload.last_ip_addr)
|
assert_not_nil(@user.reload.last_ip_addr)
|
||||||
end
|
end
|
||||||
|
|
||||||
should "not allow IP banned users to create a new session" do
|
should "not log the user in when given an incorrect password" do
|
||||||
|
post session_path, params: { name: @user.name, password: "wrong"}
|
||||||
|
|
||||||
|
assert_response 401
|
||||||
|
assert_nil(nil, session[:user_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
should "redirect the user when given an url param" do
|
||||||
|
post session_path, params: { name: @user.name, password: "password", url: tags_path }
|
||||||
|
assert_redirected_to tags_path
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not allow IP banned users to login" do
|
||||||
create(:ip_ban, ip_addr: "1.2.3.4")
|
create(:ip_ban, ip_addr: "1.2.3.4")
|
||||||
post session_path, params: { name: @user.name, password: "password" }, headers: { REMOTE_ADDR: "1.2.3.4" }
|
post session_path, params: { name: @user.name, password: "password" }, headers: { REMOTE_ADDR: "1.2.3.4" }
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
assert_redirected_to User.last
|
assert_redirected_to User.last
|
||||||
assert_equal("xxx", User.last.name)
|
assert_equal("xxx", User.last.name)
|
||||||
assert_equal(User.last, User.authenticate("xxx", "xxxxx1"))
|
assert_equal(User.last, User.last.authenticate_password("xxxxx1"))
|
||||||
assert_equal(nil, User.last.email_address)
|
assert_equal(nil, User.last.email_address)
|
||||||
assert_no_enqueued_emails
|
assert_no_enqueued_emails
|
||||||
end
|
end
|
||||||
@@ -128,7 +128,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
assert_redirected_to User.last
|
assert_redirected_to User.last
|
||||||
assert_equal("xxx", User.last.name)
|
assert_equal("xxx", User.last.name)
|
||||||
assert_equal(User.last, User.authenticate("xxx", "xxxxx1"))
|
assert_equal(User.last, User.last.authenticate_password("xxxxx1"))
|
||||||
assert_equal("webmaster@danbooru.donmai.us", User.last.email_address.address)
|
assert_equal("webmaster@danbooru.donmai.us", User.last.email_address.address)
|
||||||
assert_enqueued_email_with UserMailer, :welcome_user, args: [User.last]
|
assert_enqueued_email_with UserMailer, :welcome_user, args: [User.last]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,15 +18,15 @@ class ApiKeyTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
should "authenticate via api key" do
|
should "authenticate via api key" do
|
||||||
assert_not_nil(User.authenticate_api_key(@user.name, @api_key.key))
|
assert_equal(@user, @user.authenticate_api_key(@api_key.key))
|
||||||
end
|
end
|
||||||
|
|
||||||
should "not authenticate with the wrong api key" do
|
should "not authenticate with the wrong api key" do
|
||||||
assert_nil(User.authenticate_api_key(@user.name, "xxx"))
|
assert_equal(false, @user.authenticate_api_key("xxx"))
|
||||||
end
|
end
|
||||||
|
|
||||||
should "not authenticate with the wrong name" do
|
should "not authenticate with the wrong name" do
|
||||||
assert_nil(User.authenticate_api_key("xxx", @api_key.key))
|
assert_equal(false, create(:user).authenticate_api_key(@api_key.key))
|
||||||
end
|
end
|
||||||
|
|
||||||
should "have the same limits whether or not they have an api key" do
|
should "have the same limits whether or not they have an api key" do
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class UserDeletionTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
should "reset the password" do
|
should "reset the password" do
|
||||||
@deletion.delete!
|
@deletion.delete!
|
||||||
assert_nil(User.authenticate(@user.name, "password"))
|
assert_equal(false, @user.authenticate_password("password"))
|
||||||
end
|
end
|
||||||
|
|
||||||
should "remove any favorites" do
|
should "remove any favorites" do
|
||||||
|
|||||||
@@ -39,11 +39,9 @@ class UserTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
should "authenticate" do
|
should "authenticate password" do
|
||||||
assert(User.authenticate(@user.name, "password"), "Authentication should have succeeded")
|
assert_equal(@user, @user.authenticate_password("password"))
|
||||||
assert(!User.authenticate(@user.name, "password2"), "Authentication should not have succeeded")
|
assert_equal(false, @user.authenticate_password("password2"))
|
||||||
assert(User.authenticate_hash(@user.name, User.sha1("password")), "Authentication should have succeeded")
|
|
||||||
assert(!User.authenticate_hash(@user.name, User.sha1("xxx")), "Authentication should not have succeeded")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
should "normalize its level" do
|
should "normalize its level" do
|
||||||
|
|||||||
Reference in New Issue
Block a user