Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom session#create parameters

To integrate my website to a partner website, I need to have custom parameter names :

By default, devise sessions#create parameters are :

user[email] and user[password]

(These parameters translate to a nested Hash into :user)

For my partner, I need to be able to respond to

email and password

I went through a lot of tries, and I ended to implement a custom Devise::Strategy and a custom user#find_for_database_authentication to be able to catch these parameters.

Devise::Strategies::CustomSso.rb

require 'devise/strategies/database_authenticatable'
module Devise
  module Strategies
    class CustomSso < DatabaseAuthenticatable

      def valid?
        params[:sso]=="true"
      end

      def authenticate!
        password = params[:password]
        resource  = password.present? && mapping.to.find_for_database_authentication(params)
        hashed = false

        if validate(resource){ hashed = true; resource.valid_password?(password) }
          remember_me(resource)
          resource.after_database_authentication
          success!(resource)
        end

        mapping.to.new.password = password if !hashed && Devise.paranoid
        fail(:not_found_in_database) unless resource
      end

    end
  end
end

user.rb

class User < ActiveRecord::Base
  ....
  def self.find_for_database_authentication(params)
    User.find_by(email: "#{params[:email]}")
  end
  ....
end

It works, but is there a better or simpler way to do that ?

like image 213
oliviergg Avatar asked Dec 19 '25 05:12

oliviergg


1 Answers

Reason

The reason that authentication happens against "original" parameters is not because ApplicationController before_action callback runs late or request gets modified (restored) later, but because devise takes parameters not from controller params object.

Originally in Warden params will be taken from new Rack::Request, created from env (environment) value:

  # Convenience method to access the rack request.
  # :api: public
  def request
    @request ||= Rack::Request.new(@env)
  end # request

In Devise though, it's patched to new instance of ActionDispatch::Request instead, so that's why changing request in routes.rb (see options below) will work.

module Warden::Mixins::Common
  def request
    @request ||= ActionDispatch::Request.new(env)
  end

In fact, using ActionDispatch request allows to actually alter request params before Devise processing, see solution 4 below.

Then params are taken from that request (code from Warden Base mixin) to further extract values for authentication:

 # Convenience method to access the rack request params
 # :api: public
 def params
   request.params
 end # params

Authenticatable strategy by Devise has params_auth_hash helper to extract only needed parameters from all (scope is ~ model name, e.g. user)

  # Extract the appropriate subhash for authentication from params.
  def params_auth_hash
    params[scope]
  end

Although you can configure key names that will be taken from params_auth_hash in your Devise-based model, you can't configure to not "cut" only [scope] portion of params

#   * +authentication_keys+: parameters used for authentication. By default [:email].

Solutions

Therefore there are several options available:

1) Rewriting params method (public in Warden common mixin, not params method of request or controller!) in your strategy to "patch" parameters there, e.g.

class CustomSso < DatabaseAuthenticatable
  def params 
    request.params[:user][:email] = request.params[:email]
    request.params
  end

2) Rewrite params_auth_hash method (private in authenticatable strategy by Devise) in the same place:

  def params_auth_hash
    params[scope][:email] = params[:email]
    params[scope]
    # just return full params and not [scope] here in your case?
  end

3) Changing request params in routes

Dirty hack by altering request parameters in rails route custom constraint (config/routes.rb). Add following before devise_for :<model_name>

post '/users/sign_in', constraints: lambda { |request| 
  if URL_IS_FOR_SSO? && request.params[:password] && request.params[:email]
      request.params[:user] = {password: request.params[:password], email: request.params[:email]}
  end
  false #doesn't apply this route, proceed to further routing
}

4) (best if working) Change request parameters in application controller before_action

You can also change name of the parameters passed in the request to satisfy Devise convention and then use default Devise strategy. You can use some method of rewriting, the simplest is putting filter inside ApplicationController.

Important - as a source of confusion, params method of ActionController is relevant to StrongParamaters concept and completely different from request.params (which is basically just a hash).

class ApplicationController < ActionController::Base
  ...
  before_action :rewrite_param_names

  private

  # !! important - use request.params and not just params!!
  def rewrite_param_names
    if URL_IS_FOR_SSO? && request.params[:password] && request.params[:email]
      request.params[:user] = {password: request.params[:password], email: request.params[:email]}
    end
  end
end

This works because ActionDispatch::Request store reference to parameters hash in env header and allows updating this hash. source:

   # Returns both GET and POST \parameters in a single hash.
      def parameters
        params = get_header("action_dispatch.request.parameters")
        return params if params

         ...
        set_header("action_dispatch.request.parameters", params)
        params
      end

To make before_action more focused, you can create custom controller for Devise and set it in parent_controller setting for Devise

 # The parent controller all Devise controllers inherits from.
 # Defaults to ApplicationController. This should be set early
 # in the initialization process and should be set to a string.
 mattr_accessor :parent_controller
 @parent_controller = "ApplicationController"
like image 162
Pavel Bulanov Avatar answered Dec 20 '25 20:12

Pavel Bulanov