TokenAuthenticatable now works with HTTP Basic Auth by default (take a look at Highrise API for a good example). This basically allows you to pass the authentication token as HTTP Basic Auth username.

This commit is contained in:
José Valim 2010-04-01 19:09:33 +02:00
parent 2b5a068246
commit f5d01c217d
7 changed files with 131 additions and 42 deletions

View File

@ -3,7 +3,6 @@
* enhancements
* Rails 3 compatibility.
* All controllers and views are namespaced, for example: Devise::SessionsController and "devise/sessions".
* You can specify the controller in routes and have specific controllers for each role.
* Devise.orm is deprecated. This reduces the required API to hook your ORM with devise.
* Use metal for failure app.
* HTML e-mails now have proper formatting.
@ -15,6 +14,7 @@
* Allow to specify haml in devise_views.
* Compatibility with Datamapper and Mongoid.
* Make config.devise available on config/application.rb.
* TokenAuthenticatable now works with HTTP Basic Auth.
* Allow :unlock_strategy to be :none and add :lock_strategy which can be :failed_attempts or
none. Setting those values to :none means that you want to handle lock and unlocking by
yourself.

View File

@ -64,6 +64,10 @@ module Devise
mattr_accessor :http_authenticatable
@@http_authenticatable = true
# If params authenticatable is enabled by default.
mattr_accessor :params_authenticatable
@@params_authenticatable = true
# The realm used in Http Basic Authentication.
mattr_accessor :http_authentication_realm
@@http_authentication_realm = "Application"

View File

@ -10,6 +10,10 @@ module Devise
# authentication_keys: parameters used for authentication. By default [:email].
#
# http_authenticatable: if this model allows http authentication. By default true.
# It also accepts an array specifying the strategies that should allow http.
#
# params_authenticatable: if this model allows authentication through request params. By default true.
# It also accepts an array specifying the strategies that should allow params authentication.
#
module Authenticatable
extend ActiveSupport::Concern
@ -21,9 +25,17 @@ module Devise
end
module ClassMethods
Devise::Models.config(self, :authentication_keys, :http_authenticatable)
Devise::Models.config(self, :authentication_keys, :http_authenticatable, :params_authenticatable)
alias :http_authenticatable? :http_authenticatable
def params_authenticatable?(strategy)
params_authenticatable.is_a?(Array) ?
params_authenticatable.include?(strategy) : params_authenticatable
end
def http_authenticatable?(strategy)
http_authenticatable.is_a?(Array) ?
http_authenticatable.include?(strategy) : http_authenticatable
end
# Find first record based on conditions given (ie by the sign in form).
# Overwrite to add customized conditions, create a join, or maybe use a

View File

@ -18,7 +18,8 @@ module Devise
# User.find(1).valid_authentication_token?('rI1t6PKQ8yP7VetgwdybB') # returns true/false
#
module TokenAuthenticatable
extend ActiveSupport::Concern
extend ActiveSupport::Concern
include Devise::Models::Authenticatable
included do
before_save :ensure_authentication_token

View File

@ -26,45 +26,72 @@ module Devise
end
end
# Check if this is strategy is valid for http authentication.
def valid_for_http_auth?
mapping.to.http_authenticatable? && request.authorization && set_http_auth_hash
http_authenticatable? && request.authorization && with_authentication_hash(http_auth_hash)
end
# Check if this is strategy is valid for params authentication.
def valid_for_params_auth?
valid_controller? && valid_params? && set_params_auth_hash
params_authenticatable? && valid_controller? &&
valid_params? && with_authentication_hash(params_auth_hash)
end
# Check if the model accepts this strategy as http authenticatable.
def http_authenticatable?
mapping.to.http_authenticatable?(authenticatable_name)
end
# Check if the model accepts this strategy as params authenticatable.
def params_authenticatable?
mapping.to.params_authenticatable?(authenticatable_name)
end
# Extract the appropriate subhash for authentication from params.
def params_auth_hash
params[scope]
end
# Extract a hash with attributes:values from the http params.
def http_auth_hash
keys = [authentication_keys.first, :password]
Hash[*keys.zip(decode_credentials).flatten]
end
# Check if the controller is valid for params authentication.
def valid_controller?
mapping.controllers[:sessions] == params[:controller]
end
# Check if the params_auth_hash is valid for params authentication.
def valid_params?
params[scope].is_a?(Hash)
end
def set_http_auth_hash
keys = [authentication_keys.first, :password]
with_authentication_hash Hash[*keys.zip(decode_credentials).flatten]
params_auth_hash.is_a?(Hash)
end
# Helper to decode credentials from HTTP.
def decode_credentials
username_and_password = request.authorization.split(' ', 2).last || ''
ActiveSupport::Base64.decode64(username_and_password).split(/:/, 2)
end
def set_params_auth_hash
with_authentication_hash params[scope]
end
# Sets the authentication hash and the password from params_auth_hash or http_auth_hash.
def with_authentication_hash(hash)
self.authentication_hash = hash.slice(*authentication_keys)
self.password = hash[:password]
authentication_keys.all?{ |k| authentication_hash[k].present? } && password.present?
authentication_keys.all?{ |k| authentication_hash[k].present? }
end
# Holds the authentication keys.
def authentication_keys
@authentication_keys ||= mapping.to.authentication_keys
end
# Holds the authenticatable name for this class. Devise::Strategies::DatabaseAuthenticatable
# becomes simply :database.
def authenticatable_name
@authenticatable_name ||=
self.class.name.split("::").last.underscore.sub("_authenticatable", "").to_sym
end
end
end
end

View File

@ -2,33 +2,42 @@ require 'devise/strategies/base'
module Devise
module Strategies
# Strategy for signing in a user, based on a authenticatable token.
# Redirects to sign_in page if it's not authenticated.
class TokenAuthenticatable < Base
def valid?
authentication_token(scope).present?
end
# Authenticate a user based on authenticatable token params, returning to warden
# success and the authenticated user if everything is okay. Otherwise redirect
# to sign in page.
# Strategy for signing in a user, based on a authenticatable token. This works for both params
# and http. For the former, all you need to do is to pass the params in the URL:
#
# http://myapp.example.com/?user_token=SECRET
#
# For HTTP, you can pass the token as username. Since some clients may require a password,
# you can pass anything and it will simply be ignored.
class TokenAuthenticatable < Authenticatable
def authenticate!
if resource = mapping.to.authenticate_with_token(params[scope] || params)
if resource = mapping.to.authenticate_with_token(authentication_hash)
success!(resource)
else
fail!(:invalid_token)
fail(:invalid_token)
end
end
private
# Detect authentication token in params: scoped or not.
def authentication_token(scope)
if params[scope]
params[scope][mapping.to.token_authentication_key]
else
params[mapping.to.token_authentication_key]
end
# TokenAuthenticatable params can be given to any controller.
def valid_controller?
true
end
# Do not use remember_me behavir with token.
def remember_me?
false
end
# Try both scoped and non scoped keys.
def params_auth_hash
params[scope] || params
end
# Overwrite authentication keys to use token_authentication_key.
def authentication_keys
@authentication_keys ||= [mapping.to.token_authentication_key]
end
end
end

View File

@ -2,9 +2,9 @@ require 'test_helper'
class TokenAuthenticationTest < ActionController::IntegrationTest
test 'sign in should authenticate with valid authentication token and proper authentication token key' do
test 'authenticate with valid authentication token key and value through params' do
swap Devise, :token_authentication_key => :secret_token do
sign_in_as_new_user_with_token(:auth_token_key => :secret_token)
sign_in_as_new_user_with_token
assert_response :success
assert_template 'users/index'
@ -13,7 +13,38 @@ class TokenAuthenticationTest < ActionController::IntegrationTest
end
end
test 'signing in with valid authentication token - but improper authentication token key - return to sign in form with error message' do
test 'authenticate with valid authentication token key and value through http' do
swap Devise, :token_authentication_key => :secret_token do
sign_in_as_new_user_with_token(:http_auth => true)
assert_response :success
assert_template 'users/index'
assert_contain 'Welcome'
assert warden.authenticated?(:user)
end
end
test 'does authenticate with valid authentication token key and value through params if not configured' do
swap Devise, :token_authentication_key => :secret_token, :params_authenticatable => [:database] do
sign_in_as_new_user_with_token
assert_contain 'You need to sign in or sign up before continuing'
assert_contain 'Sign in'
assert_not warden.authenticated?(:user)
end
end
test 'does authenticate with valid authentication token key and value through http if not configured' do
swap Devise, :token_authentication_key => :secret_token, :http_authenticatable => [:database] do
sign_in_as_new_user_with_token(:http_auth => true)
assert_response 401
assert_contain 'Invalid email or password.'
assert_not warden.authenticated?(:user)
end
end
test 'does not authenticate with improper authentication token key' do
swap Devise, :token_authentication_key => :donald_duck_token do
sign_in_as_new_user_with_token(:auth_token_key => :secret_token)
assert_current_path new_user_session_path(:unauthenticated => true)
@ -24,12 +55,11 @@ class TokenAuthenticationTest < ActionController::IntegrationTest
end
end
test 'signing in with invalid authentication token should return to sign in form with error message' do
test 'does not authenticate with improper authentication token value' do
store_translations :en, :devise => {:sessions => {:invalid_token => 'LOL, that was not a single character correct.'}} do
sign_in_as_new_user_with_token(:auth_token => '*** INVALID TOKEN ***')
assert_current_path new_user_session_path(:invalid_token => true)
assert_response :success
assert_contain 'LOL, that was not a single character correct.'
assert_contain 'Sign in'
assert_not warden.authenticated?(:user)
@ -46,7 +76,13 @@ class TokenAuthenticationTest < ActionController::IntegrationTest
user.authentication_token = VALID_AUTHENTICATION_TOKEN
user.save
visit users_path(options[:auth_token_key].to_sym => options[:auth_token])
if options[:http_auth]
header = "Basic #{ActiveSupport::Base64.encode64("#{VALID_AUTHENTICATION_TOKEN}:X")}"
get users_path, {}, "HTTP_AUTHORIZATION" => header
else
visit users_path(options[:auth_token_key].to_sym => options[:auth_token])
end
user
end