authpwn_rails 0.12.0 → 0.12.1

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 (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')