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?
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 lazyrails_account_model { Authentication::Account }
. I initially recommended the former, but then someone warned me that it's causing a circular dependency, whereAccount
depends onRodauthMain
andRodauthMain
depends onAccount
. 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:
RodauthMain
class is loaded- when it gets to
rails_account_model
line, it starts loadingAuthentication::Account
class- at this point
RodauthMain
is only partially defined, it didn't yet defineaccount_password_hash_column
, which is crucial
- at this point
Authentication::Account
class evaluatesRodauth::Model
inclusionRodauth::Model
checks whetheraccount_password_hash_column
is defined onRodauthMain
, 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
orrodauth
version bump? Was it maybe the move fromRodauth::Rails.model
toRodauth::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!