authpwn_rails 0.12.0 → 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.travis.yml +7 -2
  2. data/VERSION +1 -1
  3. data/app/models/credentials/password.rb +16 -8
  4. data/app/models/credentials/token.rb +8 -0
  5. data/app/models/tokens/email_verification.rb +3 -0
  6. data/app/models/tokens/password_reset.rb +5 -2
  7. data/app/models/tokens/session_uid.rb +54 -0
  8. data/authpwn_rails.gemspec +8 -2
  9. data/lib/authpwn_rails.rb +3 -2
  10. data/lib/authpwn_rails/current_user.rb +1 -10
  11. data/lib/authpwn_rails/engine.rb +2 -2
  12. data/lib/authpwn_rails/expires.rb +23 -0
  13. data/lib/authpwn_rails/generators/all_generator.rb +9 -4
  14. data/lib/authpwn_rails/generators/templates/credential.rb +1 -1
  15. data/lib/authpwn_rails/generators/templates/credentials.yml +16 -0
  16. data/lib/authpwn_rails/generators/templates/initializer.rb +18 -0
  17. data/lib/authpwn_rails/generators/templates/session/forbidden.html.erb +1 -1
  18. data/lib/authpwn_rails/generators/templates/session/home.html.erb +1 -1
  19. data/lib/authpwn_rails/generators/templates/session/new.html.erb +3 -3
  20. data/lib/authpwn_rails/generators/templates/session/welcome.html.erb +1 -1
  21. data/lib/authpwn_rails/generators/templates/session_controller.rb +13 -4
  22. data/lib/authpwn_rails/generators/templates/session_controller_test.rb +12 -2
  23. data/lib/authpwn_rails/generators/templates/session_mailer.rb +3 -3
  24. data/lib/authpwn_rails/generators/templates/session_mailer/email_verification_email.html.erb +3 -3
  25. data/lib/authpwn_rails/generators/templates/session_mailer/reset_password_email.html.erb +3 -3
  26. data/lib/authpwn_rails/generators/templates/session_mailer_test.rb +4 -4
  27. data/lib/authpwn_rails/routes.rb +4 -4
  28. data/lib/authpwn_rails/session.rb +31 -8
  29. data/lib/authpwn_rails/session_controller.rb +27 -18
  30. data/lib/authpwn_rails/test_extensions.rb +16 -6
  31. data/lib/authpwn_rails/user_model.rb +10 -10
  32. data/test/cookie_controller_test.rb +165 -16
  33. data/test/credentials/email_verification_token_test.rb +11 -11
  34. data/test/credentials/password_credential_test.rb +31 -12
  35. data/test/credentials/session_uid_token_test.rb +98 -0
  36. data/test/credentials/token_crendential_test.rb +46 -12
  37. data/test/helpers/db_setup.rb +6 -5
  38. data/test/helpers/routes.rb +5 -2
  39. data/test/initializer_test.rb +18 -0
  40. data/test/session_controller_api_test.rb +127 -53
  41. data/test/test_extensions_test.rb +41 -0
  42. data/test/test_helper.rb +3 -0
  43. data/test/user_test.rb +11 -10
  44. metadata +9 -3
@@ -6,7 +6,7 @@ module TestExtensions
6
6
  # Stubs User#auth_bounce_reason to block a given credential.
7
7
  #
8
8
  # The default implementation of User#auth_bounce_reason always returns nil.
9
- # Your application's implementation might differ. Either way, the method is
9
+ # Your application's implementation might differ. Either way, the method is
10
10
  # replaced for the duration of the block, such that it returns :block if
11
11
  # the credential matches the given argument, and nil otherwise.
12
12
  def with_blocked_credential(blocked_credential, reason = :blocked, &block)
@@ -19,7 +19,7 @@ module TestExtensions
19
19
  credential == blocked_credential ? reason : nil
20
20
  end
21
21
  end
22
-
22
+
23
23
  begin
24
24
  yield
25
25
  ensure
@@ -40,13 +40,23 @@ end
40
40
  module ControllerTestExtensions
41
41
  # Sets the authenticated user in the test session.
42
42
  def set_session_current_user(user)
43
- request.session[:user_exuid] = user ? user.to_param : nil
43
+ if user
44
+ # Avoid database inserts, if at all possible.
45
+ if token = Tokens::SessionUid.where(:user_id => user.id).first
46
+ token.spend # Only bump updated_at if necessary.
47
+ else
48
+ token = Tokens::SessionUid.random_for user, '127.0.0.1', 'UnitTests'
49
+ end
50
+ request.session[:authpwn_suid] = token.suid
51
+ else
52
+ request.session.delete :authpwn_suid
53
+ end
44
54
  end
45
-
55
+
46
56
  # The authenticated user in the test session.
47
57
  def session_current_user
48
- return nil unless user_param = request.session[:user_exuid]
49
- User.find_by_param user_param
58
+ return nil unless suid = request.session[:authpwn_suid]
59
+ Credentials::Token.with_code(suid).user
50
60
  end
51
61
 
52
62
  # Sets the HTTP Authentication header.
@@ -18,16 +18,16 @@ module UserModel
18
18
  # This is decoupled from "id" column to avoid leaking information about
19
19
  # the application's usage.
20
20
  validates :exuid, :presence => true, :length => 1..32, :uniqueness => true
21
-
21
+
22
22
  # Credentials used to authenticate the user.
23
23
  has_many :credentials, :dependent => :destroy, :inverse_of => :user
24
24
  validates_associated :credentials
25
25
  # This is safe, because credentials use attr_accessible.
26
26
  accepts_nested_attributes_for :credentials, :allow_destroy => true
27
-
27
+
28
28
  # Automatically assign exuid.
29
29
  before_validation :set_default_exuid, :on => :create
30
-
30
+
31
31
  # Forms should not be able to touch any attribute.
32
32
  attr_accessible :credentials_attributes
33
33
  end
@@ -46,8 +46,8 @@ module UserModel
46
46
  # The method's parameter names are an acknowledgement to the email and
47
47
  # password fields on automatically-generated forms.
48
48
  #
49
- # The easiest method of accepting other login information is to override this
50
- # method, locate the user's email, and supply it in a call to super.
49
+ # The easiest method of accepting other login information is to override
50
+ # this method, locate the user's email, and supply it in a call to super.
51
51
  #
52
52
  # Returns an authenticated user, or a symbol indicating the reason why the
53
53
  # authentication failed.
@@ -55,21 +55,21 @@ module UserModel
55
55
  Credentials::Password.authenticate_email email, password
56
56
  end
57
57
  end # module Authpwn::UserModel::ClassMethods
58
-
58
+
59
59
  # Checks if a credential is acceptable for authenticating a user.
60
60
  #
61
61
  # Returns nil if the credential is acceptable, or a String containing a
62
- # user-visible reason why the credential is not acceptable.
62
+ # user-visible reason why the credential is not acceptable.
63
63
  def auth_bounce_reason(crdential)
64
64
  nil
65
65
  end
66
-
66
+
67
67
  # Use e-mails instead of exposing ActiveRecord IDs.
68
68
  def to_param
69
69
  exuid
70
70
  end
71
-
72
- # :nodoc: sets exuid to a (hopefully) unique value before validations occur.
71
+
72
+ # :nodoc: sets exuid to a (hopefully) unique value before validations occur.
73
73
  def set_default_exuid
74
74
  self.exuid ||=
75
75
  SecureRandom.random_bytes(8).unpack('Q').first & 0x7fffffffffffffff
@@ -2,8 +2,8 @@ require File.expand_path('../test_helper', __FILE__)
2
2
 
3
3
  # Mock controller used for testing session handling.
4
4
  class CookieController < ApplicationController
5
- authenticates_using_session
6
-
5
+ authenticates_using_session :except => :update
6
+
7
7
  def show
8
8
  if current_user
9
9
  render :text => "User: #{current_user.id}"
@@ -11,7 +11,16 @@ class CookieController < ApplicationController
11
11
  render :text => "No user"
12
12
  end
13
13
  end
14
-
14
+
15
+ def update
16
+ if params[:exuid].blank?
17
+ set_session_current_user nil
18
+ else
19
+ set_session_current_user User.find_by_param(params[:exuid])
20
+ end
21
+ render :text => ''
22
+ end
23
+
15
24
  def bouncer
16
25
  bounce_user
17
26
  end
@@ -20,32 +29,172 @@ end
20
29
  class CookieControllerTest < ActionController::TestCase
21
30
  setup do
22
31
  @user = users(:john)
32
+ @token = credentials(:john_session_token)
23
33
  end
24
34
 
25
- test "no user_id in session" do
35
+ test "no suid in session" do
26
36
  get :show
27
37
  assert_response :success
28
38
  assert_nil assigns(:current_user)
29
39
  assert_equal 'No user', response.body
30
40
  end
31
-
32
- test "valid user_id in session" do
33
- set_session_current_user @user
41
+
42
+ test "valid suid in session" do
43
+ request.session[:authpwn_suid] = @token.suid
34
44
  get :show
35
45
  assert_response :success
36
46
  assert_equal @user, assigns(:current_user)
37
47
  assert_equal "User: #{ActiveRecord::Fixtures.identify(:john)}",
38
48
  response.body
39
49
  end
40
-
41
- test "invalid user_pid in session" do
42
- get :show, {}, :current_user_pid => 'random@user.com'
50
+
51
+ test "valid suid in session does not refresh very recent session" do
52
+ request.session[:authpwn_suid] = @token.suid
53
+ @token.updated_at = Time.now - 5.minutes
54
+ @token.save!
55
+ get :show
56
+ assert_response :success
57
+ assert_equal @user, assigns(:current_user)
58
+ assert_operator @token.reload.updated_at, :<=, Time.now - 5.minutes
59
+ end
60
+
61
+ test "valid suid in session refreshes recent session" do
62
+ request.session[:authpwn_suid] = @token.suid
63
+ @token.updated_at = Time.now - 5.minutes
64
+ @token.save!
65
+ get :show
66
+ assert_response :success
67
+ assert_equal @user, assigns(:current_user)
68
+ assert_operator @token.reload.updated_at, :<=, Time.now - 5.minutes
69
+ end
70
+
71
+ test "valid suid in session is discarded if the session is old" do
72
+ request.session[:authpwn_suid] = @token.suid
73
+ @token.updated_at = Time.now - 3.months
74
+ @token.save!
75
+ get :show
76
+ assert_response :success
77
+ assert_nil assigns(:current_user), 'current_user set'
78
+ assert_nil Credentials::Token.with_code(@token.suid),
79
+ 'session token not destroyed'
80
+ end
81
+
82
+ test "invalid suid in session" do
83
+ request.session[:authpwn_suid] = @token.suid
84
+ @token.destroy
85
+ get :show
43
86
  assert_response :success
44
87
  assert_nil assigns(:current_user)
45
88
  end
46
-
89
+
90
+ test "set_session_current_user creates new token by default" do
91
+ assert_difference 'Credential.count', 1 do
92
+ put :update, :exuid => @user.exuid
93
+ end
94
+ assert_response :success
95
+ assert_not_equal @token.suid, request.session[:authpwn_suid]
96
+
97
+ get :show
98
+ assert_response :success
99
+ assert_equal @user, assigns(:current_user)
100
+ end
101
+
102
+ test "set_session_current_user reuses existing token when suitable" do
103
+ request.session[:authpwn_suid] = @token.suid
104
+ assert_no_difference 'Credential.count', 'existing token not reused' do
105
+ put :update, :exuid => @user.exuid
106
+ end
107
+ assert_response :success
108
+ assert_equal @token.suid, request.session[:authpwn_suid]
109
+
110
+ get :show
111
+ assert_response :success
112
+ assert_equal @user, assigns(:current_user)
113
+ end
114
+
115
+ test "set_session_current_user refreshes old token" do
116
+ @token.updated_at = Time.now - 1.day
117
+ request.session[:authpwn_suid] = @token.suid
118
+ assert_no_difference 'Credential.count', 'existing token not reused' do
119
+ put :update, :exuid => @user.exuid
120
+ end
121
+ assert_response :success
122
+ assert_operator @token.reload.updated_at, :>=, Time.now - 1.hour,
123
+ 'Old token not refreshed'
124
+
125
+ get :show
126
+ assert_response :success
127
+ assert_equal @user, assigns(:current_user)
128
+ end
129
+
130
+ test "set_session_current_user creates new token if old token is invalid" do
131
+ @token.destroy
132
+ request.session[:authpwn_suid] = @token.suid
133
+ assert_difference 'Credential.count', 1, 'session token not created' do
134
+ put :update, :exuid => @user.exuid
135
+ end
136
+ assert_response :success
137
+ assert_not_equal @token.suid, request.session[:authpwn_suid]
138
+
139
+ get :show
140
+ assert_response :success
141
+ assert_equal @user, assigns(:current_user)
142
+ end
143
+
144
+ test "set_session_current_user switches users correctly" do
145
+ old_token = credentials(:jane_session_token)
146
+ request.session[:authpwn_suid] = old_token.suid
147
+ assert_no_difference 'Credential.count',
148
+ "old user's token not destroyed or no new token created" do
149
+ put :update, :exuid => @user.exuid
150
+ end
151
+ assert_response :success
152
+ assert_nil Credentials::Token.with_code(old_token.suid),
153
+ "old user's token not destroyed"
154
+ assert_not_equal @token.suid, request.session[:authpwn_suid]
155
+
156
+ get :show
157
+ assert_response :success
158
+ assert_equal @user, assigns(:current_user)
159
+ end
160
+
161
+ test "set_session_current_user reuses token when switching users" do
162
+ @token.destroy
163
+ request.session[:authpwn_suid] = credentials(:jane_session_token).suid
164
+ assert_no_difference 'Credential.count',
165
+ "old user's token not destroyed or new user's token not created" do
166
+ put :update, :exuid => @user.exuid
167
+ end
168
+ assert_response :success
169
+
170
+ get :show
171
+ assert_response :success
172
+ assert_equal @user, assigns(:current_user)
173
+ end
174
+
175
+ test "set_session_current_user logs off a user correctly" do
176
+ request.session[:authpwn_suid] = @token.suid
177
+ assert_difference 'Credential.count', -1, 'token not destroyed' do
178
+ put :update, :exuid => ''
179
+ end
180
+ assert_response :success
181
+ assert_nil request.session[:authpwn_suid]
182
+
183
+ get :show
184
+ assert_response :success
185
+ assert_equal nil, assigns(:current_user)
186
+ end
187
+
188
+ test "set_session_current_user behaves when no user is logged off" do
189
+ assert_no_difference 'Credential.count' do
190
+ put :update, :exuid => ''
191
+ end
192
+ assert_response :success
193
+ assert_nil request.session[:authpwn_suid]
194
+ end
195
+
47
196
  test "valid user_id bounced" do
48
- set_session_current_user @user
197
+ request.session[:authpwn_suid] = @token.suid
49
198
  get :bouncer
50
199
  assert_response :forbidden
51
200
  assert_template 'session/forbidden'
@@ -53,19 +202,19 @@ class CookieControllerTest < ActionController::TestCase
53
202
  end
54
203
 
55
204
  test "valid user_id bounced in json" do
56
- set_session_current_user @user
205
+ request.session[:authpwn_suid] = @token.suid
57
206
  get :bouncer, :format => 'json'
58
207
  assert_response :ok
59
208
  data = ActiveSupport::JSON.decode response.body
60
209
  assert_match(/not allowed/i, data['error'])
61
210
  end
62
-
211
+
63
212
  test "no user_id bounced" do
64
213
  get :bouncer
65
214
  assert_response :forbidden
66
215
  assert_template 'session/forbidden'
67
216
  assert_equal bouncer_cookie_url, flash[:auth_redirect_url]
68
-
217
+
69
218
  assert_select 'script', %r/.*window.location.*#{new_session_path}.*/
70
219
  end
71
220
 
@@ -75,7 +224,7 @@ class CookieControllerTest < ActionController::TestCase
75
224
  data = ActiveSupport::JSON.decode response.body
76
225
  assert_match(/sign in/i, data['error'])
77
226
  end
78
-
227
+
79
228
  test "auth_controller? is false" do
80
229
  assert_equal false, @controller.auth_controller?
81
230
  end
@@ -1,27 +1,27 @@
1
1
  require File.expand_path('../../test_helper', __FILE__)
2
2
 
3
- class EmailVerificationTokenCredentialTest < ActiveSupport::TestCase
3
+ class EmailVerificationTokenTest < ActiveSupport::TestCase
4
4
  def setup
5
5
  @credential = Tokens::EmailVerification.new(
6
6
  :code => 'AyCMIixa5C7BBqU-XFI7l7IaUFJ4zQZPmcK6oNb3FLo',
7
7
  :key => 'jane@gmail.com')
8
8
  @credential.user = users(:jane)
9
9
  end
10
-
10
+
11
11
  test 'setup' do
12
12
  assert @credential.valid?
13
13
  end
14
-
14
+
15
15
  test 'code required' do
16
16
  @credential.code = nil
17
17
  assert !@credential.valid?
18
18
  end
19
-
19
+
20
20
  test 'code uniqueness' do
21
21
  @credential.code = credentials(:john_token).code
22
22
  assert !@credential.valid?
23
23
  end
24
-
24
+
25
25
  test 'email required' do
26
26
  @credential.email = nil
27
27
  assert !@credential.valid?
@@ -31,22 +31,22 @@ class EmailVerificationTokenCredentialTest < ActiveSupport::TestCase
31
31
  @credential.user = nil
32
32
  assert !@credential.valid?
33
33
  end
34
-
34
+
35
35
  test 'email_credential' do
36
36
  assert_equal credentials(:jane_email), @credential.email_credential
37
37
  assert_equal credentials(:john_email),
38
38
  credentials(:john_email_token).email_credential
39
-
39
+
40
40
  @credential.email = 'bill@gmail.com'
41
41
  assert_nil @credential.email_credential
42
42
  end
43
-
43
+
44
44
  test 'spend verifies the e-mail and destroys the token' do
45
45
  email_credential = credentials(:john_email)
46
46
  assert !email_credential.verified, 'bad setup'
47
47
  credential = credentials(:john_email_token)
48
48
  assert_equal Tokens::EmailVerification, credential.class, 'bad setup'
49
-
49
+
50
50
  assert_difference 'Credential.count', -1 do
51
51
  credential.spend
52
52
  end
@@ -58,14 +58,14 @@ class EmailVerificationTokenCredentialTest < ActiveSupport::TestCase
58
58
  email_credential = credentials(:john_email)
59
59
  credential = credentials(:john_email_token)
60
60
  credential.email = 'bill@gmail.com'
61
-
61
+
62
62
  assert_difference 'Credential.count', -1 do
63
63
  credential.spend
64
64
  end
65
65
  assert credential.frozen?, 'not destroyed'
66
66
  assert !email_credential.reload.verified?, 'e-mail wrongly verified'
67
67
  end
68
-
68
+
69
69
  test 'random_for' do
70
70
  token = Tokens::EmailVerification.random_for credentials(:jane_email)
71
71
  assert token.valid?, 'valid token'
@@ -1,61 +1,80 @@
1
1
  require File.expand_path('../../test_helper', __FILE__)
2
2
 
3
- class PasswordCredentialTest < ActiveSupport::TestCase
3
+ class PasswordCredentialTest < ActiveSupport::TestCase
4
4
  def setup
5
5
  @credential = Credentials::Password.new :password => 'awesome',
6
6
  :password_confirmation => 'awesome'
7
7
  @credential.user = users(:bill)
8
+ @_password_expires = Credentials::Password.expires_after
8
9
  end
9
-
10
+
11
+ def teardown
12
+ Credentials::Password.expires_after = @_password_expires
13
+ end
14
+
10
15
  test 'setup' do
11
16
  assert @credential.valid?
12
17
  end
13
-
18
+
14
19
  test 'key not required' do
15
20
  @credential.key = nil
16
21
  assert @credential.valid?
17
22
  end
18
-
23
+
19
24
  test 'user presence' do
20
25
  @credential.user = nil
21
26
  assert !@credential.valid?
22
27
  end
23
-
28
+
24
29
  test 'user uniqueness' do
25
30
  @credential.user = users(:john)
26
31
  assert !@credential.valid?
27
32
  end
28
-
33
+
29
34
  test 'password confirmation' do
30
35
  @credential.password_confirmation = 'not awesome'
31
36
  assert !@credential.valid?
32
37
  end
33
-
38
+
34
39
  test 'password required' do
35
40
  @credential.password = @credential.password_confirmation = nil
36
41
  assert !@credential.valid?
37
42
  end
38
-
43
+
39
44
  test 'check_password' do
40
45
  assert_equal true, @credential.check_password('awesome')
41
46
  assert_equal false, @credential.check_password('not awesome'),
42
47
  'Bogus password'
43
48
  assert_equal false, @credential.check_password('password'),
44
- "Another user's password"
49
+ "Another user's password"
50
+ end
51
+
52
+ test 'expired?' do
53
+ @credential.updated_at = Time.now
54
+ assert_equal false, @credential.expired?
55
+ @credential.updated_at = Time.now - 2.years
56
+ assert_equal true, @credential.expired?
57
+ Credentials::Password.expires_after = nil
58
+ assert_equal false, @credential.expired?
45
59
  end
46
-
60
+
47
61
  test 'authenticate' do
62
+ @credential.updated_at = Time.now
48
63
  assert_equal users(:bill), @credential.authenticate('awesome')
49
64
  assert_equal :invalid, @credential.authenticate('not awesome')
65
+ Credentials::Password.expires_after = 1.month
66
+ @credential.updated_at = Time.now - 1.year
67
+ assert_equal :expired, @credential.authenticate('awesome')
50
68
  end
51
-
69
+
52
70
  test 'authenticate calls User#auth_bounce_reason' do
53
71
  user = @credential.user
54
72
  flexmock(user).should_receive(:auth_bounce_reason).and_return(:reason)
73
+ @credential.updated_at = Time.now
55
74
  assert_equal :reason, @credential.authenticate('awesome')
56
75
  assert_equal :invalid, @credential.authenticate('not awesome')
57
76
  end
58
-
77
+
59
78
  test 'authenticate_email' do
60
79
  assert_equal users(:john),
61
80
  Credentials::Password.authenticate_email('john@gmail.com', 'password')