abevoelker / devise-passwordless

Devise passwordless logins using emailed magic links
MIT License
201 stars 37 forks source link

concurent use of logging, via password and passwordless, missed case #42

Closed loicginoux closed 10 months ago

loicginoux commented 1 year ago

Hello, I see you recently fixed this issue https://github.com/abevoelker/devise-passwordless/issues/13 In my case it seems like there is a missing case in which the current implementation does not seem to work. I have a model User, with 2 roles (guest and customer). I want user with role guest to be able to login only via passwordless strategy I want user with role customer to be able to login only via password strategy

I tried with something like this:

class User < ApplicationRecord
  devise  :database_authenticatable,
          :registerable,
          :recoverable,
          :rememberable,
          :trackable,
          :validatable,
          :confirmable,
          :magic_link_authenticatable,

  enum role: [:guest, :customer]

  def active_for_authentication?
    super && !guest?
  end

  def active_for_magic_link_authentication?
    super && guest?
  end
end

but it seems like when I sign in with a customer user, and a valid password, the callback defined in devise-passwordless-1.0.1/lib/devise/hooks/magic_link_authenticatable.rb

on after_set_user it will try user.active_for_magic_link_authentication? and will return false. I will then have an error response like "you are not a guest user" (my custom message coming from magic_link_inactive_message). Is there something I missed or it's not possible to use both strategy conditionnally on the same resource ?

I think you miss a case in your specs https://github.com/abevoelker/devise-passwordless/blob/adc78bb90d6fee55490bb712a0a373c056ce190b/spec/dummy_app_config/shared_source/all/spec/system/combined_user/sign_in_spec.rb

context "signing in with password" do
  context "with passwordless authentication disabled" do
    it "successfully logs in combined user using password" do ...

I would say this should pass and it does not currently.

version used: v1.0.1

abevoelker commented 1 year ago

What do your routes look like? Annoyingly there does need to be separate paths for password vs passwordless logins because they use different SessionsControllers. In the spec you mentioned and in the README it's done like this: https://github.com/abevoelker/devise-passwordless/blob/adc78bb90d6fee55490bb712a0a373c056ce190b/spec/dummy_app_config/shared_source/all/config/routes.rb#L34-L41

I would say this should pass and it does not currently.

That spec runs on every commit, and it does pass. You can test locally with https://github.com/nektos/act

loicginoux commented 1 year ago

ok thank you... I guess it will be something about my current setup then.. I guess showing you my current route won't be of much help because I had to mix features from your gem and the devise_token_auth gem because my rails app is a JSON API. So I cannot just do like in your example. What I have at the moment is something like this:

namespace :api, defaults: { format: :json }, constraints: { format: 'json' } do
  namespace :auth do
    devise_scope :user do
      post 'guest/sign_in', to: 'sessions#guest_sign_in'
      get 'guest/magic_link', to: 'sessions#guest_magic_link'
    end
  end
end

so POST /api/auth/guest/sign_in goes to my custom session controller with a method guest_sign_in that sends the email and return a json response and GET /api/auth/guest/magic_link also goes to a custom controller method where I authenticate like you do on your gem and sends back token as json response.

I'll try with 2 définitions of devise_for in routes.rb. What I didn't get is how, by declaring 2 routing paths for the 2 separate sign in strategy, it will help prevent this bug from happening. In other word how come, in your example, if I call POST /combined_users/sign_in it won't enter inside the hook from devise/hooks/magic_link_authenticatable.rb. I'll continue investigate, thank you for the quick response anyway

loicginoux commented 1 year ago

I could not identify the problem but it might be because of my custom implementation. It still does not work with a custom namespace but anyway, I managed to find a solution go around this problem. I guess I can close the issue

abevoelker commented 12 months ago

Sorry kinda slammed at the moment but would like to look into this later to make this work for your use case better. Not sure when I'll be able to dig in more, possibly next week

loicginoux commented 12 months ago

Thank you, at the moment, as a workaround, guest user have a long password and cannot reset it, and customer users won't have access to passwordless login via UI, even if they find the api endpoint to send the mail, we think it's not a huge problem

tbelliard commented 10 months ago

I encountered the same issue as loicginoux, who had the correct analysis: when activating both database_authenticable and magic_link_authenticatable and making active_for_magic_link_authentication? return false on the User model made password authentication inoperable, because the warden hook does reset the session when active_for_magic_link_authentication? returns false without testing the actual source of successful authentication.

I published pull request #47 as an attempt to fix the issue by testing the winning strategy in the after_set_user hook. I also added a test case to cover this scenario.

I am not especially familiar with the inner workings of Devise and Warden, so not totally sure my approach here is correct; for now at least I have not observed any negative side effects, and I do not think the fix should affect existing setups.

abevoelker commented 10 months ago

@tbelliard Thanks very much for digging in and providing the fix! Fantastic work. I have merged it.

@loicginoux Merging the PR seems to have auto-closed the issue, however if you feel this didn't resolve your problem feel free to reopen. I appreciate you reporting this issue.