minimalist_authentication 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.gitignore +5 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README +45 -0
  4. data/Rakefile +35 -0
  5. data/init.rb +2 -0
  6. data/lib/app/views/sessions/_form.html.erb +12 -0
  7. data/lib/app/views/sessions/new.html.erb +1 -0
  8. data/lib/minimalist/authentication.rb +124 -0
  9. data/lib/minimalist/authorization.rb +49 -0
  10. data/lib/minimalist/sessions.rb +59 -0
  11. data/lib/minimalist/test_helper.rb +12 -0
  12. data/lib/minimalist/version.rb +3 -0
  13. data/lib/minimalist_authentication.rb +4 -0
  14. data/lib/tasks/minimalist_authentication_tasks.rake +4 -0
  15. data/minimalist_authentication.gemspec +15 -0
  16. data/test/.gitignore +1 -0
  17. data/test/authentication_test.rb +99 -0
  18. data/test/authorization_test.rb +77 -0
  19. data/test/factories.rb +8 -0
  20. data/test/rails_root/Gemfile +33 -0
  21. data/test/rails_root/Gemfile.lock +79 -0
  22. data/test/rails_root/README +256 -0
  23. data/test/rails_root/Rakefile +7 -0
  24. data/test/rails_root/app/controllers/application_controller.rb +5 -0
  25. data/test/rails_root/app/controllers/sessions_controller.rb +3 -0
  26. data/test/rails_root/app/helpers/application_helper.rb +2 -0
  27. data/test/rails_root/app/models/user.rb +3 -0
  28. data/test/rails_root/app/views/layouts/application.html.erb +14 -0
  29. data/test/rails_root/config.ru +4 -0
  30. data/test/rails_root/config/application.rb +42 -0
  31. data/test/rails_root/config/boot.rb +13 -0
  32. data/test/rails_root/config/database.yml +22 -0
  33. data/test/rails_root/config/environment.rb +5 -0
  34. data/test/rails_root/config/environments/development.rb +26 -0
  35. data/test/rails_root/config/environments/production.rb +49 -0
  36. data/test/rails_root/config/environments/test.rb +35 -0
  37. data/test/rails_root/config/initializers/backtrace_silencers.rb +7 -0
  38. data/test/rails_root/config/initializers/inflections.rb +10 -0
  39. data/test/rails_root/config/initializers/mime_types.rb +5 -0
  40. data/test/rails_root/config/initializers/secret_token.rb +7 -0
  41. data/test/rails_root/config/initializers/session_store.rb +8 -0
  42. data/test/rails_root/config/locales/en.yml +5 -0
  43. data/test/rails_root/config/routes.rb +5 -0
  44. data/test/rails_root/db/.gitignore +2 -0
  45. data/test/rails_root/db/schema.rb +21 -0
  46. data/test/rails_root/db/seeds.rb +7 -0
  47. data/test/rails_root/doc/README_FOR_APP +2 -0
  48. data/test/rails_root/lib/tasks/.gitkeep +0 -0
  49. data/test/rails_root/log/.gitignore +1 -0
  50. data/test/rails_root/log/.gitkeep +0 -0
  51. data/test/rails_root/script/rails +6 -0
  52. data/test/rails_root/test/performance/browsing_test.rb +9 -0
  53. data/test/rails_root/test/test_helper.rb +13 -0
  54. data/test/sessions_test.rb +30 -0
  55. data/test/test_helper.rb +11 -0
  56. metadata +161 -0
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ coverage
2
+ .svn
3
+ .loadpath
4
+ .project
5
+ .redcar
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 'WWIDEA, INC'
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,45 @@
1
+ MinimalistAuthentication
2
+ ========================
3
+
4
+ A Rails authentication plugin that takes a minimalist approach. It is designed to be simple to understand, use, and modify for your application.
5
+
6
+ This plugin was largely inspired by the restful-authentication plugin (http://github.com/technoweenie/restful-authentication/tree/master). I selected the essential methods for password based authentication, reorganized them, trimmed them down when possible, added a couple of features, and resisted the urge to start adding more.
7
+
8
+
9
+ Installation
10
+ ============
11
+ script/plugin install git://github.com/aaron/minimalist_authentication.git
12
+
13
+ ruby script/generate scaffold user active:boolean email:string crypted_password:string salt:string using_digest_version:integer last_logged_in_at:datetime
14
+
15
+
16
+ Example
17
+ =======
18
+
19
+ app/models/user.rb
20
+ class User < ActiveRecord::Base
21
+ include Minimalist::Authentication
22
+ end
23
+
24
+ app/controllers/application.rb
25
+ class ApplicationController < ActionController::Base
26
+ include Minimalist::Authorization
27
+
28
+ # Lock down everything by default
29
+ # use skip_before_filter to open up sepecific actions
30
+ prepend_before_filter :authorization_required
31
+ end
32
+
33
+ app/controllers/sessions_controller.rb
34
+ class SessionsController < ApplicationController
35
+ include Minimalist::Sessions
36
+ skip_before_filter :authorization_required, :only => [:new, :create]
37
+ end
38
+
39
+ test/test_helper.rb
40
+ class Test::Unit::TestCase
41
+ include Minimalist::TestHelper
42
+ end
43
+
44
+
45
+ Copyright (c) 2009 Aaron Baldwin, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the minimalist_authentication plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.libs << 'test'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
15
+
16
+ desc 'Generate documentation for the minimalist_authentication plugin.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'MinimalistAuthentication'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
25
+ desc 'Measures test coverage using rcov'
26
+ task :rcov do
27
+ rm_f "coverage"
28
+ rcov = "rcov --rails --text-summary -Ilib --exclude /gems/,/app/,/Library/"
29
+ system("#{rcov} --html #{Dir.glob('test/**/*_test.rb').join(' ')}")
30
+ if PLATFORM['darwin'] #Mac
31
+ system("open coverage/index.html")
32
+ elsif PLATFORM[/linux/] #Ubuntu, etc.
33
+ system("/etc/alternatives/x-www-browser coverage/index.html")
34
+ end
35
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ # Include hook code here
2
+ require 'minimalist_authentication'
@@ -0,0 +1,12 @@
1
+ <%= form_tag session_path do %>
2
+ <div>
3
+ <%= label_tag 'email' %>
4
+ <%= text_field_tag 'email', @email %>
5
+ </div>
6
+ <div>
7
+ <%= label_tag 'password' %>
8
+ <%= password_field_tag 'password', nil %>
9
+ </div>
10
+
11
+ <p><%= submit_tag 'Log in', :class =>'submit' %></p>
12
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= render :partial => 'form' %>
@@ -0,0 +1,124 @@
1
+ require 'digest/sha1'
2
+
3
+ module Minimalist
4
+ module Authentication
5
+ GUEST_USER_EMAIL = 'guest'
6
+ PREFERRED_DIGEST_VERSION = 2
7
+
8
+ def self.included( base )
9
+ base.extend(ClassMethods)
10
+ base.class_eval do
11
+ include InstanceMethods
12
+
13
+ attr_accessor :password
14
+ before_save :encrypt_password
15
+
16
+ validates_presence_of :email, :if => :validate_email_presence?
17
+ validates_uniqueness_of :email, :if => :validate_email_uniqueness?
18
+ validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :if => :validate_email_format?
19
+ validates_presence_of :password, :if => :password_required?
20
+ validates_confirmation_of :password, :if => :password_required?
21
+ validates_length_of :password, :within => 6..40, :if => :password_required?
22
+
23
+ scope :active, :conditions => {:active => true}
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+ def authenticate(email, password)
29
+ return if email.blank? || password.blank?
30
+ user = active.first(:conditions => {:email => email})
31
+ return unless user && user.authenticated?(password)
32
+ return user
33
+ end
34
+
35
+ def secure_digest(string, salt, version = 1)
36
+ case version
37
+ when 0 then Digest::MD5.hexdigest(string.to_s)
38
+ when 1 then Digest::SHA1.hexdigest("#{string}--#{salt}")
39
+ when 2 then Digest::SHA2.hexdigest("#{string}#{salt}", 512)
40
+ end
41
+ end
42
+
43
+ def make_token
44
+ secure_digest(Time.now, (1..10).map{ rand.to_s.gsub(/0\./,'') }.join, PREFERRED_DIGEST_VERSION)
45
+ end
46
+
47
+ def guest
48
+ new.tap do |user|
49
+ user.email = GUEST_USER_EMAIL
50
+ end
51
+ end
52
+ end
53
+
54
+ module InstanceMethods
55
+
56
+ def active?
57
+ active
58
+ end
59
+
60
+ def authenticated?(password)
61
+ if crypted_password == encrypt(password)
62
+ if self.respond_to?(:using_digest_version) and using_digest_version != PREFERRED_DIGEST_VERSION
63
+ new_salt = self.class.make_token
64
+ self.update_attribute(:crypted_password,self.class.secure_digest(password, new_salt, PREFERRED_DIGEST_VERSION))
65
+ self.update_attribute(:salt, new_salt)
66
+ self.update_attribute(:using_digest_version, PREFERRED_DIGEST_VERSION)
67
+ end
68
+ return true
69
+ else
70
+ return false
71
+ end
72
+ end
73
+
74
+ def logged_in
75
+ self.class.update_all("last_logged_in_at='#{Time.now.to_s(:db)}'", "id=#{self.id}") # use update_all to avoid updated_on trigger
76
+ end
77
+
78
+ def is_guest?
79
+ email == GUEST_USER_EMAIL
80
+ end
81
+
82
+ #######
83
+ private
84
+ #######
85
+
86
+ def password_required?
87
+ active? && (crypted_password.blank? || !password.blank?)
88
+ end
89
+
90
+ def encrypt(password)
91
+ self.class.secure_digest(password, salt, digest_version)
92
+ end
93
+
94
+ def encrypt_password
95
+ return if password.blank?
96
+ self.salt = self.class.make_token if new_record?
97
+ self.crypted_password = self.class.secure_digest(password, salt, (self.respond_to?(:using_digest_version) ? PREFERRED_DIGEST_VERSION : 1))
98
+ self.using_digest_version = PREFERRED_DIGEST_VERSION if self.respond_to?(:using_digest_version)
99
+ end
100
+
101
+ def digest_version
102
+ self.respond_to?(:using_digest_version) ? (using_digest_version || 1) : 1
103
+ end
104
+
105
+ # email validation
106
+ def validate_email?
107
+ # allows applications to turn off email validation
108
+ true
109
+ end
110
+
111
+ def validate_email_presence?
112
+ validate_email? && active?
113
+ end
114
+
115
+ def validate_email_format?
116
+ validate_email? && active?
117
+ end
118
+
119
+ def validate_email_uniqueness?
120
+ validate_email? && active?
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,49 @@
1
+ module Minimalist
2
+ module Authorization
3
+ def self.included( base )
4
+ base.class_eval do
5
+ include InstanceMethods
6
+ helper_method :current_user, :logged_in?, :authorized?
7
+ end
8
+ end
9
+
10
+ module InstanceMethods
11
+ #######
12
+ private
13
+ #######
14
+
15
+ def current_user
16
+ @current_user ||= (get_user_from_session || User.guest)
17
+ end
18
+
19
+ def get_user_from_session
20
+ User.find_by_id(session[:user_id]) if session[:user_id]
21
+ end
22
+
23
+ def authorization_required
24
+ authorized? || access_denied
25
+ end
26
+
27
+ def authorized?(action = action_name, resource = controller_name)
28
+ logged_in?
29
+ end
30
+
31
+ def logged_in?
32
+ !current_user.is_guest?
33
+ end
34
+
35
+ def access_denied
36
+ store_location if request.method == :get && !logged_in?
37
+ redirect_to new_session_path
38
+ end
39
+
40
+ def store_location
41
+ session[:return_to] = request.request_uri
42
+ end
43
+
44
+ def redirect_back_or_default(default)
45
+ redirect_to(session.delete(:return_to) || default)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,59 @@
1
+ module Minimalist
2
+ module Sessions
3
+ def self.included( base )
4
+ base.class_eval do
5
+ include InstanceMethods
6
+ append_view_path File.join(File.dirname(__FILE__), '..', '/app/views')
7
+ end
8
+ end
9
+
10
+ module InstanceMethods
11
+ def show
12
+ redirect_to new_session_path
13
+ end
14
+
15
+ def new
16
+ end
17
+
18
+ def create
19
+ if user = User.authenticate(params[:email], params[:password])
20
+ user.logged_in
21
+ session[:user_id] = user.id
22
+ after_authentication(user)
23
+ redirect_back_or_default(login_redirect_to(user))
24
+ return
25
+ else
26
+ after_authentication_failure(user)
27
+ flash.now[:error] = "Couldn't log you in as '#{params[:email]}'"
28
+ render :action => 'new'
29
+ end
30
+ end
31
+
32
+ def destroy
33
+ session[:user_id] = nil
34
+ flash[:notice] = "You have been logged out."
35
+ redirect_to logout_redirect_to
36
+ end
37
+
38
+ #######
39
+ private
40
+ #######
41
+
42
+ def login_redirect_to(user)
43
+ '/'
44
+ end
45
+
46
+ def logout_redirect_to
47
+ '/'
48
+ end
49
+
50
+ def after_authentication(user)
51
+ # overide in application
52
+ end
53
+
54
+ def after_authentication_failure(user)
55
+ # overide in application
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,12 @@
1
+ module Minimalist
2
+ module TestHelper
3
+ # Sets the current user in the session from the user fixtures.
4
+ def login_as(user)
5
+ @request.session[:user_id] = user ? users(user).id : nil
6
+ end
7
+
8
+ def current_user
9
+ @current_user ||= User.find(@request.session[:user_id])
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module MinimalistAuthentication
2
+ VERSION = '0.5'
3
+ end
@@ -0,0 +1,4 @@
1
+ # MinimalistAuthentication
2
+ require 'minimalist/authentication'
3
+ require 'minimalist/authorization'
4
+ require 'minimalist/sessions'
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :minimalist_authentication do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,15 @@
1
+ require File.expand_path('../lib/minimalist/version', __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "minimalist_authentication"
5
+ s.version = MinimalistAuthentication::VERSION
6
+ s.platform = Gem::Platform::RUBY
7
+ s.authors = ['Aaron Baldwin', 'Jonathan S. Garvin', 'WWIDEA, Inc']
8
+ s.email = ["developers@wwidea.org"]
9
+ s.homepage = "https://github.com/wwidea/minimalist_authentication"
10
+ s.summary = %q{A Rails authentication plugin that takes a minimalist approach.}
11
+ s.description = %q{A Rails authentication plugin that takes a minimalist approach. It is designed to be simple to understand, use, and modify for your application.}
12
+ s.files = `git ls-files`.split("\n")
13
+ s.test_files = `git ls-files -- test/*`.split("\n")
14
+ s.require_paths = ["lib"]
15
+ end
data/test/.gitignore ADDED
@@ -0,0 +1 @@
1
+ debug.log
@@ -0,0 +1,99 @@
1
+ require 'test_helper'
2
+
3
+ class AuthenticationTest < ActiveSupport::TestCase
4
+
5
+ test "should not be able to set crypted_password through mass assignment" do
6
+ user = Factory(:user)
7
+ old_crypted_password = user.crypted_password
8
+ user.update_attributes(:crypted_password => 'should not work')
9
+ assert_equal(old_crypted_password, user.crypted_password)
10
+ end
11
+
12
+ test "should return active user" do
13
+ user = Factory(:user)
14
+ assert_equal([user], User.active)
15
+ end
16
+
17
+ test "should authenticate user" do
18
+ user = Factory(:user)
19
+ assert_equal(user, User.authenticate(user.email, 'password'))
20
+ end
21
+
22
+ test "should fail to authenticate when email is blank" do
23
+ user = Factory(:user)
24
+ assert_nil(User.authenticate('', 'password'))
25
+ end
26
+
27
+ test "should fail to authenticate when password is blank" do
28
+ user = Factory(:user)
29
+ assert_nil(User.authenticate(user.email, ''))
30
+ end
31
+
32
+ test "should fail to authenticate when user is not active" do
33
+ user = Factory(:user, :active => false)
34
+ assert_nil(User.authenticate(user.email, 'password'))
35
+ end
36
+
37
+ test "should fail to authenticate for incorrect password" do
38
+ user = Factory(:user)
39
+ assert_nil(User.authenticate(user.email, 'incorrect_password'))
40
+ end
41
+
42
+ test "should create salt and encrypted_password for new user" do
43
+ user = User.new(:email => 'test@testing.com', :password => 'testing')
44
+ assert(user.save)
45
+ assert_not_nil(user.salt)
46
+ assert_not_nil(user.crypted_password)
47
+ assert(user.authenticated?('testing'))
48
+ end
49
+
50
+ test "should update last_logged_in_at without updating updated_at timestamp" do
51
+ user = Factory(:user, :updated_at => 1.day.ago)
52
+ updated_at = user.updated_at
53
+ user.logged_in
54
+ assert(user.updated_at == updated_at)
55
+ end
56
+
57
+ test "guest should be guest" do
58
+ assert(User.guest.is_guest?)
59
+ end
60
+
61
+ test "should allow inactive user to pass validation without an email or password" do
62
+ assert(User.new.valid?)
63
+ end
64
+
65
+ test "should fail validation for active user without email" do
66
+ user = User.new(:active => true)
67
+ assert_equal(false, user.valid?)
68
+ assert(user.errors[:email])
69
+ end
70
+
71
+ test "should fail validation for active user without password" do
72
+ user = User.new(:active => true)
73
+ assert_equal(false, user.valid?)
74
+ assert(user.errors[:password])
75
+ end
76
+
77
+ test "should use latest digest version for new users" do
78
+ assert_equal(User::PREFERRED_DIGEST_VERSION,Factory(:user).using_digest_version)
79
+ end
80
+
81
+ test "should migrate legacy users to new digest version" do
82
+ #Setup a user using the old digest version.
83
+ #This wouldn't be necessary with fixtures.
84
+ legacy_user = User.create(:active => true, :email => 'legacy@user.com', :password => '123456', :password_confirmation => '123456')
85
+ legacy_user.password = nil
86
+ legacy_user.salt = 'my_salt'
87
+ legacy_user.crypted_password = User.secure_digest('my_password', 'my_salt', 1)
88
+ legacy_user.using_digest_version = nil
89
+ assert(legacy_user.save)
90
+ assert_equal(nil, legacy_user.reload.using_digest_version)
91
+ assert_equal('86f156baf9e4868e6dcf910b65775efdeaa347d8',legacy_user.crypted_password)
92
+
93
+ # Ok, now we can finally do the test.
94
+ legacy_crypted_password = legacy_user.crypted_password
95
+ assert(legacy_user.authenticated?('my_password'))
96
+ assert_equal(2,legacy_user.reload.using_digest_version)
97
+ assert_not_equal(legacy_crypted_password,legacy_user.crypted_password)
98
+ end
99
+ end