Search code examples
ruby-on-railsauthenticationdevise

How to redirect users on timeout to different pages based on login method?


I am using devise with multiple authentication strategies (SAML using the devise_saml_authenticatable gem):

devise :database_authenticatable, :saml_authenticatable, :timeoutable

A user can log in with a standard devise login page (database authentication) or with a SAML login. Once logged in, there is no difference in the model used for the user (both login methods give the same model: User).

The problem

Whenever the user is timed out, I want to redirect them to the login page they used to sign in with. Figuring out which login method was used is not a problem, but I have not found any way to access this information upon timeout.

The actual redirect happens in the Devise FailureApp. I have set a custom failure_app for the custom redirect:

class CustomFailureApp < Devise::FailureApp
  def redirect_url
    if warden_message == :timeout
      # Find some way to obtain the login method
      redirect_to ...
    end
  end
end

I need to now obtain the used login method in this method.

What I have already tried

I tried using:

Warden::Manager.before_logout do |user, auth, opts|
  opts[:login_method] = user.login_method
end

Here I still have access to the user object and can retrieve the login method, but anything I set in the options is not actually propagated to the CustomFailureApp.

I then tried:

Warden::Manager.before_failure do |env, opts|
  opts[:login_method] = ?
end

Any options I set here are propagated to the CustomFailureApp, but I don't have access to the user and cannot determine their login method here.

I tried finding the user object in the CustomFailureApp from the warden session:

def redirect_url
  user = warden.session(:user) # Crashes, session invalid
  ...
end

However, the this fails because the session has already been destroyed. Furthermore, looking through the request object I don't see any clear user-identifying information.

Similar solutions which I cannot use

I know it is possible to redirect a user to a different login page based on scope, as stated in How to define custom Failure for devise in case of two different models User and active admin?. However, my users are not distinguishable by scope.

7 years ago someone posted a way which does identify the user in the Warden::Manager.before_failure method, which I would be able to use: https://stackoverflow.com/a/33230548. However, this solution no longer seems to work/does not seem to work for me. I am unable to find any user-related information in the request parameters (or anywhere in the request), besides the (encrypted) warden session which warden refuses to still use as it is timed out.

The question(s)

Is there any way with which I can identify the user in CustomFailureApp or pass information to the CustomFailureApp from a point where I can?

Is there a way to make the scopes differ for users logged in with these different methods?

Otherwise, can I store the login method used in some different way where I can access it in the CustomFailureApp?

If not, is there any other way to have this "redirect to the correct login page" based on which login method the user used before?


Solution

  • I was having so much trouble with this. I just found your post, and I tried many of the same things you did but your question finally got me to a solution. Big thanks for your detailed question. Hope this helps you or others looking for an answer.

    I wasn't ultimately able to base the logic off of the provider but instead off of an attribute on the user model. So, in place of some_stuff below you could use a company_id or something.

    In your config/initializers/devise.rb you want something like this:

    Warden::Manager.before_failure do |env, opts|
      env['warden.options']['some_stuff'] = @some_stuff
    end
    
    Warden::Manager.after_set_user do |record, warden, options|
      @some_stuff = record.some_stuff
    end
    

    And then in your CustomFailureApp (I have it in lib/custom_failure_app.rb):

    class CustomFailureApp < Devise::FailureApp
      def redirect_url
        if request.env['warden.options']['some_stuff'] == 'The Good Stuff'
          "https://your.saml.redirect/path/here"
        else
          super
        end
      end
    end 
    

    This does feel hacky so if someone thinks it smells and has some suggestion please feel free. I would love to know a better way to achieve this behavior.