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 ?
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"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With