sparkly-auth 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +17 -0
  3. data/Rakefile +50 -0
  4. data/VERSION +1 -0
  5. data/app/controllers/sparkly_accounts_controller.rb +59 -0
  6. data/app/controllers/sparkly_controller.rb +47 -0
  7. data/app/controllers/sparkly_sessions_controller.rb +52 -0
  8. data/app/models/password.rb +3 -0
  9. data/app/models/remembrance_token.rb +50 -0
  10. data/app/views/sparkly_accounts/edit.html.erb +24 -0
  11. data/app/views/sparkly_accounts/new.html.erb +24 -0
  12. data/app/views/sparkly_accounts/show.html.erb +0 -0
  13. data/app/views/sparkly_sessions/new.html.erb +22 -0
  14. data/dependencies.rb +1 -0
  15. data/generators/sparkly/USAGE +27 -0
  16. data/generators/sparkly/sparkly_generator.rb +76 -0
  17. data/generators/sparkly/templates/accounts_controller.rb +65 -0
  18. data/generators/sparkly/templates/accounts_helper.rb +2 -0
  19. data/generators/sparkly/templates/help_file.txt +56 -0
  20. data/generators/sparkly/templates/initializer.rb +30 -0
  21. data/generators/sparkly/templates/migrations/add_confirmed_to_sparkly_passwords.rb +9 -0
  22. data/generators/sparkly/templates/migrations/create_sparkly_passwords.rb +19 -0
  23. data/generators/sparkly/templates/migrations/create_sparkly_remembered_tokens.rb +15 -0
  24. data/generators/sparkly/templates/sessions_controller.rb +45 -0
  25. data/generators/sparkly/templates/sessions_helper.rb +2 -0
  26. data/generators/sparkly/templates/tasks/migrations.rb +1 -0
  27. data/generators/sparkly/templates/views/sparkly_accounts/edit.html.erb +24 -0
  28. data/generators/sparkly/templates/views/sparkly_accounts/new.html.erb +24 -0
  29. data/generators/sparkly/templates/views/sparkly_accounts/show.html.erb +0 -0
  30. data/generators/sparkly/templates/views/sparkly_sessions/new.html.erb +22 -0
  31. data/init.rb +44 -0
  32. data/lib/auth.rb +52 -0
  33. data/lib/auth/behavior/base.rb +64 -0
  34. data/lib/auth/behavior/core.rb +87 -0
  35. data/lib/auth/behavior/core/authenticated_model_methods.rb +52 -0
  36. data/lib/auth/behavior/core/controller_extensions.rb +52 -0
  37. data/lib/auth/behavior/core/controller_extensions/class_methods.rb +24 -0
  38. data/lib/auth/behavior/core/controller_extensions/current_user.rb +54 -0
  39. data/lib/auth/behavior/core/password_methods.rb +65 -0
  40. data/lib/auth/behavior/remember_me.rb +17 -0
  41. data/lib/auth/behavior/remember_me/configuration.rb +21 -0
  42. data/lib/auth/behavior/remember_me/controller_extensions.rb +66 -0
  43. data/lib/auth/behavior_lookup.rb +10 -0
  44. data/lib/auth/configuration.rb +328 -0
  45. data/lib/auth/encryptors/sha512.rb +20 -0
  46. data/lib/auth/generators/configuration_generator.rb +20 -0
  47. data/lib/auth/generators/controllers_generator.rb +34 -0
  48. data/lib/auth/generators/migration_generator.rb +32 -0
  49. data/lib/auth/generators/route_generator.rb +19 -0
  50. data/lib/auth/generators/views_generator.rb +26 -0
  51. data/lib/auth/model.rb +94 -0
  52. data/lib/auth/observer.rb +21 -0
  53. data/lib/auth/target_list.rb +5 -0
  54. data/lib/auth/tasks/migrations.rb +71 -0
  55. data/lib/auth/token.rb +10 -0
  56. data/lib/sparkly-auth.rb +1 -0
  57. data/rails/init.rb +17 -0
  58. data/rails/routes.rb +19 -0
  59. data/sparkly-auth.gemspec +143 -0
  60. data/spec/controllers/application_controller_spec.rb +13 -0
  61. data/spec/generators/sparkly_spec.rb +64 -0
  62. data/spec/lib/auth/behavior/core_spec.rb +184 -0
  63. data/spec/lib/auth/behavior/remember_me_spec.rb +127 -0
  64. data/spec/lib/auth/extensions/controller_spec.rb +32 -0
  65. data/spec/lib/auth/model_spec.rb +57 -0
  66. data/spec/lib/auth_spec.rb +32 -0
  67. data/spec/mocks/models/user.rb +3 -0
  68. data/spec/routes_spec.rb +24 -0
  69. data/spec/spec_helper.rb +61 -0
  70. data/spec/views_spec.rb +18 -0
  71. metadata +210 -0
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe :sparkly do
4
+ before(:each) do
5
+ Auth.configure do |config|
6
+ config.authenticate :user
7
+ end
8
+ end
9
+
10
+ context "with :remember_me behavior" do
11
+ before(:each) { Auth.configure { |c| c.behaviors << :remember_me } }
12
+
13
+ with_args :migrations do
14
+ it "should generate a remembered_tokens migration" do
15
+ subject.should generate("db/migrate/002_create_sparkly_remembered_tokens.rb")
16
+ end
17
+ end
18
+ end
19
+
20
+
21
+ with_args :migrations do
22
+ it "should generate db/migrate" do
23
+ subject.should generate("db/migrate")
24
+ subject.should generate("db/migrate/001_create_sparkly_passwords.rb")
25
+ end
26
+ end
27
+
28
+ with_args :config do
29
+ it "should generate the sparkly initializers" do
30
+ subject.should generate('lib/tasks/sparkly_migration.rb')
31
+ subject.should generate('config/initializers/sparkly_authentication.rb')
32
+ end
33
+ end
34
+
35
+ with_args :controllers do
36
+ it "should generate the sparkly controllers" do
37
+ Auth.configuration.authenticate(:user, :accounts_controller => 'users', :sessions_controller => 'user_sessions')
38
+ Auth.kick!
39
+
40
+ subject.should generate("app/controllers/users_controller.rb")
41
+ subject.should generate("app/controllers/user_sessions_controller.rb")
42
+ subject.should generate("app/helpers/users_helper.rb")
43
+ subject.should generate("app/helpers/user_sessions_helper.rb")
44
+ end
45
+ end
46
+
47
+ with_args :views do
48
+ it "should generate the sparkly views with /users if given the users controller" do
49
+ Auth.configuration.authenticate(:user, :accounts_controller => 'users', :sessions_controller => 'user_sessions')
50
+ Auth.kick!
51
+
52
+ subject.should generate("app/views/users/edit.html.erb")
53
+ subject.should generate("app/views/users/new.html.erb")
54
+ subject.should generate("app/views/users/show.html.erb")
55
+ subject.should generate("app/views/user_sessions/new.html.erb")
56
+ end
57
+ end
58
+
59
+ with_args :help do
60
+ it "should generate the sparkly help" do
61
+ subject.should generate('doc/sparkly_authentication.txt')
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,184 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Behavior: Core" do
4
+ subject { Auth::Model.new(:user, :behaviors => [:core], :password_update_frequency => 30.days) }
5
+
6
+ before(:each) do
7
+ subject.apply_options!
8
+ end
9
+
10
+ context "user" do
11
+ it "should not be able to reuse a previous password" do
12
+ u = User.new(:email => "generic1@example.com")
13
+ u.password = u.password_confirmation = "Generic12"
14
+ u.save!
15
+ u.password = u.password_confirmation = "Generic13"
16
+ u.save!
17
+ u.password = u.password_confirmation = "Generic12"
18
+
19
+ proc { u.save! }.should raise_error(ActiveRecord::RecordInvalid)
20
+ end
21
+
22
+ it "should produce multiple password models" do
23
+ u = User.new(:email => "generic2@example.com")
24
+ u.password = u.password_confirmation = "Generic12"
25
+ u.save!
26
+ u.password = u.password_confirmation = "Generic13"
27
+ u.save!
28
+
29
+ u.passwords.length.should == 2
30
+ end
31
+
32
+ it "should not store more than 4 passwords" do
33
+ User.destroy_all
34
+ u = User.new(:email => "generic3@example.com")
35
+ %w(Generic12 Generic13 Generic14 Generic15 Generic16 Generic17 Generic18).each do |pw|
36
+ u.password = u.password_confirmation = pw
37
+ u.save!
38
+ end
39
+
40
+ u.passwords.length.should == 4
41
+ end
42
+
43
+ it "should not be valid if password and confirmation are skipped" do
44
+ u = User.new(:email => "generic@example.com")
45
+ u.should_not be_valid
46
+ end
47
+
48
+ it "should create a user that can have a password validated against it" do
49
+ email = "generic@example.com"
50
+ password = "Ab12345"
51
+
52
+ User.destroy_all
53
+ returning(User.new(:email => email)) { |user|
54
+ user.password = password
55
+ user.password_confirmation = password
56
+ user.save!
57
+ }
58
+
59
+ User.find_by_email(email).password_matches?(password).should == true
60
+ end
61
+
62
+ it "should not produce error messages that contain 'secret' or are duplicates" do
63
+ u = User.new(:email => "generic@example.com")
64
+ u.password = u.password_confirmation = nil
65
+ u.valid?
66
+ u.errors.to_a.should == u.errors.to_a.uniq
67
+ u.errors.to_a.collect { |ar| ar.first }.should_not include('secret')
68
+ u.errors.to_a.collect { |ar| ar.first }.should_not include('secret_confirmation')
69
+ end
70
+
71
+ it "should create #password" do
72
+ u = User.new
73
+ u.password.should be_blank
74
+ end
75
+
76
+ it "should create #password=" do
77
+ u = User.new
78
+ u.password = "hi there"
79
+ u.password.should_not be_blank
80
+ end
81
+
82
+ it "should encrypt password assignments" do
83
+ u = User.new
84
+ u.password = "hi there"
85
+ u.password.should_not == "hi there"
86
+ end
87
+
88
+ it "should create #password_confirmation" do
89
+ u = User.new
90
+ u.password_confirmation.should be_blank
91
+ end
92
+
93
+ it "should create #password_confirmation=" do
94
+ u = User.new
95
+ u.password_confirmation = "hi there"
96
+ u.password_confirmation.should_not be_blank
97
+ end
98
+
99
+ it "should encrypt password confirmation assignments" do
100
+ u = User.new
101
+ u.password_confirmation = "hi there"
102
+ u.password_confirmation.should_not == "hi there"
103
+ end
104
+
105
+ it "should delegate persistence token to password model" do
106
+ u = User.new
107
+ u.password = "hi there"
108
+ u.persistence_token.should_not be_blank
109
+ end
110
+ end
111
+
112
+ it "should reset the persistence token when password is assigned" do
113
+ pw = Password.new
114
+ pw.secret = "Hello12"
115
+ pw.persistence_token.should_not be_blank
116
+ end
117
+
118
+ it "should encrypt the :secret and :secret_confirmation upon assignment in Password" do
119
+ u = User.new
120
+ pw = u.passwords.build
121
+ pw.secret = "Hello12"
122
+ pw.secret_confirmation = "Hello12"
123
+
124
+ pw.secret.should_not == "Hello12" # ...but we don't know what it actually should equal, 'cause that's randomized.
125
+ pw.secret_confirmation.should_not == "Hello12"
126
+ pw.secret.should == pw.secret_confirmation
127
+ end
128
+
129
+ it "should force confirmation of the :secret on Password" do
130
+ u = User.new
131
+ pw = u.passwords.build
132
+ pw.secret = "Hello12"
133
+ pw.valid?
134
+ pw.errors.to_a.should_not be_empty
135
+
136
+ pw.secret_confirmation = "Hello1"
137
+ pw.valid?
138
+ pw.errors.on(:secret).should == "doesn't match confirmation"
139
+
140
+ pw.secret_confirmation = "Hello12"
141
+ pw.should be_valid
142
+ end
143
+
144
+ it 'should validate presence of :secret on Password' do
145
+ error_on(Password, :secret).should == "can't be blank"
146
+ end
147
+
148
+ it 'should validate password length >= 7' do
149
+ error_on(Password, :secret, "123456").should include("must be at least 7 characters")
150
+ end
151
+
152
+ it 'should validate password complexity' do
153
+ error_on(Password, :secret, "Ab12345").should == nil
154
+ error_on(Password, :secret, "1234567").should ==
155
+ "must contain at least 1 uppercase, 1 lowercase and 1 number"
156
+ error_on(Password, :secret, "abcdefg").should ==
157
+ "must contain at least 1 uppercase, 1 lowercase and 1 number"
158
+ error_on(Password, :secret, "ABCDEFG").should ==
159
+ "must contain at least 1 uppercase, 1 lowercase and 1 number"
160
+ end
161
+
162
+ it 'should add has_many :passwords to User' do
163
+ User.new.should respond_to(:passwords)
164
+ end
165
+
166
+ it "should expire passwords after 30 days or if no password is available" do
167
+ (u = User.new).password_expired?.should be_true
168
+ p = u.passwords.build(:created_at => 1.month.ago)
169
+ p.authenticatable = u # need to build the reverse relationship cause it's not saved yet... i hate that.
170
+ p.should be_expired
171
+ end
172
+
173
+ context "with password update frequency 90 days" do
174
+ subject { Auth::Model.new(:user, :behaviors => [:core], :password_update_frequency => 90.days) }
175
+
176
+ it "should expire passwords after 90 days or if no password is available" do
177
+ (u = User.new).password_expired?.should be_true # because there're no passwords yet
178
+
179
+ p = u.passwords.build(:created_at => 1.month.ago)
180
+ p.authenticatable = u # need to build the reverse relationship cause it's not saved yet... i hate that.
181
+ p.should_not be_expired
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+ require 'spec/rails'
3
+
4
+ describe "Behavior: Remember Me", :type => :controller do
5
+ controller_name :sparkly_sessions
6
+
7
+ def cookies
8
+ controller.send(:cookies)
9
+ end
10
+
11
+ def reset_auth!
12
+ # the lack of a current user will trigger authentication of various flavors, making these tests possible.
13
+ controller.instance_variable_set("@current_user", nil)
14
+ end
15
+
16
+ before(:each) do
17
+ Auth.configure do |c|
18
+ c.authenticate :user
19
+ c.behaviors = :core, :remember_me
20
+ c.remember_me.duration = 6.months
21
+ end
22
+
23
+ Auth.kick!
24
+ u = User.new(:email => "generic12@example.com")
25
+ u.password = u.password_confirmation = "Generic12"
26
+ u.save!
27
+ end
28
+
29
+ it "should set an auth token cookie upon successful login" do
30
+ post :create, { :model => "User", :user => { :email => "generic12@example.com", :password => "Generic12", :remember_me => true } }
31
+
32
+ session[:session_token].should_not be_blank
33
+
34
+ # There should be a token in the remebered_tokens table. We can use that data to decide
35
+ # what should be in the cookie.
36
+ RemembranceToken.count.should == 1
37
+
38
+ # We're looking for a string containing password model ID, series token, and auth token.
39
+ token = RemembranceToken.first
40
+
41
+ cookies[:remembrance_token].should == token.value
42
+ end
43
+
44
+ context "a user with a remember token" do
45
+ before(:each) do
46
+ post :create, { :model => "User", :user => { :email => "generic12@example.com", :password => "Generic12", :remember_me => true } }
47
+ end
48
+
49
+ shared_examples_for "an expired or missing session" do
50
+ it "should authenticate with the auth token cookie" do
51
+ controller.current_user.should_not be_nil
52
+ end
53
+
54
+ it "should generate a new auth token cookie" do
55
+ token = cookies[:remembrance_token]
56
+ controller.current_user
57
+ cookies[:remembrance_token].should_not == token
58
+ end
59
+
60
+ it "should not change the series identifier" do
61
+ controller.current_user
62
+ controller.current_user.remembrance_tokens.first.series_token == @series_identifier
63
+ end
64
+
65
+ context "and an invalid token id but valid series id" do
66
+ before(:each) do
67
+ cookies[:remembrance_token] = { :value => cookies[:remembrance_token]+"1", :expires => 6.months.from_now }
68
+ reset_auth!
69
+ end
70
+
71
+ it "should be considered a theft" do
72
+ # because the token is changed every time - if the wrong token is used it is due either to tampering or to
73
+ # using an expired token, indicating that someone has stolen and used the one-use token.
74
+ controller.current_user
75
+ flash[:error].should == Auth.remember_me.token_theft_message
76
+ end
77
+
78
+ it "should delete all remembrance tokens" do
79
+ controller.current_user.remembrance_tokens.count.should == 0
80
+ end
81
+ end
82
+
83
+ context "and token data is not present" do
84
+ before(:each) do
85
+ cookies[:remembrance_token] = { :value => "", :expires => 6.months.from_now }
86
+ reset_auth!
87
+ end
88
+
89
+ it "should not authenticate the user" do
90
+ controller.current_user.should == false
91
+ end
92
+ end
93
+
94
+ context "and token is missing" do
95
+ before(:each) do
96
+ cookies.delete(:remembrance_token)
97
+ #cookies[:remembrance_token] = nil
98
+ reset_auth!
99
+ end
100
+
101
+ it "should not authenticate the user" do
102
+ controller.current_user.should == false
103
+ end
104
+ end
105
+ end
106
+
107
+ context "and an expired session" do
108
+ before(:each) do
109
+ @series_identifier = controller.current_user.remembrance_tokens.first.series_token
110
+ session[:active_at] = 30.days.ago # i'm pretty sure this is past the session duration.
111
+ reset_auth!
112
+ end
113
+
114
+ it_should_behave_like "an expired or missing session"
115
+ end
116
+
117
+ context "and a missing session" do
118
+ before(:each) do
119
+ @series_identifier = controller.current_user.remembrance_tokens.first.series_token
120
+ session.clear
121
+ reset_auth!
122
+ end
123
+
124
+ it_should_behave_like "an expired or missing session"
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ describe Auth::Behavior::Core::ControllerExtensions do
4
+ #subject { ApplicationController.new }
5
+ subject { ApplicationController.call(Rack::MockRequest.env_for("/").merge('REQUEST_URI' => '')).template.controller }
6
+
7
+ before(:each) do
8
+ Auth.configure do |config|
9
+ config.session_duration = nil
10
+ config.authenticate :user
11
+ end
12
+
13
+ Auth.kick!
14
+
15
+ unless User.count == 1
16
+ u = User.new(:email => "generic4@example.com")
17
+ u.password = u.password_confirmation = "Generic12"
18
+ u.save!
19
+ end
20
+ end
21
+
22
+ it "should let users authenticate with single access token" do
23
+ subject.params = { :single_access_token => User.first.single_access_token }
24
+ subject.current_user.should be_kind_of(User)
25
+ end
26
+
27
+ it "should not raise nil errors when Auth.session_duration is nil" do
28
+ subject.session = { :session_token => User.first.persistence_token }
29
+
30
+ subject.current_user.should be_kind_of(User)
31
+ end
32
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe Auth::Model do
4
+ context "given nonexisting model name" do
5
+ subject { Auth::Model.new(:nonexisting_user) }
6
+
7
+ it "should fail silently during initialization because it might not have been generated yet" do
8
+ proc { subject }.should_not raise_error
9
+ end
10
+ end
11
+
12
+ context "with default options" do
13
+ subject { Auth::Model.new(:user) }
14
+
15
+ before(:each) do
16
+ Dispatcher.cleanup_application
17
+ Dispatcher.reload_application
18
+ subject.apply_options!
19
+ end
20
+
21
+ it "should validate presence of :email on User" do
22
+ error_on(User, :email).should == "can't be blank"
23
+ end
24
+
25
+ it "should use :core behavior" do
26
+ subject.behaviors.should include(:core)
27
+ end
28
+ end
29
+
30
+ context "with an empty :behaviors option" do
31
+ subject { Auth::Model.new(:user, :behaviors => []) }
32
+ before(:each) do
33
+ Dispatcher.cleanup_application
34
+ Dispatcher.reload_application
35
+ subject.apply_options!
36
+ end
37
+ it "should have no behaviors" do subject.behaviors.should be_empty end
38
+ end
39
+
40
+ context "with a hash for :with option" do
41
+ subject { Auth::Model.new(:user, :with => {
42
+ :secret => :passwd,
43
+ :format => /^.{8}$/,
44
+ :message => "must be exactly 8 characters"
45
+ })}
46
+
47
+ before(:each) do
48
+ Dispatcher.cleanup_application
49
+ Dispatcher.reload_application
50
+ subject.apply_options!
51
+ end
52
+
53
+ it "should validate presence of :email on User" do
54
+ error_on(User, :email).should == "can't be blank"
55
+ end
56
+ end
57
+ end