diff --git a/MIT-LICENSE b/MIT-LICENSE new file mode 100644 index 00000000..9376605b --- /dev/null +++ b/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2009 [name of plugin creator] + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..c336dea6 --- /dev/null +++ b/Rakefile @@ -0,0 +1,23 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the devise plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the devise plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'Devise' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/init.rb b/init.rb new file mode 100644 index 00000000..dcdc597a --- /dev/null +++ b/init.rb @@ -0,0 +1,2 @@ +require 'devise' + diff --git a/lib/devise.rb b/lib/devise.rb new file mode 100644 index 00000000..989728cc --- /dev/null +++ b/lib/devise.rb @@ -0,0 +1,2 @@ +require 'devise/authenticable' + diff --git a/lib/devise/authenticable.rb b/lib/devise/authenticable.rb new file mode 100644 index 00000000..63d7d6ad --- /dev/null +++ b/lib/devise/authenticable.rb @@ -0,0 +1,55 @@ +module Devise + module Authenticable + require 'digest/sha1' + + # Password digest config + # Auth key for encrypting password + SECURE_AUTH_SITE_KEY = '23c64df433d9b08e464db5c05d1e6202dd2823f0' + # Times digest will be applied to crypted password + SECURE_AUTH_DIGEST_STRETCHES = 10 + + def self.included(base) + base.class_eval do + #attr_accessor :password, :password_confirmation + attr_reader :password + attr_accessor :password_confirmation + attr_accessible :email, :password, :password_confirmation + end + end + + # Defines the new password, generating a salt and encrypting it. + # + def password=(new_password) + if new_password != @password + @password = new_password + if @password.present? + generate_salt + encrypt_password + end + end + end + + private + + # Generate password salt using SHA1 based on password and Time.now + # + def generate_salt + self.password_salt = secure_digest(Time.now.utc, password) if password_salt.blank? + end + + # Encrypt password using SHA1 based on salt, password and SECURE_AUTH_SITE_KEY + # + def encrypt_password + self.encrypted_password = secure_digest(password_salt, SECURE_AUTH_SITE_KEY, password) + end + + # Generate a SHA1 digest joining args. Generated token is something like + # + # --arg1--arg2--arg3--argN-- + # + def secure_digest(*tokens) + ::Digest::SHA1.hexdigest('--' << tokens.flatten.join('--') << '--') + end + end +end + diff --git a/test/authenticable_test.rb b/test/authenticable_test.rb new file mode 100644 index 00000000..c4f4397d --- /dev/null +++ b/test/authenticable_test.rb @@ -0,0 +1,108 @@ +require 'test_helper' +require 'digest/sha1' + +class AuthenticableTest < ActiveSupport::TestCase + + def valid_attributes(attributes={}) + { :email => 'test@email.com', + :password => '12345', + :password_confirmation => '12345' }.update(attributes) + end + + def new_user(attributes={}) + User.new(valid_attributes(attributes)) + end + + def create_user(attributes={}) + User.create!(valid_attributes(attributes)) + end + + def field_accessible?(field) + new_user(field => 'test').send(field) == 'test' + end + + test 'should respond to password and password confirmation' do + user = new_user + assert user.respond_to?(:password) + assert user.respond_to?(:password_confirmation) + end + + test 'should have email acessible' do + assert field_accessible?(:email) + end + + test 'should have password acessible' do + assert field_accessible?(:password) + end + + test 'should have password confirmation accessible' do + assert field_accessible?(:password) + end + + test 'should not have password salt accessible' do + assert_not field_accessible?(:password_salt) + end + + test 'should not have encrypted password accessible' do + assert_not field_accessible?(:encrypted_password) + end + + test 'should generate password salt after set the password' do + assert_present new_user.password_salt + assert_present create_user.password_salt + end + + test 'should not generate salt while setting password to nil or blank string' do + assert_nil new_user(:password => nil).password_salt + assert_nil new_user(:password => '').password_salt + end + + test 'should not change password salt when updating' do + user = create_user + salt = user.password_salt + user.expects(:password_salt=).never + user.save! + assert_equal salt, user.password_salt + end + + test 'should generate a sha1 hash for password salt' do + now = Time.now + Time.stubs(:now).returns(now) + expected_salt = ::Digest::SHA1.hexdigest("--#{now.utc}--#{12345}--") + user = create_user + assert_equal expected_salt, user.password_salt + end + + test 'should generate encrypted password after setting a password' do + assert_present new_user.encrypted_password + assert_present create_user.encrypted_password + end + + test 'should not generate encrypted password while setting password to nil or blank string' do + assert_nil new_user(:password => nil).encrypted_password + assert_nil new_user(:password => '').encrypted_password + end + + test 'should not encrypt password if it didn\'t change' do + user = create_user + encrypted_password = user.encrypted_password + user.expects(:encrypted_password=).never + user.password = '12345' + assert_equal encrypted_password, user.encrypted_password + end + + test 'should encrypt password again if password has changed' do + user = create_user + encrypted_password = user.encrypted_password + user.password = 'new_password' + assert_not_equal encrypted_password, user.encrypted_password + end + + test 'should encrypt password using a sha1 hash' do + digest_key = Devise::Authenticable::SECURE_AUTH_SITE_KEY = 'digest_key' + user = create_user + expected_password = ::Digest::SHA1.hexdigest("--#{user.password_salt}--#{digest_key}--#{12345}--") + assert_equal expected_password, user.encrypted_password + end +end + diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..c21a5449 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,38 @@ +require 'test/unit' +require 'rubygems' +require 'active_support' +require 'active_support/test_case' +require 'active_record' +require 'mocha' + +require File.join(File.dirname(__FILE__), '..', 'lib', 'devise') + +ActiveRecord::Base.logger = Logger.new(nil) +ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:") +ActiveRecord::Schema.define(:version => 1) do + create_table :users do |t| + t.string :email, :null => false + t.string :encrypted_password, :null => false + t.string :password_salt, :null => false + end +end + +class User < ::ActiveRecord::Base + include ::Devise::Authenticable +end + +class ActiveSupport::TestCase + def assert_not(assertion) + assert !assertion + end + + def assert_blank(assertion) + assert assertion.blank? + end + + def assert_not_blank(assertion) + assert !assertion.blank? + end + alias :assert_present :assert_not_blank +end +