diff --git a/app/controllers/devise_token_auth/application_controller.rb b/app/controllers/devise_token_auth/application_controller.rb index 18e930cb9..8fd3f0577 100644 --- a/app/controllers/devise_token_auth/application_controller.rb +++ b/app/controllers/devise_token_auth/application_controller.rb @@ -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 diff --git a/app/controllers/devise_token_auth/passwords_controller.rb b/app/controllers/devise_token_auth/passwords_controller.rb index dce44a8cb..7e692d067 100644 --- a/app/controllers/devise_token_auth/passwords_controller.rb +++ b/app/controllers/devise_token_auth/passwords_controller.rb @@ -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, @@ -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 @@ -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 diff --git a/app/controllers/devise_token_auth/sessions_controller.rb b/app/controllers/devise_token_auth/sessions_controller.rb index c3a33e093..cbd446abd 100644 --- a/app/controllers/devise_token_auth/sessions_controller.rb +++ b/app/controllers/devise_token_auth/sessions_controller.rb @@ -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 (!@resource.respond_to?(:active_for_authentication?) or @resource.active_for_authentication?) - # create client id + if resource_params[:password].present? && @resource.valid_password?(resource_params[:password]) \ + && (!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?) @client_id = SecureRandom.urlsafe_base64(nil, false) @token = SecureRandom.urlsafe_base64(nil, false) @@ -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 @@ -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 @@ -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 diff --git a/test/controllers/devise_token_auth/passwords_controller_test.rb b/test/controllers/devise_token_auth/passwords_controller_test.rb index 00cb1f6eb..1841e62b2 100644 --- a/test/controllers/devise_token_auth/passwords_controller_test.rb +++ b/test/controllers/devise_token_auth/passwords_controller_test.rb @@ -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 @@ -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 @@ -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 @@ -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, @@ -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, @@ -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 diff --git a/test/controllers/devise_token_auth/sessions_controller_test.rb b/test/controllers/devise_token_auth/sessions_controller_test.rb index cc7f66d96..451705a0e 100644 --- a/test/controllers/devise_token_auth/sessions_controller_test.rb +++ b/test/controllers/devise_token_auth/sessions_controller_test.rb @@ -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 diff --git a/test/dummy/app/controllers/overrides/sessions_controller.rb b/test/dummy/app/controllers/overrides/sessions_controller.rb index 3be969c34..d2482d0c1 100644 --- a/test/dummy/app/controllers/overrides/sessions_controller.rb +++ b/test/dummy/app/controllers/overrides/sessions_controller.rb @@ -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) diff --git a/test/dummy/app/models/account.rb b/test/dummy/app/models/account.rb new file mode 100644 index 000000000..26b61d23f --- /dev/null +++ b/test/dummy/app/models/account.rb @@ -0,0 +1,5 @@ +class Account < ActiveRecord::Base + include DeviseTokenAuth::Concerns::User + belongs_to :owner, polymorphic: true + has_one :profile +end diff --git a/test/dummy/app/models/company.rb b/test/dummy/app/models/company.rb new file mode 100644 index 000000000..cf1ab91d5 --- /dev/null +++ b/test/dummy/app/models/company.rb @@ -0,0 +1,3 @@ +class Company < ActiveRecord::Base + has_one :account, as: :owner +end diff --git a/test/dummy/app/models/profile.rb b/test/dummy/app/models/profile.rb new file mode 100644 index 000000000..be9a86db4 --- /dev/null +++ b/test/dummy/app/models/profile.rb @@ -0,0 +1,3 @@ +class Profile < ActiveRecord::Base + belongs_to :account +end diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index 2c1c7371a..6f7709a4e 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -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', diff --git a/test/dummy/db/migrate/20160213200808_devise_token_auth_create_accounts.rb b/test/dummy/db/migrate/20160213200808_devise_token_auth_create_accounts.rb new file mode 100644 index 000000000..7c9046da8 --- /dev/null +++ b/test/dummy/db/migrate/20160213200808_devise_token_auth_create_accounts.rb @@ -0,0 +1,62 @@ +include MigrationDatabaseHelper + +class DeviseTokenAuthCreateAccounts < ActiveRecord::Migration + def change + create_table(:accounts) do |t| + ## Database authenticatable + t.string :email + t.string :encrypted_password, :null => false, :default => "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + t.string :reset_password_redirect_url + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + t.integer :sign_in_count, :default => 0, :null => false + t.datetime :current_sign_in_at + t.datetime :last_sign_in_at + t.string :current_sign_in_ip + t.string :last_sign_in_ip + + ## Confirmable + t.string :confirmation_token + t.datetime :confirmed_at + t.datetime :confirmation_sent_at + t.string :confirm_success_url + t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + ## User Info + t.string :name + t.string :nickname + t.string :image + + ## unique oauth id + t.string :provider + t.string :uid, :null => false, :default => "" + + ## Tokens + if json_supported_database? + t.json :tokens + else + t.text :tokens + end + + t.timestamps + end + + add_index :accounts, :email + add_index :accounts, [:uid, :provider], :unique => true + add_index :accounts, :reset_password_token, :unique => true + add_index :accounts, :confirmation_token, :unique => true + # add_index :accounts, :unlock_token, :unique => true + end +end diff --git a/test/dummy/db/migrate/20160213200818_devise_token_auth_create_profiles.rb b/test/dummy/db/migrate/20160213200818_devise_token_auth_create_profiles.rb new file mode 100644 index 000000000..c80f4a279 --- /dev/null +++ b/test/dummy/db/migrate/20160213200818_devise_token_auth_create_profiles.rb @@ -0,0 +1,12 @@ +include MigrationDatabaseHelper + +class DeviseTokenAuthCreateProfiles < ActiveRecord::Migration + def change + create_table :profiles do |t| + t.references :account, index: true, foreign_key: true + t.string :other_field + + t.timestamps null: false + end + end +end diff --git a/test/dummy/db/migrate/20160213201542_devise_token_auth_create_companies.rb b/test/dummy/db/migrate/20160213201542_devise_token_auth_create_companies.rb new file mode 100644 index 000000000..a589741ab --- /dev/null +++ b/test/dummy/db/migrate/20160213201542_devise_token_auth_create_companies.rb @@ -0,0 +1,11 @@ +include MigrationDatabaseHelper + +class DeviseTokenAuthCreateCompanies < ActiveRecord::Migration + def change + create_table :companies do |t| + t.string :other_field + + t.timestamps null: false + end + end +end diff --git a/test/dummy/db/migrate/20160213201638_add_owner_to_accounts.rb b/test/dummy/db/migrate/20160213201638_add_owner_to_accounts.rb new file mode 100644 index 000000000..bee8e2c9e --- /dev/null +++ b/test/dummy/db/migrate/20160213201638_add_owner_to_accounts.rb @@ -0,0 +1,7 @@ +class AddOwnerToAccounts < ActiveRecord::Migration + def change + change_table :accounts do |t| + t.references :owner, polymorphic: true, index: true + end + end +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index c5e24774c..c752d2e83 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -11,7 +11,48 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160103235141) do +ActiveRecord::Schema.define(version: 20160213201638) do + + create_table "accounts", force: :cascade do |t| + t.string "email" + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.string "reset_password_redirect_url" + t.datetime "remember_created_at" + t.integer "sign_in_count", default: 0, null: false + t.datetime "current_sign_in_at" + t.datetime "last_sign_in_at" + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" + t.string "confirmation_token" + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "confirm_success_url" + t.string "unconfirmed_email" + t.string "name" + t.string "nickname" + t.string "image" + t.string "provider" + t.string "uid", default: "", null: false + t.text "tokens" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "owner_id" + t.string "owner_type" + end + + add_index "accounts", ["confirmation_token"], name: "index_accounts_on_confirmation_token", unique: true + add_index "accounts", ["email"], name: "index_accounts_on_email" + add_index "accounts", ["owner_type", "owner_id"], name: "index_accounts_on_owner_type_and_owner_id" + add_index "accounts", ["reset_password_token"], name: "index_accounts_on_reset_password_token", unique: true + add_index "accounts", ["uid", "provider"], name: "index_accounts_on_uid_and_provider", unique: true + + create_table "companies", force: :cascade do |t| + t.string "other_field" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end create_table "evil_users", force: :cascade do |t| t.string "email" @@ -122,6 +163,15 @@ add_index "only_email_users", ["email"], name: "index_only_email_users_on_email" add_index "only_email_users", ["uid", "provider"], name: "index_only_email_users_on_uid_and_provider", unique: true + create_table "profiles", force: :cascade do |t| + t.integer "account_id" + t.string "other_field" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "profiles", ["account_id"], name: "index_profiles_on_account_id" + create_table "scoped_users", force: :cascade do |t| t.string "provider", null: false t.string "uid", default: "", null: false diff --git a/test/fixtures/accounts.yml b/test/fixtures/accounts.yml new file mode 100644 index 000000000..f0d8b9a7b --- /dev/null +++ b/test/fixtures/accounts.yml @@ -0,0 +1,24 @@ +<% timestamp = DateTime.parse(2.weeks.ago.to_s).to_time.strftime("%F %T") %> +<% @email = Faker::Internet.email %> +one: + owner: one (Company) + uid: "<%= @email %>" + email: "<%= @email %>" + nickname: 'roofus' + provider: 'email' + confirmed_at: '<%= timestamp %>' + created_at: '<%= timestamp %>' + updated_at: '<%= timestamp %>' + encrypted_password: <%= Account.new.send(:password_digest, 'secret123') %> + +<% @second_email = Faker::Internet.email %> +two: + owner: two (Company) + uid: "<%= @second_email %>" + email: "<%= @second_email %>" + nickname: 'roofus2' + provider: 'email' + confirmed_at: '<%= timestamp %>' + created_at: '<%= timestamp %>' + updated_at: '<%= timestamp %>' + encrypted_password: <%= Account.new.send(:password_digest, 'secret321') %> diff --git a/test/fixtures/companies.yml b/test/fixtures/companies.yml new file mode 100644 index 000000000..8ec7e6c10 --- /dev/null +++ b/test/fixtures/companies.yml @@ -0,0 +1,8 @@ +# Read about fixtures at +# http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html + +one: + other_field: <%= Faker::Company.name %> + +two: + other_field: <%= Faker::Company.name %> diff --git a/test/fixtures/profiles.yml b/test/fixtures/profiles.yml new file mode 100644 index 000000000..966eedd4b --- /dev/null +++ b/test/fixtures/profiles.yml @@ -0,0 +1,10 @@ +# Read about fixtures at +# http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html + +one: + account: one + other_field: <%= Faker::Internet.user_name %> + +two: + account: two + other_field: <%= Faker::Internet.user_name %>