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

Paranoid mode still autosaves its associated authenticatable model #217

Closed
nevans opened this issue Apr 23, 2024 · 8 comments · Fixed by #219
Closed

Paranoid mode still autosaves its associated authenticatable model #217

nevans opened this issue Apr 23, 2024 · 8 comments · Fixed by #219

Comments

@nevans
Copy link
Contributor

nevans commented Apr 23, 2024

When config.paranoid = true, the authenticatable model is automatically created and saved to the database. This is unexpected and unwanted.

The authenticatable relationship is defined here:

belongs_to(
:authenticatable,
polymorphic: true,
inverse_of: :passwordless_sessions
)

From https://api.rubyonrails.org/v7.1.3/classes/ActiveRecord/AutosaveAssociation.html

Note that autosave: false is not same as not declaring :autosave. When the :autosave option is not present then new association records are saved but the updated association records are not saved.

So, when handle_resource_not_found does this:

def handle_resource_not_found
if Passwordless.config.paranoid
@resource = authenticatable_class.new(email: normalized_email_param)
@skip_after_session_save_callback = true

And then does this:
handle_resource_not_found unless @resource = find_authenticatable
@session = build_passwordless_session(@resource)
if @session.save

...assuming there are no failing validations on @resource, then both the Passwordless::Session and the authenticatable association are saved.

We worked around this by adding the following to our config/initializers/passwordless.rb:

reflection = Passwordless::Session.reflect_on_association :authenticatable
reflection.options[:autosave] = false

As a simple fix, I propose adding autosave: false to the association. Although, that may result in breaking the "paranoid" aspect of config.paranoid = true. I haven't looked into the best way to handle this in the controller (yet), and automatically creating the authenticatable record was the more immediately pressing issue for us.

@mikker
Copy link
Owner

mikker commented Apr 23, 2024

Thank you for a thorough explanation – however, I am not totally sure what's going on.

Paranoid is for not telling the user whether a user record with their supplied email address exists. Isn't what you're describing here something else?

@avdi
Copy link

avdi commented Apr 23, 2024

Thank you for a thorough explanation – however, I am not totally sure what's going on.

Paranoid is for not telling the user whether a user record with their supplied email address exists. Isn't what you're describing here something else?

The upshot is that for an account that doesn't exist, Passwordless:

  1. Behaves as if it exists (correctly) by generating a session and associated ephemeral account...
  2. ...then accidentally creates the account by saving the should-be-ephemeral authenticatable as a side-effect of saving the session.

@mikker
Copy link
Owner

mikker commented Apr 23, 2024

Ohh, I get it. That's not good.

As a simple fix, I propose adding autosave: false to the association. Although, that may result in breaking the "paranoid" aspect of config.paranoid = true.

Breaking it how?

@nevans
Copy link
Contributor Author

nevans commented Apr 24, 2024

Breaking it how?

So I haven't adequately tested it yet, but...

I believe that belongs_to creates a validation that will fail when the authenticatable object isn't persisted. So then the if @session.save statement will evaluate the else clause.

The point of config.paranoid = true is that whoever is inputting an email address into the form shouldn't be given any information about whether or not that email exists in our system (unless they can read email sent to that address). And evaluating the else clause breaks that.

If that is how it works, that means that our workaround fixes the autosave problem but breaks the "paranoid" feature.

@nevans
Copy link
Contributor Author

nevans commented Apr 24, 2024

So I haven't adequately tested it yet, but...

So, we just tested it and fortunately, it looks like this is not an issue! The Passwordless::Session object is saved successfully, the authenticatable_type field has been set, but the authenticatable_id field is nil. I think that the default rails belongs_to presence validation works very simply and literally: it checks that session.authenticatable.present? is truthy, but it does not check that it has been persisted, nor does it check that session.authenticatable_id.present? is truthy.

@nevans
Copy link
Contributor Author

nevans commented Apr 24, 2024

And after digging just a little bit deeper, I see that the code surrounding this has changed between Rails 7.0 and 7.1: See the docs for config.active_record.belongs_to_required_validates_foreign_key. The app I've been testing is running rails 7.1, but is still configured to use the rails 7.0 defaults.

But, if I'm understanding this correctly, this config setting doesn't actually make any difference to this issue. The relevant source code for it here: https://github.com/rails/rails/blob/9fd8b33ebba3c281c6cc5bbf8f48cde38c6fb0da/activerecord/lib/active_record/associations/builder/belongs_to.rb#L126-L141. And, athough the rails 7.1 default does add an :if condition to the validation, it does not change the actual validation itself. It's still just model.requires_presence_of reflection.name, i.e: requires_presence_of :authenticatable, which is simply going to check that session.authenticatable.present?. And Session.new(authenticatable: authenticatable_class.new(email:)).authenticatable.present? will always be truthy.

In summary, I think it should be safe to simply set autosave: false. :)

@nevans
Copy link
Contributor Author

nevans commented Apr 24, 2024

And I see now that passwordless also adds an explicit presence validation for :authenticatable, but this will behave identically to the implicit belongs_to validation so it doesn't change the analysis.

nevans added a commit to nevans/passwordless that referenced this issue Apr 24, 2024
This prevents the authenticable model from being auto-created when
`config.paranoid = true`.

Fixes mikker#217.
mikker pushed a commit that referenced this issue Apr 25, 2024
This prevents the authenticable model from being auto-created when
`config.paranoid = true`.

Fixes #217.
mikker added a commit that referenced this issue Apr 25, 2024
* Disable autosave of session.authenticatable

This prevents the authenticable model from being auto-created when
`config.paranoid = true`.

Fixes #217.

* Add assertion

---------

Co-authored-by: nick evans <[email protected]>
@mikker
Copy link
Owner

mikker commented Apr 25, 2024

Some real nice detective work there. Thank you so much! I'll get a new release out soon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants