1
0
Fork 0
mirror of https://github.com/heartcombo/devise.git synced 2022-11-09 12:18:31 -05:00

Get rid of token authentication

This commit is contained in:
José Valim 2013-09-02 19:15:47 -03:00
parent 07f0ee75ee
commit dff7891b97
17 changed files with 9 additions and 488 deletions

View file

@ -11,7 +11,6 @@ en:
already_authenticated: "You are already signed in."
inactive: "Your account is not activated yet."
invalid: "Invalid email or password."
invalid_token: "Invalid authentication token."
locked: "Your account is locked."
not_found_in_database: "Invalid email or password."
timeout: "Your session expired. Please sign in again to continue."

View file

@ -84,11 +84,6 @@ module Devise
devise_modules_hook! do
include Devise::Models::Authenticatable
if selected_modules.include?(:token_authenticatable)
ActiveSupport::Deprecation.warn "devise :token_authenticatable is deprecated. " \
"Please check Devise 3.1 release notes for more information on how to upgrade."
end
selected_modules.each do |m|
mod = Devise::Models.const_get(m.to_s.classify)

View file

@ -29,9 +29,7 @@ module Devise
# It also accepts an array specifying the strategies that should allow params authentication.
#
# * +skip_session_storage+: By default Devise will store the user in session.
# You can skip storage for http and token auth by appending values to array:
# :skip_session_storage => [:token_auth] or :skip_session_storage => [:http_auth, :token_auth],
# by default is set to :skip_session_storage => [:http_auth].
# By default is set to :skip_session_storage => [:http_auth].
#
# == active_for_authentication?
#

View file

@ -1,92 +0,0 @@
require 'devise/strategies/token_authenticatable'
module Devise
module Models
# The TokenAuthenticatable module is responsible for generating an authentication token and
# validating the authenticity of the same while signing in.
#
# This module only provides a few helpers to help you manage the token, but it is up to you
# to choose how to use it. For example, if you want to have a new token every time the user
# saves his account, you can do the following:
#
# before_save :reset_authentication_token
#
# On the other hand, if you want to generate token unless one exists, you should use instead:
#
# before_save :ensure_authentication_token
#
# If you want to delete the token after it is used, you can do so in the
# after_token_authentication callback.
#
# == APIs
#
# If you are using token authentication with APIs and using trackable. Every
# request will be considered as a new sign in (since there is no session in
# APIs). You can disable this by creating a before filter as follow:
#
# before_filter :skip_trackable
#
# def skip_trackable
# request.env['devise.skip_trackable'] = true
# end
#
# == Options
#
# TokenAuthenticatable adds the following options to devise_for:
#
# * +token_authentication_key+: Defines name of the authentication token params key. E.g. /users/sign_in?some_key=...
#
module TokenAuthenticatable
extend ActiveSupport::Concern
def self.required_fields(klass)
[:authentication_token]
end
# Generate new authentication token (a.k.a. "single access token").
def reset_authentication_token
self.authentication_token = self.class.authentication_token
end
# Generate new authentication token and save the record.
def reset_authentication_token!
reset_authentication_token
save(:validate => false)
end
# Generate authentication token unless already exists.
def ensure_authentication_token
reset_authentication_token if authentication_token.blank?
end
# Generate authentication token unless already exists and save the record.
def ensure_authentication_token!
reset_authentication_token! if authentication_token.blank?
end
# Hook called after token authentication.
def after_token_authentication
end
def expire_auth_token_on_timeout
self.class.expire_auth_token_on_timeout
end
module ClassMethods
def find_for_token_authentication(conditions)
find_for_authentication(:authentication_token => conditions[token_authentication_key])
end
# Generate a token checking if one does not already exist in the database.
def authentication_token
loop do
token = Devise.friendly_token
break token unless to_adapter.find_first({ :authentication_token => token })
end
end
Devise::Models.config(self, :token_authentication_key, :expire_auth_token_on_timeout)
end
end
end
end

View file

@ -5,7 +5,6 @@ Devise.with_options :model => true do |d|
d.with_options :strategy => true do |s|
routes = [nil, :new, :destroy]
s.add_module :database_authenticatable, :controller => :sessions, :route => { :session => routes }
s.add_module :token_authenticatable, :controller => :sessions, :route => { :session => routes }, :no_input => true
s.add_module :rememberable, :no_input => true
end

View file

@ -1,91 +0,0 @@
require 'devise/strategies/base'
module Devise
module Strategies
# 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 headers, you can use basic authentication passing the token as username and
# blank password. Since some clients may require a password, you can pass "X" as
# password and it will simply be ignored.
#
# You may also pass the token using the Token authentication mechanism provided
# by Rails: http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
# The token options are stored in request.env['devise.token_options']
class TokenAuthenticatable < Authenticatable
def store?
super && !mapping.to.skip_session_storage.include?(:token_auth)
end
def valid?
super || valid_for_token_auth?
end
def authenticate!
resource = mapping.to.find_for_token_authentication(authentication_hash)
return fail(:invalid_token) unless resource
if validate(resource)
resource.after_token_authentication
success!(resource)
end
end
private
# Token Authenticatable can be authenticated with params in any controller and any verb.
def valid_params_request?
true
end
# Do not use remember_me behavior with token.
def remember_me?
false
end
# Check if the model accepts this strategy as token authenticatable.
def token_authenticatable?
mapping.to.http_authenticatable?(:token_options)
end
# Check if this is strategy is valid for token authentication by:
#
# * Validating if the model allows http token authentication;
# * If the http auth token exists;
# * If all authentication keys are present;
#
def valid_for_token_auth?
token_authenticatable? && auth_token.present? && with_authentication_hash(:token_auth, token_auth_hash)
end
# Extract the auth token from the request
def auth_token
@auth_token ||= ActionController::HttpAuthentication::Token.token_and_options(request)
end
# Extract a hash with attributes:values from the auth_token
def token_auth_hash
request.env['devise.token_options'] = auth_token.last
{ authentication_keys.first => auth_token.first }
end
# Try both scoped and non scoped keys
def params_auth_hash
if params[scope].kind_of?(Hash) && params[scope].has_key?(authentication_keys.first)
params[scope]
else
params
end
end
# Overwrite authentication keys to use token_authentication_key.
def authentication_keys
@authentication_keys ||= [mapping.to.token_authentication_key]
end
end
end
end
Warden::Strategies.add(:token_authenticatable, Devise::Strategies::TokenAuthenticatable)

View file

@ -47,9 +47,6 @@ module Mongoid
# field :failed_attempts, :type => Integer, :default => 0 # Only if lock strategy is :failed_attempts
# field :unlock_token, :type => String # Only if unlock strategy is :email or :both
# field :locked_at, :type => Time
## Token authenticatable
# field :authentication_token, :type => String
RUBY
end
end

View file

@ -56,12 +56,9 @@ Devise.setup do |config|
# Tell if authentication through HTTP Auth is enabled. False by default.
# It can be set to an array that will enable http authentication only for the
# given strategies, for example, `config.http_authenticatable = [:token]` will
# enable it only for token authentication. The supported strategies are:
# given strategies, for example, `config.http_authenticatable = [:database]` will
# enable it only for database authentication. The supported strategies are:
# :database = Support basic authentication with authentication key + password
# :token = Support basic authentication with token authentication key
# :token_options = Support token authentication with options as defined in
# http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
# config.http_authenticatable = false
# If http headers should be returned for AJAX requests. True by default.
@ -76,7 +73,7 @@ Devise.setup do |config|
# config.paranoid = true
# By default Devise will store the user in session. You can skip storage for
# :http_auth and :token_auth by adding those symbols to the array below.
# particular strategies by setting this option.
# Notice that if you are skipping storage for all authentication paths, you
# may want to disable generating routes to Devise's sessions controller by
# passing :skip => :sessions to `devise_for` in your config/routes.rb
@ -196,10 +193,6 @@ Devise.setup do |config|
# Require the `devise-encryptable` gem when using anything other than bcrypt
# config.encryptor = :sha512
# ==> Configuration for :token_authenticatable
# Defines name of the authentication token params key
# config.token_authentication_key = :auth_token
# ==> Scopes configuration
# Turn scoped views on. Before rendering "sessions/new", it will first check for
# "users/sessions/new". It's turned off by default because it's slower if you

View file

@ -55,7 +55,7 @@ class HelpersTest < ActionController::TestCase
end
test 'require no authentication tests current mapping' do
@mock_warden.expects(:authenticate?).with(:rememberable, :token_authenticatable, :scope => :user).returns(true)
@mock_warden.expects(:authenticate?).with(:rememberable, :scope => :user).returns(true)
@mock_warden.expects(:user).with(:user).returns(User.new)
@controller.expects(:redirect_to).with(root_path)
@controller.send :require_no_authentication
@ -71,7 +71,7 @@ class HelpersTest < ActionController::TestCase
end
test 'require no authentication sets a flash message' do
@mock_warden.expects(:authenticate?).with(:rememberable, :token_authenticatable, :scope => :user).returns(true)
@mock_warden.expects(:authenticate?).with(:rememberable, :scope => :user).returns(true)
@mock_warden.expects(:user).with(:user).returns(User.new)
@controller.expects(:redirect_to).with(root_path)
@controller.send :require_no_authentication

View file

@ -88,16 +88,6 @@ class HttpAuthenticationTest < ActionDispatch::IntegrationTest
end
end
test 'sign in should authenticate with really long token' do
token = "token_containing_so_many_characters_that_the_base64_encoding_will_wrap"
user = create_user
user.update_attribute :authentication_token, token
get users_path(:format => :xml), {}, "HTTP_AUTHORIZATION" => "Basic #{Base64.encode64("#{token}:x")}"
assert_response :success
assert_match "<email>user@test.com</email>", response.body
assert warden.authenticated?(:user)
end
private
def sign_in_as_new_user_with_http(username="user@test.com", password="12345678")

View file

@ -1,205 +0,0 @@
require 'test_helper'
class TokenAuthenticationTest < ActionDispatch::IntegrationTest
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
assert_response :success
assert_current_url "/users?secret_token=#{VALID_AUTHENTICATION_TOKEN}"
assert_contain 'Welcome'
assert warden.authenticated?(:user)
end
end
test 'authenticate with valid authentication token key and value through params, when params with the same key as scope exist' do
swap Devise, :token_authentication_key => :secret_token do
user = create_user_with_authentication_token
post exhibit_user_path(user), Devise.token_authentication_key => user.authentication_token, :user => { :some => "data" }
assert_response :success
assert_contain 'User is authenticated'
assert warden.authenticated?(:user)
end
end
test 'authenticate with valid authentication token key but does not store if stateless' do
swap Devise, :token_authentication_key => :secret_token, :skip_session_storage => [:token_auth] do
sign_in_as_new_user_with_token
assert warden.authenticated?(:user)
get users_path
assert_redirected_to new_user_session_path
assert_not warden.authenticated?(:user)
end
end
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_match '<email>user@test.com</email>', response.body
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_equal new_user_session_path, @request.path
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 not authenticate with improper authentication token value' do
store_translations :en, :devise => {:failure => {:invalid_token => 'LOL, that was not a single character correct.'}} do
sign_in_as_new_user_with_token(:auth_token => '*** INVALID TOKEN ***')
assert_equal new_user_session_path, @request.path
assert_contain 'LOL, that was not a single character correct.'
assert_contain 'Sign in'
assert_not warden.authenticated?(:user)
end
end
test 'authenticate with valid authentication token key and do not store if stateless and timeoutable are enabled' do
swap Devise, :token_authentication_key => :secret_token, :skip_session_storage => [:token_auth], :timeout_in => (0.1).second do
user = sign_in_as_new_user_with_token
assert warden.authenticated?(:user)
# Expiring does not work because we are setting the session value when accessing it
sleep 0.3
get_users_path_as_existing_user(user)
assert warden.authenticated?(:user)
end
end
test 'should reset token and not authenticate when expire_auth_token_on_timeout is set to true, timeoutable is enabled and we have a timed out session' do
swap Devise, :token_authentication_key => :secret_token, :expire_auth_token_on_timeout => true, :timeout_in => (-1).minute do
user = sign_in_as_new_user_with_token
assert warden.authenticated?(:user)
token = user.authentication_token
get_users_path_as_existing_user(user)
assert_not warden.authenticated?(:user)
user.reload
assert_not_equal token, user.authentication_token
end
end
test 'should not be subject to injection' do
swap Devise, :token_authentication_key => :secret_token do
user1 = create_user_with_authentication_token()
# Clean up user cache
@user = nil
user2 = create_user_with_authentication_token(:email => "another@test.com")
user2.update_attribute(:authentication_token, "ANOTHERTOKEN")
assert_not_equal user1, user2
visit users_path(Devise.token_authentication_key.to_s + '[$ne]' => user1.authentication_token)
assert_nil warden.user(:user)
end
end
test 'authenticate with valid authentication token key and value through http header' do
swap Devise, :token_authentication_key => :secret_token do
sign_in_as_new_user_with_token(:token_auth => true)
assert_response :success
assert_match '<email>user@test.com</email>', response.body
assert_equal request.env['devise.token_options'], {}
assert warden.authenticated?(:user)
end
end
test 'authenticate with valid authentication token key and value through http header, with options' do
swap Devise, :token_authentication_key => :secret_token, :http_authenticatable => [:token_options] do
signature = "**TESTSIGNATURE**"
sign_in_as_new_user_with_token(:token_auth => true, :token_options => {:signature => signature, :nonce => 'def'})
assert_response :success
assert_match '<email>user@test.com</email>', response.body
assert_equal request.env['devise.token_options'][:signature], signature
assert_equal request.env['devise.token_options'][:nonce], 'def'
assert warden.authenticated?(:user)
end
end
test 'authenticate with valid authentication token key and value through http header without allowing token authorization setting is denied' do
swap Devise, :token_authentication_key => :secret_token, :http_authenticatable => false do
sign_in_as_new_user_with_token(:token_auth => true)
assert_response :unauthorized
assert_nil warden.user(:user)
end
end
test 'does not authenticate with improper authentication token value in header' do
sign_in_as_new_user_with_token(:token_auth => true, :auth_token => '*** INVALID TOKEN ***')
assert_response :unauthorized
assert_nil warden.user(:user)
end
private
def sign_in_as_new_user_with_token(options = {})
user = options.delete(:user) || create_user_with_authentication_token(options)
options[:auth_token_key] ||= Devise.token_authentication_key
options[:auth_token] ||= user.authentication_token
if options[:http_auth]
header = "Basic #{Base64.encode64("#{VALID_AUTHENTICATION_TOKEN}:X")}"
get users_path(:format => :xml), {}, "HTTP_AUTHORIZATION" => header
elsif options[:token_auth]
token_options = options[:token_options] || {}
header = ActionController::HttpAuthentication::Token.encode_credentials(options[:auth_token], token_options)
get users_path(:format => :xml), {}, "HTTP_AUTHORIZATION" => header
else
visit users_path(options[:auth_token_key].to_sym => options[:auth_token])
end
user
end
def create_user_with_authentication_token(options={})
user = create_user(options)
user.authentication_token = VALID_AUTHENTICATION_TOKEN
user.save
user
end
def get_users_path_as_existing_user(user)
sign_in_as_new_user_with_token(:user => user)
end
end

View file

@ -50,12 +50,12 @@ class MappingTest < ActiveSupport::TestCase
end
test 'has strategies depending on the model declaration' do
assert_equal [:rememberable, :token_authenticatable, :database_authenticatable], Devise.mappings[:user].strategies
assert_equal [:rememberable, :database_authenticatable], Devise.mappings[:user].strategies
assert_equal [:database_authenticatable], Devise.mappings[:admin].strategies
end
test 'has no input strategies depending on the model declaration' do
assert_equal [:rememberable, :token_authenticatable], Devise.mappings[:user].no_input_strategies
assert_equal [:rememberable], Devise.mappings[:user].no_input_strategies
assert_equal [], Devise.mappings[:admin].no_input_strategies
end

View file

@ -1,55 +0,0 @@
require 'test_helper'
class TokenAuthenticatableTest < ActiveSupport::TestCase
test 'should reset authentication token' do
user = new_user
user.reset_authentication_token
previous_token = user.authentication_token
user.reset_authentication_token
assert_not_equal previous_token, user.authentication_token
end
test 'should ensure authentication token' do
user = new_user
user.ensure_authentication_token
previous_token = user.authentication_token
user.ensure_authentication_token
assert_equal previous_token, user.authentication_token
end
test 'should authenticate a valid user with authentication token and return it' do
user = create_user
user.ensure_authentication_token!
user.confirm!
authenticated_user = User.find_for_token_authentication(:auth_token => user.authentication_token)
assert_equal authenticated_user, user
end
test 'should return nil when authenticating an invalid user by authentication token' do
user = create_user
user.ensure_authentication_token!
user.confirm!
authenticated_user = User.find_for_token_authentication(:auth_token => user.authentication_token.reverse)
assert_nil authenticated_user
end
test 'should not be subject to injection' do
user1 = create_user
user1.ensure_authentication_token!
user1.confirm!
user2 = create_user
user2.ensure_authentication_token!
user2.confirm!
user = User.find_for_token_authentication(:auth_token => {'$ne' => user1.authentication_token})
assert_nil user
end
test 'required_fields should contain the fields that Devise uses' do
assert_same_content Devise::Models::TokenAuthenticatable.required_fields(User), [
:authentication_token
]
end
end

View file

@ -36,7 +36,4 @@ class User
field :failed_attempts, :type => Integer, :default => 0 # Only if lock strategy is :failed_attempts
field :unlock_token, :type => String # Only if unlock strategy is :email or :both
field :locked_at, :type => Time
## Token authenticatable
field :authentication_token, :type => String
end

View file

@ -33,9 +33,6 @@ class CreateTables < ActiveRecord::Migration
t.string :unlock_token # Only if unlock strategy is :email or :both
t.datetime :locked_at
## Token authenticatable
t.string :authentication_token
t.timestamps
end

View file

@ -44,7 +44,6 @@ ActiveRecord::Schema.define(:version => 20100401102949) do
t.integer "failed_attempts", :default => 0
t.string "unlock_token"
t.datetime "locked_at"
t.string "authentication_token"
t.datetime "created_at"
t.datetime "updated_at"
end

View file

@ -3,7 +3,7 @@ module SharedUser
included do
devise :database_authenticatable, :confirmable, :lockable, :recoverable,
:registerable, :rememberable, :timeoutable, :token_authenticatable,
:registerable, :rememberable, :timeoutable,
:trackable, :validatable, :omniauthable
attr_accessor :other_key