janko / rodauth-model

Password attribute and associations for Rodauth account model

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ActiveRecord::AssociationTypeMismatch on password assignment

hmasing opened this issue · comments

I have started getting this error when assignining passwords to an account:

ActiveRecord::AssociationTypeMismatch: Authentication::Account::PasswordHash(#130740) expected, 
got "$2a$12$FZ4poi3KsWfFVbnLdkBYlubKzTJV3vAYenkbSstQfE7H7jCYjpKmC" 
which is an instance of BCrypt::Password(#130760) (ActiveRecord::AssociationTypeMismatch)

Context:

relevant /app/mist/rodauth_main.rb settings:

class RodauthMain < Rodauth::Rails::Auth
  configure do
    enable :create_account, :verify_account,
           :login, :logout, :remember,
           :reset_password, :change_password, :change_password_notify,
           :change_login, :verify_login_change, :close_account,
           :i18n

    accounts_table :authentication_accounts
    rails_account_model Authentication::Account
    account_password_hash_column :password_hash

The Authentication::Account class inherits Rodauth::Model thusly (and has for a year since I wrote this code). Relevant code:

class Authentication::Account < ApplicationRecord
  include Rodauth::Model(RodauthMain) unless ENV['ASSET_PRECOMPILE']
  validates :password, presence: true, password_complexity: true, if: :password_validation_required?, on: :update

Duplicating the error:

Loading development environment (Rails 7.1.2)
irb(main):001> account = Authentication::Account.new
=> #<Authentication::Account:0x0000000165a54ef0
 id: nil,
 status: "unverified",
 role: "user",
 email: nil,
 password_hash: nil,
 is_locked: false,
 party_id: nil,
 email_hash: nil,
 data: {"client_targets"=>[]},
 code_int: nil,
 discarded_at: nil,
 created_at: nil,
 updated_at: nil>
irb(main):002> account.password
=> nil
irb(main):003> account.password = 'MyBestPassword'
Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/associations/association.rb:313:in `raise_on_type_mismatch!': Authentication::Account::PasswordHash(#198460) expected, got "$2a$12$wJeBmJUvhbhX/OjW0UIxe.yN/nG/hQhiUXuhjJR9c0hmMym935zm2" which is an instance of BCrypt::Password(#198480) (ActiveRecord::AssociationTypeMismatch)

Backtrace:

Authentication::Account::PasswordHash(#130740) expected, got "$2a$12$d8j72Mh8039mZEhBr76/j.o80zYCEmoRnp4R34sQgzqme5WEaie4O" which is an instance of BCrypt::Password(#130760)
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/associations/association.rb:313:in `raise_on_type_mismatch!'
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/associations/has_one_association.rb:60:in `replace'
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/associations/singular_association.rb:19:in `writer'
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/associations/builder/association.rb:112:in `password_hash='
/Users/hmasing/gems/gems/rodauth-model-0.2.1/lib/rodauth/model/active_record.rb:25:in `public_send'
/Users/hmasing/gems/gems/rodauth-model-0.2.1/lib/rodauth/model/active_record.rb:25:in `block in define_methods'
/Users/hmasing/gems/gems/rodauth-model-0.2.1/lib/rodauth/model/active_record.rb:20:in `block in define_methods'
/Users/hmasing/gems/gems/activemodel-7.1.2/lib/active_model/attribute_assignment.rb:49:in `public_send'
/Users/hmasing/gems/gems/activemodel-7.1.2/lib/active_model/attribute_assignment.rb:49:in `_assign_attribute'
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/attribute_assignment.rb:19:in `block in _assign_attributes'
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/attribute_assignment.rb:11:in `each'
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/attribute_assignment.rb:11:in `_assign_attributes'
/Users/hmasing/gems/gems/activemodel-7.1.2/lib/active_model/attribute_assignment.rb:34:in `assign_attributes'
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/core.rb:432:in `initialize'
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/inheritance.rb:76:in `new'
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/inheritance.rb:76:in `new'
/Users/hmasing/Development/Hum/letshum-core/db/seeds.rb:148:in `<main>'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/engine.rb:556:in `load'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/engine.rb:556:in `block in load_seed'
/Users/hmasing/gems/gems/activesupport-7.1.2/lib/active_support/callbacks.rb:121:in `block in run_callbacks'
/Users/hmasing/gems/gems/activesupport-7.1.2/lib/active_support/execution_wrapper.rb:92:in `wrap'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/engine.rb:642:in `block (2 levels) in <class:Engine>'
/Users/hmasing/gems/gems/activesupport-7.1.2/lib/active_support/callbacks.rb:130:in `instance_exec'
/Users/hmasing/gems/gems/activesupport-7.1.2/lib/active_support/callbacks.rb:130:in `block in run_callbacks'
/Users/hmasing/gems/gems/activesupport-7.1.2/lib/active_support/callbacks.rb:141:in `run_callbacks'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/engine.rb:556:in `load_seed'
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/tasks/database_tasks.rb:468:in `load_seed'
/Users/hmasing/gems/gems/activerecord-7.1.2/lib/active_record/railties/databases.rake:405:in `block (2 levels) in <main>'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/task.rb:281:in `block in execute'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/task.rb:281:in `each'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/task.rb:281:in `execute'
/Users/hmasing/Development/Hum/letshum-core/lib/tasks/hum.rake:51:in `block (2 levels) in <main>'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/task.rb:281:in `block in execute'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/task.rb:281:in `each'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/task.rb:281:in `execute'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/task.rb:219:in `block in invoke_with_call_chain'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/task.rb:199:in `synchronize'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/task.rb:199:in `invoke_with_call_chain'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/task.rb:188:in `invoke'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/application.rb:182:in `invoke_task'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/application.rb:138:in `block (2 levels) in top_level'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/application.rb:138:in `each'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/application.rb:138:in `block in top_level'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/application.rb:147:in `run_with_threads'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/application.rb:132:in `top_level'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/commands/rake/rake_command.rb:27:in `block (2 levels) in perform'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/application.rb:208:in `standard_exception_handling'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/commands/rake/rake_command.rb:27:in `block in perform'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/commands/rake/rake_command.rb:44:in `block in with_rake'
/Users/hmasing/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/rake-13.1.0/lib/rake/rake_module.rb:59:in `with_application'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/commands/rake/rake_command.rb:41:in `with_rake'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/commands/rake/rake_command.rb:20:in `perform'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/command.rb:156:in `invoke_rake'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/command.rb:73:in `block in invoke'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/command.rb:149:in `with_argv'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/command.rb:69:in `invoke'
/Users/hmasing/gems/gems/railties-7.1.2/lib/rails/commands.rb:18:in `<main>'
/Users/hmasing/gems/gems/bootsnap-1.17.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
/Users/hmasing/gems/gems/bootsnap-1.17.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'

Issue is consistent with Rails 7.0.2 and 7.1.2, and Ruby 3.2.1 and 3.3.0.

bcrypt 3.1.20 with native extensions

I have dug through my change logs and codebase, and cannot identify why this may be happening quite suddenly. This area of the stack hasn't changed in about 6 months, and it's been working fine.

I suspect somewhere around here?

define_method(:password=) do |password|

Hmm, that's strange. It seems that Rodauth::Model thinks that you're using a separate table for password hashes (default Rodauth), but setting account_password_hash_column overrides this behavior, so Rodauth::Model shouldn't be defining this association anymore.

You say you don't know what change caused this regression? It wasn't a rodauth-rails or rodauth version bump? Was it maybe the move from Rodauth::Rails.model to Rodauth::Model(RodauthMain)? Could you try the former, and see if you still get the error?

You know what, it could be a code (re)loading issue. I see you have an eager rails_account_model Authentication::Account declaration, but it should be a lazy rails_account_model { Authentication::Account }. I initially recommended the former, but then someone warned me that it's causing a circular dependency, where Account depends on RodauthMain and RodauthMain depends on Account. The lazy block evaluation fixes that circularity.

You know what, it could be a code (re)loading issue. I see you have an eager rails_account_model Authentication::Account declaration, but it should be a lazy rails_account_model { Authentication::Account }. I initially recommended the former, but then someone warned me that it's causing a circular dependency, where Account depends on RodauthMain and RodauthMain depends on Account. The lazy block evaluation fixes that circularity.

I've tried with and without the eager load with the same results. Trying the first suggestion momentarily!

I've tried with and without the eager load with the same results.

Ah, I really thought that was the issue. This is what I thought was happening:

  1. RodauthMain class is loaded
  2. when it gets to rails_account_model line, it starts loading Authentication::Account class
    • at this point RodauthMain is only partially defined, it didn't yet define account_password_hash_column, which is crucial
  3. Authentication::Account class evaluates Rodauth::Model inclusion
  4. Rodauth::Model checks whether account_password_hash_column is defined on RodauthMain, sees that it's not (because that code hasn't been evaluated yet), and then proceeds defining a password hash association

Any chance you could reproduce the issue by modifying the demo app?

You say you don't know what change caused this regression? It wasn't a rodauth-rails or rodauth version bump? Was it maybe the move from Rodauth::Rails.model to Rodauth::Model(RodauthMain)? Could you try the former, and see if you still get the error?

I've tried all these permutations:

class Authentication::Account < ApplicationRecord
  include Rodauth::Model(RodauthMain) unless ENV['ASSET_PRECOMPILE']
  # include Rodauth::Model(RodauthMain)
  # include Rodauth::Rails.model

Using Rodauth::Model(RodauthMain) with or without ASSET_PRECOMPILE produces the exception above.

Using Rodauth::Rails.model fails even more magnificiently :-)

bin/rails aborted!
NoMethodError: undefined method `rodauth' for class RodauthApp (NoMethodError)

        rodauth(name) or fail ArgumentError, "unknown rodauth configuration: #{name.inspect}"
        ^^^^^^^
Did you mean?  rodauth!
/Users/hmasing/Development/Hum/letshum-core/app/models/authentication/account.rb:36:in `<class:Account>'
/Users/hmasing/Development/Hum/letshum-core/app/models/authentication/account.rb:33:in `<main>'
/Users/hmasing/Development/Hum/letshum-core/app/models/authentication.rb:9:in `<module:Authentication>'
/Users/hmasing/Development/Hum/letshum-core/app/models/authentication.rb:3:in `<main>'
/Users/hmasing/Development/Hum/letshum-core/app/misc/rodauth_main.rb:13:in `block in <class:RodauthMain>'
/Users/hmasing/Development/Hum/letshum-core/app/misc/rodauth_main.rb:4:in `<class:RodauthMain>'
/Users/hmasing/Development/Hum/letshum-core/app/misc/rodauth_main.rb:3:in `<main>'
/Users/hmasing/Development/Hum/letshum-core/app/misc/rodauth_app.rb:5:in `<class:RodauthApp>'
/Users/hmasing/Development/Hum/letshum-core/app/misc/rodauth_app.rb:3:in `<main>'
/Users/hmasing/Development/Hum/letshum-core/db/seeds.rb:2:in `<main>'
/Users/hmasing/Development/Hum/letshum-core/lib/tasks/hum.rake:51:in `block (2 levels) in <main>'
Tasks: TOP => db:nuke_and_seed
(See full trace by running task with --trace)

I tried the demo app, loaded in all the gems in my stack, and everything ran fine.

For grins, I switched from bcrypt to argon2 and got the same results.

ActiveRecord::AssociationTypeMismatch: Authentication::Account::PasswordHash(#130780) expected, got "$argon2id$v=19$m=65536,t=2,p=1$MLbr/z2sjzVaPXnlW+WMRA$GWg2ANf/6PHYbPiZBmTD+8Fn25VoMDEQ/mJq33uMFTg" which is an instance of String(#7580) (ActiveRecord::AssociationTypeMismatch)

To be frank, I'm at wits end here... I appreciate the help you've already given and any other insight you may have.

rails_account_model { Authentication::Account }

CORRECTION: This did, indeed, fix the issue. Thank you!