Toggling Devise authentication modules per controller action
I have a Rails site that uses Devise for authentication. I have one page (PhotosController#create
) that needs to authenticate users without cookies. I'm doing this with the :token_authenticatable
module of Devise开发者_Go百科, which authenticates a user if the supplied token matches the token stored on the server side. (See this SO question if you're curious.)
It's good policy to expire or change the token after the action is complete. This prevents an attacker from sniffing the token and using it to successfully authenticate as the user. However, in my case, I can't expire or change the token because the client-side photo uploader uploads multiple photos, each resulting in a separate POST to PhotosController#create
. So if I expire the token after a successful create, the second, third, etc. uploads will fail.
Devise modules are specified at the model level (e.g. the User
model). I need more granularity than this.
My question is, how do I enable the :token_authenticatable
module only for a single action of a single controller? Or, equivalently, how do I disable the :token_authenticatable
module for all controllers and actions except for one action?
As the developer of one devise plugin (devise_rpx_connectable) I'm happy to answer your question.
TokenAuthenticatable is one Devise strategy, you can read its code here :
https://github.com/plataformatec/devise/blob/master/lib/devise/strategies/token_authenticatable.rb
As you can see, each devise strategy has a valid? and/or valid_request? method that is called to determine if the strategy should be enabled. So you can easily override this strategy for your needs, or you can also only override the valid_request? method. Just load this kind of code in an initializer (AFTER devise is loaded of course) :
module Devise
module Strategies
class TokenAuthenticatable < Authenticatable
private
def valid_request?
params[:controller] == "photos" && params[:action] == "create"
end
end
end
end
I haven't tested this, I don't know if that works out of the box but I hope you see the point, if that doesn't work, use a debugger, or write your own Devise Strategy (see my plugin, it's easy to understand), etc.
Moreover, when you use this strategy, the user will be stored in session unless you use the stateless_token option, see : https://github.com/plataformatec/devise/blob/master/lib/devise/models/token_authenticatable.rb#L27
Put this in an initializer
require 'devise/strategies/base'
require 'devise/strategies/token_authenticatable'
module Devise
module Strategies
class TokenAuthenticatable < Authenticatable
private
def valid_request?
params[:controller] == "photos" && params[:action] == "create"
end
end
end
end
slainer68's approach is good, but it didn't work for me as-is, so I'll add my eventual solution here.
In config/initializers/devise.rb
, which already had a Devise.setup do |config| ... end
block, I prepended the following:
Warden::Strategies.add(:my_token_authenticatable, Devise::Strategies::TokenAuth
def valid?
mapping.to.respond_to?(:authenticate_with_token) && authentication_token(scope).present? && params[:controller] == 'photos' && params[:action] == 'create'
end
end
I also added this to the Devise.setup
block:
config.warden do |manager|
manager.default_strategies.unshift :my_token_authenticatable
end
It would have been better to simply update the existing :token_authenticatable
strategy, but that wasn't loaded into Warden by the time this code was executed. As a result of having to use a strategy with a different name, I had to duplicate some methods and class methods from Devise::Strategies::TokenAuthenticatable
, including:
reset_authentication_token
reset_authentication_token!
self.authenticate_with_token(attributes)
self.token_authentication_key
valid_authentication_token?(incoming_auth_token)
self.find_for_token_authentication(token)
I also had to remove :token_authenticatable from the devise
line at the top of the User model.
I also took slainer68's advice and added :stateless_token => false
to the options for devise_for
in config/routes.rb
.
The simplest solution can be found here: Devise allow token authentication on single controller action My apologies, but I don't know how to mark the question as a possible repeat of, etc. The mentioned solution worked for me using rails 3.2.7.
In summary, it doesn't use def valid_request?
which is private and cannot be called directly from a class outside. (check the base code for a strategy at https://github.com/plataformatec/devise/blob/master/lib/devise/strategies/authenticatable.rb#L15 ) Therefore, not exactly the best place to override. Instead, you should use valid?
and have something like this:
require 'devise/strategies/base'
require 'devise/strategies/token_authenticatable'
module Devise
module Strategies
class TokenAuthenticatable < Authenticatable
def valid?
super && params[:controller] == "your controller" && params[:action] == "your action"
end
end
end
end
精彩评论