Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow backup field authorization for login & password reset #538

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions app/controllers/devise_token_auth/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,48 @@ def resource_class(m=nil)
mapping.to
end

def set_resource(default_identification)
@resource = nil
return unless default_identification.present?

resource_list = [[resource_class.name, default_identification]]
backup_identification = resource_params[:backup_field_name]
if backup_identification.present?
backup_class = resource_params[:backup_field_class]
backup_class ||= resource_class.name
resource_list << [backup_class, backup_identification]
end
resource_list.map!{ |tmp_array| tmp_array.map(&:to_s) }

resource_class_name = resource_class.name.parameterize.to_sym
resource_list.each do |(class_name, field_name)|
current_class = class_name.classify.constantize

field_key = ( field_name == 'uid' ) ? :email : field_name.to_sym
field_value = resource_params[field_name] || resource_params[:email]
field_value = field_value.downcase \
if resource_class.case_insensitive_keys.include?(field_key)

q = current_class
if ActiveRecord::Base.connection.adapter_name.downcase.starts_with? 'mysql'
q = q.where("BINARY #{field_name} = ?", field_value)
else
q = q.where(field_name => field_value)
end

if current_class == resource_class
@resource = q.find_by(provider: 'email')
else
q = q.joins(resource_class_name)
current_resource = q.find_by("#{resource_class.table_name}.provider" => 'email')
next unless current_resource.present?
@resource = current_resource.public_send(resource_class_name)
end

break if @resource.present?
end
end

def is_json_api
return false unless defined?(ActiveModel::Serializer)
return ActiveModel::Serializer.config.adapter == :json_api
Expand Down
24 changes: 8 additions & 16 deletions app/controllers/devise_token_auth/passwords_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,14 @@ def create
end
end

# honor devise configuration for case_insensitive_keys
if resource_class.case_insensitive_keys.include?(:email)
@email = resource_params[:email].downcase
else
@email = resource_params[:email]
end

q = "uid = ? AND provider='email'"

# fix for mysql default case insensitivity
if ActiveRecord::Base.connection.adapter_name.downcase.starts_with? 'mysql'
q = "BINARY uid = ? AND provider='email'"
end

@resource = resource_class.where(q, @email).first
set_resource('uid')

@errors = nil
@error_status = 400

if @resource
@email = @resource.email

yield if block_given?
@resource.send_reset_password_instructions({
email: @email,
Expand All @@ -61,6 +49,7 @@ def create
@errors = @resource.errors
end
else
@email = resource_params[:email]
@errors = [I18n.t("devise_token_auth.passwords.user_not_found", email: @email)]
@error_status = 404
end
Expand Down Expand Up @@ -223,7 +212,10 @@ def render_update_error
private

def resource_params
params.permit(:email, :password, :password_confirmation, :current_password, :reset_password_token)
params.permit(
:email, :password, :password_confirmation, :current_password,
:reset_password_token, :backup_field_name, :backup_field_class
)
end

def password_resource_params
Expand Down
35 changes: 10 additions & 25 deletions app/controllers/devise_token_auth/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,14 @@ def create
# Check
field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first

@resource = nil
if field
q_value = resource_params[field]

if resource_class.case_insensitive_keys.include?(field)
q_value.downcase!
end

q = "#{field.to_s} = ? AND provider='email'"

if ActiveRecord::Base.connection.adapter_name.downcase.starts_with? 'mysql'
q = "BINARY " + q
end

@resource = resource_class.where(q, q_value).first
set_resource(field)
unless @resource.present?
render_create_error_bad_credentials
return
end

if @resource and valid_params?(field, q_value) and @resource.valid_password?(resource_params[:password]) and ([email protected]_to?(:active_for_authentication?) or @resource.active_for_authentication?)
# create client id
if resource_params[:password].present? && @resource.valid_password?(resource_params[:password]) \
&& ([email protected]_to?(:active_for_authentication?) || @resource.active_for_authentication?)
@client_id = SecureRandom.urlsafe_base64(nil, false)
@token = SecureRandom.urlsafe_base64(nil, false)

Expand All @@ -41,11 +30,9 @@ def create
@resource.save

sign_in(:user, @resource, store: false, bypass: false)

yield if block_given?

render_create_success
elsif @resource and not (!@resource.respond_to?(:active_for_authentication?) or @resource.active_for_authentication?)
elsif @resource.respond_to?(:active_for_authentication?) && !@resource.active_for_authentication?
render_create_error_not_confirmed
else
render_create_error_bad_credentials
Expand All @@ -72,10 +59,6 @@ def destroy

protected

def valid_params?(key, val)
resource_params[:password] && key && val
end

def get_auth_params
auth_key = nil
auth_val = nil
Expand Down Expand Up @@ -141,7 +124,9 @@ def render_destroy_error
private

def resource_params
params.permit(*params_for_resource(:sign_in))
allowed_params = params_for_resource(:sign_in)
allowed_params += [:backup_field_name, :backup_field_class]
params.permit(*allowed_params)
end

end
Expand Down
92 changes: 62 additions & 30 deletions test/controllers/devise_token_auth/passwords_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
# was the appropriate message delivered in the json payload?

class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase

def get_mail_info
@mail = ActionMailer::Base.deliveries.last
@resource.reload

@mail_config_name = CGI.unescape(@mail.body.match(/config=([^&]*)&/)[1])
@mail_redirect_url = CGI.unescape(@mail.body.match(/redirect_url=([^&]*)&/)[1])
@mail_reset_token = @mail.body.match(/reset_password_token=(.*)\"/)[1]
end

describe DeviseTokenAuth::PasswordsController do
describe "Password reset" do
before do
Expand Down Expand Up @@ -81,13 +91,8 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase
redirect_url: @redirect_url
}

@mail = ActionMailer::Base.deliveries.last
@resource.reload
get_mail_info()
@data = JSON.parse(response.body)

@mail_config_name = CGI.unescape(@mail.body.match(/config=([^&]*)&/)[1])
@mail_redirect_url = CGI.unescape(@mail.body.match(/redirect_url=([^&]*)&/)[1])
@mail_reset_token = @mail.body.match(/reset_password_token=(.*)\"/)[1]
end

test 'response should return success status' do
Expand Down Expand Up @@ -442,12 +447,7 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase
redirect_url: @redirect_url
}

@mail = ActionMailer::Base.deliveries.last
@resource.reload

@mail_config_name = CGI.unescape(@mail.body.match(/config=([^&]*)&/)[1])
@mail_redirect_url = CGI.unescape(@mail.body.match(/redirect_url=([^&]*)&/)[1])
@mail_reset_token = @mail.body.match(/reset_password_token=(.*)\"/)[1]
get_mail_info()
end

test 'response should return success status' do
Expand All @@ -473,12 +473,7 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase
redirect_url: @redirect_url
}

@mail = ActionMailer::Base.deliveries.last
@resource.reload

@mail_config_name = CGI.unescape(@mail.body.match(/config=([^&]*)&/)[1])
@mail_redirect_url = CGI.unescape(@mail.body.match(/redirect_url=([^&]*)&/)[1])
@mail_reset_token = @mail.body.match(/reset_password_token=(.*)\"/)[1]
get_mail_info()

xhr :get, :edit, {
reset_password_token: @mail_reset_token,
Expand Down Expand Up @@ -506,12 +501,7 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase
redirect_url: @redirect_url
}

@mail = ActionMailer::Base.deliveries.last
@resource.reload

@mail_config_name = CGI.unescape(@mail.body.match(/config=([^&]*)&/)[1])
@mail_redirect_url = CGI.unescape(@mail.body.match(/redirect_url=([^&]*)&/)[1])
@mail_reset_token = @mail.body.match(/reset_password_token=(.*)\"/)[1]
get_mail_info()

xhr :get, :edit, {
reset_password_token: @mail_reset_token,
Expand All @@ -534,17 +524,59 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase
config_name: @config_name
}

@mail = ActionMailer::Base.deliveries.last
@resource.reload

@mail_config_name = CGI.unescape(@mail.body.match(/config=([^&]*)&/)[1])
@mail_redirect_url = CGI.unescape(@mail.body.match(/redirect_url=([^&]*)&/)[1])
@mail_reset_token = @mail.body.match(/reset_password_token=(.*)\"/)[1]
get_mail_info()
end

test 'config_name param is included in the confirmation email link' do
assert_equal @config_name, @mail_config_name
end
end

describe 'backup authorization field' do
setup do
@request.env['devise.mapping'] = Devise.mappings[:account]
end

teardown do
@request.env['devise.mapping'] = Devise.mappings[:user]
end

before do
@resource = accounts(:one)
@redirect_url = 'http://ng-token-auth.dev'
end

test 'for same resource' do
xhr :post, :create, {
email: @resource.nickname,
redirect_url: @redirect_url,
backup_field_name: 'nickname'
}
get_mail_info()
assert_equal @mail.to.first, @resource.email
end

test 'for basic relationship' do
xhr :post, :create, {
email: @resource.profile.other_field,
redirect_url: @redirect_url,
backup_field_name: 'other_field',
backup_field_class: 'profile'
}
get_mail_info()
assert_equal @mail.to.first, @resource.email
end

test 'for polymorphic relationship' do
xhr :post, :create, {
email: @resource.owner.other_field,
redirect_url: @redirect_url,
backup_field_name: 'other_field',
backup_field_class: 'company'
}
get_mail_info()
assert_equal @mail.to.first, @resource.email
end
end
end
end
51 changes: 51 additions & 0 deletions test/controllers/devise_token_auth/sessions_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -374,5 +374,56 @@ def @controller.reset_session; @reset_session_called = true; end
refute OnlyEmailUser.method_defined?(:confirmed_at)
end
end

describe "backup authorization field" do
setup do
@request.env['devise.mapping'] = Devise.mappings[:account]
end

teardown do
@request.env['devise.mapping'] = Devise.mappings[:user]
end

before do
@existing_user = accounts(:one)
@existing_user.skip_confirmation!
@existing_user.save!
end

test 'for same resource' do
xhr :post, :create, {
email: @existing_user.nickname,
password: 'secret123',
backup_field_name: 'nickname'
}
@resource = assigns(:resource)
@data = JSON.parse(response.body)
assert_equal @existing_user.email, @data['data']['email']
end

test 'for basic relationship' do
xhr :post, :create, {
email: @existing_user.profile.other_field,
password: 'secret123',
backup_field_name: 'other_field',
backup_field_class: 'profile'
}
@resource = assigns(:resource)
@data = JSON.parse(response.body)
assert_equal @existing_user.email, @data['data']['email']
end

test 'for polymorphic relationship' do
xhr :post, :create, {
email: @existing_user.owner.other_field,
password: 'secret123',
backup_field_name: 'other_field',
backup_field_class: 'company'
}
@resource = assigns(:resource)
@data = JSON.parse(response.body)
assert_equal @existing_user.email, @data['data']['email']
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class SessionsController < DeviseTokenAuth::SessionsController
def create
@resource = resource_class.find_by_email(resource_params[:email])

if @resource and valid_params?(:email, resource_params[:email]) and @resource.valid_password?(resource_params[:password]) and @resource.confirmed?
if @resource and resource_params[:password].present? and @resource.valid_password?(resource_params[:password]) and @resource.confirmed?
# create client id
@client_id = SecureRandom.urlsafe_base64(nil, false)
@token = SecureRandom.urlsafe_base64(nil, false)
Expand Down
5 changes: 5 additions & 0 deletions test/dummy/app/models/account.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Account < ActiveRecord::Base
include DeviseTokenAuth::Concerns::User
belongs_to :owner, polymorphic: true
has_one :profile
end
3 changes: 3 additions & 0 deletions test/dummy/app/models/company.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Company < ActiveRecord::Base
has_one :account, as: :owner
end
3 changes: 3 additions & 0 deletions test/dummy/app/models/profile.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Profile < ActiveRecord::Base
belongs_to :account
end
4 changes: 4 additions & 0 deletions test/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
# need to be defined within a devise_scope as shown below
mount_devise_token_auth_for "Mang", at: 'mangs'

# define :accounts as the second devise mapping. routes using this class will
# need to be defined within a devise_scope as shown below
mount_devise_token_auth_for "Account", at: 'accounts'

mount_devise_token_auth_for 'EvilUser', at: 'evil_user_auth', controllers: {
confirmations: 'overrides/confirmations',
passwords: 'overrides/passwords',
Expand Down
Loading