vigetlabs-simplest_auth 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile ADDED
@@ -0,0 +1,107 @@
1
+ h1. SimplestAuth
2
+
3
+ simplest_auth is a gem to be used with Rails applications where RESTful Authentication is overkill - it handles authentication and nothing else (e.g. password resets, etc...)
4
+
5
+ simplest_auth is now compatible with both ActiveRecord and DataMapper (the README displays examples for AR)
6
+
7
+ h2. Installation
8
+
9
+ SimplestAuth depends (for now) on the BCrypt gem, so install that first:
10
+
11
+ <pre><code>$ sudo gem install bcrypt-ruby</code></pre>
12
+
13
+ Configure for the gem:
14
+
15
+ <pre><code>config.gem 'vigetlabs-simplest_auth', :lib => 'simplest_auth'</code></pre>
16
+
17
+ h2. Usage
18
+
19
+ SimplestAuth is an extension to the existing models and controllers in your Rails application. It makes some decisions about how you structure your models, but will give you flexibility with naming and any ActiveRecord validations that you want to use.
20
+
21
+ h3. Model Integration
22
+
23
+ If you're starting out with a fresh User model, you just need an identifier such as @email@ and @crypted_password@ columns in your database:
24
+
25
+ <pre><code>$ ./script/generate model User email:string crypted_password:string</code></pre>
26
+
27
+ To get started, just use the @SimplestAuth::Model@ mix-in, and tell it how you want to identify, in your User class:
28
+
29
+ <pre><code>
30
+ class User < ActiveRecord::Base
31
+ include SimplestAuth::Model
32
+
33
+ authenticate_by :email
34
+ end
35
+ </code></pre>
36
+
37
+ The module provides accessors for both @password@ and @password_confirmation@, but you will need to provide the validations required for your application. A @password_required?@ method is defined, as well. Some sane defaults:
38
+
39
+ <pre><code>
40
+ validates_presence_of :email
41
+ validates_uniqueness_of :email
42
+
43
+ validates_presence_of :password, :on => :create
44
+ validates_confirmation_of :password, :if => :password_required?
45
+ </code></pre>
46
+
47
+ Before creating new records, the password is crypted before storing the User in the database.
48
+
49
+ The full model class:
50
+
51
+ <pre><code>
52
+ class User < ActiveRecord::Base
53
+ include SimplestAuth::Model
54
+
55
+ validates_presence_of :email
56
+ validates_uniqueness_of :email
57
+
58
+ validates_presence_of :password, :on => :create
59
+ validates_confirmation_of :password, :if => :password_required?
60
+ end
61
+ </code></pre>
62
+
63
+ h3. Controller
64
+
65
+ To initialize the Controller functionality for use in your application, you need to include it in your @ApplicationController@:
66
+
67
+ <pre><code>
68
+ class ApplicationController < ActionController::Base
69
+ include SimplestAuth::Controller
70
+ end
71
+ </code></pre>
72
+
73
+ The plugin defines the @user_class@ method so that it can find the appropriate object in your application, it defaults to User but can be Account or anything else. Once that is included, you can use the controller methods in your application - logging in, for example:
74
+
75
+ <pre><code>
76
+ class SessionsController < ApplicationController
77
+
78
+ def new; end
79
+
80
+ def create
81
+ if user = User.authenticate(params[:email], params[:password])
82
+ self.current_user = user
83
+ flash[:notice] = 'Welcome!'
84
+ redirect_to root_path
85
+ else
86
+ flash.now[:error] = "Couldn't locate a user with those credentials"
87
+ render :action => :new
88
+ end
89
+ end
90
+ end
91
+ </code></pre>
92
+
93
+ h3. Helpers
94
+
95
+ The plug-in also defines some convenient helpers to use in your views:
96
+
97
+ * *@current_user@*: The user object of the currently logged-in user (or nil if the user isn't logged-in)
98
+ * *@logged_in?@*: Is there a user logged in?
99
+ * *@authorized?@*: Is this user authorized? Defaults to simply checking for logged_in? Override for your authorization scheme.
100
+
101
+ h2. TODO
102
+
103
+ * Document the usage of helper methods (e.g. :logged_in? / :authorized?) in the controller
104
+
105
+ h2. Credits
106
+
107
+ Tony Pitale and Matt Swasey of Viget Labs (http://www.viget.com)
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rake/testtask'
4
+
5
+ require 'lib/simplest_auth/version'
6
+
7
+ task :default => :test
8
+
9
+ spec = Gem::Specification.new do |s|
10
+ s.name = 'simplest_auth'
11
+ s.version = SimplestAuth::Version.to_s
12
+ s.summary = "Simple implementation of authentication for Rails"
13
+ s.author = 'Tony Pitale'
14
+ s.email = 'tony.pitale@viget.com'
15
+ s.homepage = 'http://viget.com/extend'
16
+ s.files = %w(README.textile Rakefile) + Dir.glob("lib/**/*")
17
+ s.test_files = Dir.glob("test/**/*_test.rb")
18
+
19
+ s.add_dependency('bcrypt-ruby', '~> 2.0.5')
20
+ end
21
+
22
+ Rake::GemPackageTask.new(spec) do |pkg|
23
+ pkg.gem_spec = spec
24
+ end
25
+
26
+ Rake::TestTask.new do |t|
27
+ t.libs << 'test'
28
+ t.test_files = FileList["test/**/*_test.rb"]
29
+ t.verbose = true
30
+ end
31
+
32
+ desc 'Generate the gemspec to serve this Gem from Github'
33
+ task :github do
34
+ file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
35
+ File.open(file, 'w') {|f| f << spec.to_ruby }
36
+ puts "Created gemspec: #{file}"
37
+ end
38
+
39
+ begin
40
+ require 'rcov/rcovtask'
41
+
42
+ desc "Generate RCov coverage report"
43
+ Rcov::RcovTask.new(:rcov) do |t|
44
+ t.test_files = FileList['test/**/*_test.rb']
45
+ t.rcov_opts << "-x lib/simplest_auth.rb -x lib/simplest_auth/version.rb"
46
+ end
47
+ rescue LoadError
48
+ nil
49
+ end
50
+
51
+ task :default => 'test'
@@ -0,0 +1,13 @@
1
+ begin
2
+ require 'bcrypt'
3
+ rescue LoadError
4
+ begin
5
+ gem 'bcrypt-ruby'
6
+ rescue Gem::LoadError
7
+ puts "Please install the bcrypt-ruby gem"
8
+ end
9
+ end
10
+
11
+ # SimplestAuth
12
+ require 'simplest_auth/model'
13
+ require 'simplest_auth/controller'
@@ -0,0 +1,69 @@
1
+ module SimplestAuth
2
+ class UndefinedMethodError < StandardError; end
3
+
4
+ module Controller
5
+ def user_class
6
+ User
7
+ end
8
+
9
+ def authorized?
10
+ logged_in?
11
+ end
12
+
13
+ def access_denied
14
+ store_location
15
+ flash[:error] = login_message
16
+ redirect_to new_session_url
17
+ end
18
+
19
+ def login_message
20
+ "Login or Registration Required"
21
+ end
22
+
23
+ def store_location
24
+ session[:return_to] = request.request_uri
25
+ end
26
+
27
+ def redirect_back_or_default(default)
28
+ redirect_to(session[:return_to] || default)
29
+ session[:return_to] = nil
30
+ end
31
+
32
+ def login_required
33
+ authorized? || access_denied
34
+ end
35
+
36
+ def logged_in?
37
+ !current_user_id.nil?
38
+ end
39
+
40
+ def current_user
41
+ if @current_user.nil?
42
+ begin
43
+ @current_user = user_class.find(current_user_id)
44
+ rescue user_class::RecordNotFound
45
+ clear_session
46
+ @current_user = nil
47
+ end
48
+ end
49
+ @current_user
50
+ end
51
+
52
+ def current_user=(user)
53
+ session[:user_id] = user ? user.id : nil
54
+ @current_user = user || false
55
+ end
56
+
57
+ def current_user_id
58
+ session[:user_id]
59
+ end
60
+
61
+ def clear_session
62
+ session[:user_id] = nil
63
+ end
64
+
65
+ def self.included(base)
66
+ base.send :helper_method, :current_user, :logged_in?, :authorized? if base.respond_to? :helper_method
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,79 @@
1
+ module SimplestAuth
2
+ module Model
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.send(:include, InstanceMethods)
6
+
7
+ base.class_eval do
8
+ attr_accessor :password, :password_confirmation
9
+ end
10
+
11
+ if base.active_record?
12
+ base.class_eval do
13
+ before_save :hash_password, :if => :password_required?
14
+ end
15
+ elsif base.data_mapper?
16
+ base.class_eval do
17
+ before :save, :hash_password, :if => :password_required?
18
+ end
19
+ end
20
+ end
21
+
22
+ module ClassMethods
23
+ def active_record?
24
+ defined?(ActiveRecord)
25
+ end
26
+
27
+ def data_mapper?
28
+ defined?(DataMapper)
29
+ end
30
+
31
+ def authenticate(email, password)
32
+ if active_record?
33
+ klass = find_by_email(email)
34
+ elsif data_mapper?
35
+ klass = first(:email => email)
36
+ end
37
+
38
+ (klass && klass.authentic?(password)) ? klass : nil
39
+ end
40
+
41
+ def authenticate_by(ident)
42
+ if active_record?
43
+ instance_eval <<-EOM
44
+ def authenticate(#{ident}, password)
45
+ klass = find_by_#{ident}(#{ident})
46
+ (klass && klass.authentic?(password)) ? klass : nil
47
+ end
48
+ EOM
49
+ elsif data_mapper?
50
+ instance_eval <<-EOM
51
+ def authenticate(#{ident}, password)
52
+ klass = first(:#{ident} => #{ident})
53
+ (klass && klass.authentic?(password)) ? klass : nil
54
+ end
55
+ EOM
56
+ end
57
+ end
58
+ end
59
+
60
+ module InstanceMethods
61
+ include BCrypt
62
+
63
+ RecordNotFound = Class.new(StandardError)
64
+
65
+ def authentic?(password)
66
+ Password.new(self.crypted_password) == password
67
+ end
68
+
69
+ private
70
+ def hash_password
71
+ self.crypted_password = Password.create(self.password) if password_required?
72
+ end
73
+
74
+ def password_required?
75
+ self.crypted_password.blank? || !self.password.blank?
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,13 @@
1
+ module SimplestAuth
2
+ module Version
3
+
4
+ MAJOR = 0
5
+ MINOR = 1
6
+ TINY = 1
7
+
8
+ def self.to_s # :nodoc:
9
+ [MAJOR, MINOR, TINY].join('.')
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ require File.dirname(__FILE__) + '/../../test_helper'
2
+
3
+ ARUser = Class.new
4
+
5
+ class ARUserTest < Test::Unit::TestCase
6
+ include BCrypt
7
+
8
+ context "with ActiveRecord" do
9
+ setup do
10
+ ARUser.stubs(:active_record?).returns(true)
11
+ ARUser.expects(:before_save).with(:hash_password, :if => :password_required?)
12
+ ARUser.send(:include, SimplestAuth::Model)
13
+ end
14
+
15
+ context "the ARUser class" do
16
+ should "redefine authenticate for AR" do
17
+ ARUser.expects(:instance_eval).with(kind_of(String))
18
+ ARUser.authenticate_by :email
19
+ end
20
+
21
+ should "have a default authenticate to email" do
22
+ user = mock do |m|
23
+ m.expects(:authentic?).with('password').returns(true)
24
+ end
25
+
26
+ ARUser.expects(:find_by_email).with('joe@schmoe.com').returns(user)
27
+ assert_equal user, ARUser.authenticate('joe@schmoe.com', 'password')
28
+ end
29
+
30
+ context "with authenticate_by set to username" do
31
+ setup do
32
+ ARUser.authenticate_by :username
33
+ end
34
+
35
+ should "find a user with email for authentication" do
36
+ user = mock do |m|
37
+ m.expects(:authentic?).with('password').returns(true)
38
+ end
39
+
40
+ ARUser.expects(:find_by_username).with('joeschmoe').returns(user)
41
+ assert_equal user, ARUser.authenticate('joeschmoe', 'password')
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,144 @@
1
+ require File.dirname(__FILE__) + '/../../test_helper'
2
+
3
+ class User
4
+ class RecordNotFound < StandardError; end
5
+ end
6
+
7
+ class ControllerTest < Test::Unit::TestCase
8
+ include SimplestAuth::Controller
9
+
10
+ context "the Controller module" do
11
+ should "know if a user is authorized" do
12
+ stubs(:logged_in?).returns(true)
13
+ assert authorized?
14
+ end
15
+
16
+ should "redirect to a new session if access is denied" do
17
+ stubs(:store_location)
18
+ expects(:redirect_to).with("")
19
+ stubs(:new_session_url).returns("")
20
+ stubs(:flash).returns({})
21
+ access_denied
22
+ end
23
+
24
+ should "set the error flash if access is denied" do
25
+ stubs(:store_location)
26
+ stubs(:redirect_to).with("")
27
+ stubs(:new_session_url).returns("")
28
+ stubs(:login_message).returns("blah")
29
+ flash_stub = {}
30
+ stubs(:flash).returns(flash_stub)
31
+ access_denied
32
+ assert_equal "blah", flash_stub[:error]
33
+ end
34
+
35
+ should "store the location of the desired page before redirecting" do
36
+ expects(:store_location)
37
+ stubs(:redirect_to)
38
+ stubs(:new_session_url)
39
+ stubs(:flash).returns({})
40
+ access_denied
41
+ end
42
+
43
+ should "store the location of the current request to session" do
44
+ expects(:session).returns({})
45
+ stubs(:request).returns(stub(:request_uri => ''))
46
+ store_location
47
+ end
48
+
49
+ should "redirect back to the stored uri" do
50
+ stubs(:session).returns({:return_to => 'somewhere'})
51
+ expects(:redirect_to).with('somewhere')
52
+ redirect_back_or_default('')
53
+ end
54
+
55
+ should "redirect to a default location if the session url is nil" do
56
+ stubs(:session).returns({:return_to => nil})
57
+ expects(:redirect_to).with('default')
58
+ redirect_back_or_default('default')
59
+ end
60
+
61
+ should "clear the session stored url after redirect" do
62
+ session = {:return_to => 'somewhere'}
63
+ stubs(:session).returns(session)
64
+ stubs(:redirect_to)
65
+ redirect_back_or_default('')
66
+ assert_nil session[:return_to]
67
+ end
68
+
69
+ should "know if login is required from authorized method" do
70
+ stubs(:authorized?).returns(true)
71
+ assert login_required
72
+ end
73
+
74
+ should "consider access denied if login is required and not authorized" do
75
+ stubs(:authorized?).returns(false)
76
+ expects(:access_denied)
77
+ login_required
78
+ end
79
+
80
+ should "know if a user is logged in" do
81
+ stubs(:current_user_id).returns(1)
82
+ assert logged_in?
83
+ end
84
+
85
+ should "know if a user is not logged in" do
86
+ stubs(:current_user_id).returns(nil)
87
+ assert_equal false, logged_in?
88
+ end
89
+
90
+ should "find the current user" do
91
+ user_stub = stub()
92
+ user_stub.stubs(:find).with(1).returns("user")
93
+
94
+ stubs(:current_user_id).returns(1)
95
+ stubs(:user_class).returns(user_stub)
96
+
97
+ assert_equal "user", current_user
98
+ end
99
+
100
+ should "return nil for the current user if it doesn't exist" do
101
+ User.stubs(:find).with('1').raises(User::RecordNotFound)
102
+ stubs(:current_user_id).with().returns('1')
103
+ stubs(:clear_session)
104
+
105
+ assert_nil current_user
106
+ end
107
+
108
+ should "be able to clear its session variables" do
109
+ expects(:session).with().returns(mock() {|m| m.expects(:[]=).with(:user_id, nil) })
110
+ clear_session
111
+ end
112
+
113
+ should "clear the :user_id from session if the user cannot be found" do
114
+ User.stubs(:find).with('1').raises(User::RecordNotFound)
115
+ stubs(:current_user_id).with().returns('1')
116
+ expects(:clear_session).with()
117
+
118
+ current_user
119
+ end
120
+
121
+ should "allow assigning to the current user" do
122
+ stubs(:session).returns({})
123
+ user = mock(:id => 1)
124
+ self.current_user = user
125
+ end
126
+
127
+ should "save the current user to avoid lookup" do
128
+ stubs(:session).returns({})
129
+ user = stub(:id => 1)
130
+ self.current_user = user
131
+ assert_equal user, current_user
132
+ end
133
+
134
+ should "know the current user id from session" do
135
+ stubs(:session).returns({:user_id => 1})
136
+ assert_equal 1, current_user_id
137
+ end
138
+
139
+ should "have a default login error message" do
140
+ assert_equal "Login or Registration Required", login_message
141
+ end
142
+ end
143
+
144
+ end
@@ -0,0 +1,47 @@
1
+ require File.dirname(__FILE__) + '/../../test_helper'
2
+
3
+ DMUser = Class.new
4
+
5
+ class DMUserTest < Test::Unit::TestCase
6
+ include BCrypt
7
+
8
+ context "with DataMapper" do
9
+ setup do
10
+ DMUser.stubs(:active_record?).returns(false)
11
+ DMUser.stubs(:data_mapper?).returns(true)
12
+ DMUser.expects(:before).with(:save, :hash_password, :if => :password_required?)
13
+ DMUser.send(:include, SimplestAuth::Model)
14
+ end
15
+
16
+ context "the DMUser class" do
17
+ should "redefine authenticate for DM" do
18
+ DMUser.expects(:instance_eval).with(kind_of(String))
19
+ DMUser.authenticate_by :email
20
+ end
21
+
22
+ should "have a default authenticate to email" do
23
+ user = mock do |m|
24
+ m.expects(:authentic?).with('password').returns(true)
25
+ end
26
+
27
+ DMUser.expects(:first).with(:email => 'joe@schmoe.com').returns(user)
28
+ assert_equal user, DMUser.authenticate('joe@schmoe.com', 'password')
29
+ end
30
+
31
+ context "with authenticate_by set to username" do
32
+ setup do
33
+ DMUser.authenticate_by :username
34
+ end
35
+
36
+ should "find a user with email for authentication" do
37
+ user = mock do |m|
38
+ m.expects(:authentic?).with('password').returns(true)
39
+ end
40
+
41
+ DMUser.expects(:first).with(:username => 'joeschmoe').returns(user)
42
+ assert_equal user, DMUser.authenticate('joeschmoe', 'password')
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,79 @@
1
+ require File.dirname(__FILE__) + '/../../test_helper'
2
+
3
+ class User; end
4
+
5
+ class UserTest < Test::Unit::TestCase
6
+ include BCrypt
7
+
8
+ context "with no ORM" do
9
+ setup do
10
+ User.stubs(:active_record?).returns(false)
11
+ User.stubs(:data_mapper?).returns(false)
12
+ User.send(:include, SimplestAuth::Model)
13
+ end
14
+
15
+ should "return nil for authenticate" do
16
+ assert_equal nil, User.authenticate('email', 'password')
17
+ end
18
+ end
19
+
20
+ context "an instance of the User class" do
21
+ setup do
22
+ User.send(:include, SimplestAuth::Model)
23
+ @user = User.new
24
+ @user.stubs(:crypted_password).returns('abcdefg')
25
+ end
26
+
27
+ should "determine if a password is authentic" do
28
+ password_stub = stub
29
+ password_stub.stubs(:==).with('password').returns(true)
30
+ Password.stubs(:new).with('abcdefg').returns(password_stub)
31
+
32
+ assert @user.authentic?('password')
33
+ end
34
+
35
+ should "determine when a password is not authentic" do
36
+ password_stub = stub
37
+ password_stub.stubs(:==).with('password').returns(false)
38
+ Password.stubs(:new).with('abcdefg').returns(password_stub)
39
+
40
+ assert_equal false, @user.authentic?('password')
41
+ end
42
+
43
+ should "use the Password class == method for comparison" do
44
+ password_stub = mock
45
+ password_stub.expects(:==).with('password').returns(true)
46
+ Password.stubs(:new).with('abcdefg').returns(password_stub)
47
+
48
+ @user.authentic?('password')
49
+ end
50
+
51
+ should "use a new Password made from crypted_password" do
52
+ password_stub = stub
53
+ password_stub.stubs(:==).with('password').returns(true)
54
+ Password.expects(:new).with('abcdefg').returns(password_stub)
55
+
56
+ @user.authentic?('password')
57
+ end
58
+
59
+ should "hash a password using bcrypt" do
60
+ @user.stubs(:password_required?).returns(true)
61
+ @user.expects(:crypted_password=).with('abcdefg')
62
+ @user.password = 'password'
63
+ Password.expects(:create).with('password').returns('abcdefg')
64
+
65
+ @user.send(:hash_password)
66
+ end
67
+
68
+ should "require a password if crypted password is blank" do
69
+ @user.stubs(:crypted_password).returns(stub(:blank? => true))
70
+ assert_equal true, @user.send(:password_required?)
71
+ end
72
+
73
+ should "require a password if a password has been set" do
74
+ @user.stubs(:crypted_password).returns(stub(:blank? => false))
75
+ @user.stubs(:password).returns(stub(:blank? => false))
76
+ assert_equal true, @user.send(:password_required?)
77
+ end
78
+ end
79
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vigetlabs-simplest_auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Tony Pitale
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-03 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bcrypt-ruby
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 2.0.5
24
+ version:
25
+ description:
26
+ email: tony.pitale@viget.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README.textile
35
+ - Rakefile
36
+ - lib/simplest_auth
37
+ - lib/simplest_auth/controller.rb
38
+ - lib/simplest_auth/model.rb
39
+ - lib/simplest_auth/version.rb
40
+ - lib/simplest_auth.rb
41
+ - test/unit/simplest_auth/ar_model_test.rb
42
+ - test/unit/simplest_auth/controller_test.rb
43
+ - test/unit/simplest_auth/dm_model_test.rb
44
+ - test/unit/simplest_auth/model_test.rb
45
+ has_rdoc: false
46
+ homepage: http://viget.com/extend
47
+ post_install_message:
48
+ rdoc_options: []
49
+
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ requirements: []
65
+
66
+ rubyforge_project:
67
+ rubygems_version: 1.2.0
68
+ signing_key:
69
+ specification_version: 2
70
+ summary: Simple implementation of authentication for Rails
71
+ test_files:
72
+ - test/unit/simplest_auth/ar_model_test.rb
73
+ - test/unit/simplest_auth/controller_test.rb
74
+ - test/unit/simplest_auth/dm_model_test.rb
75
+ - test/unit/simplest_auth/model_test.rb