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
@@ -20,19 +20,19 @@
20
20
  :placeholder => 'your@email.com' %>
21
21
  </span>
22
22
  </div>
23
-
23
+
24
24
  <div class="field">
25
25
  <%= label_tag :password %><br />
26
26
  <span class="value">
27
27
  <%= password_field_tag :password %>
28
28
  </span>
29
29
  </div>
30
-
30
+
31
31
  <div class="actions">
32
32
  <%= button_tag 'Log in', :name => 'login', :value => 'requested' %>
33
33
  <%= button_tag 'Reset Password', :name => 'reset_password',
34
34
  :value => 'requested', :formaction => reset_password_session_path %>
35
-
35
+
36
36
  <% if @redirect_url %>
37
37
  <%= hidden_field_tag :redirect_url, @redirect_url %>
38
38
  <% end %>
@@ -1,5 +1,5 @@
1
1
  <p>
2
2
  This view gets displayed when the user is not logged in. Entice the user to
3
3
  sign up for your application, and allow them to
4
- <%= link_to 'Log in', new_session_path %>.
4
+ <%= link_to 'Log in', new_session_path %>.
5
5
  </p>
@@ -1,7 +1,7 @@
1
1
  # Manages logging in and out of the application.
2
2
  class SessionController < ApplicationController
3
3
  include Authpwn::SessionController
4
-
4
+
5
5
  # Sets up the 'session/welcome' view. No user is logged in.
6
6
  def welcome
7
7
  # You can brag about some statistics.
@@ -15,17 +15,19 @@ class SessionController < ApplicationController
15
15
  @user = current_user
16
16
  end
17
17
  private :home
18
-
18
+
19
19
  # The notification text displayed when a session authentication fails.
20
20
  def bounce_notice_text(reason)
21
21
  case reason
22
22
  when :invalid
23
23
  'Invalid e-mail or password'
24
+ when :expired
25
+ 'Password expired. Please click "Forget password"'
24
26
  when :blocked
25
27
  'Account blocked. Please verify your e-mail address'
26
28
  end
27
29
  end
28
-
30
+
29
31
  # A user is logged in, based on a token.
30
32
  def home_with_token(token)
31
33
  respond_to do |format|
@@ -44,7 +46,14 @@ class SessionController < ApplicationController
44
46
  end
45
47
  end
46
48
  private :home_with_token
47
-
49
+
50
+ # If true, every successful login results in a SQL query that removes expired
51
+ # session tokens from the database, to keep its size down.
52
+ #
53
+ # For better performance, set this to false and periodically call
54
+ # Tokens::SessionUid.remove_expired in background thread.
55
+ self.auto_purge_sessions = true
56
+
48
57
  # You shouldn't extend the session controller, so you can benefit from future
49
58
  # features. But, if you must, you can do it here.
50
59
  end
@@ -16,6 +16,17 @@ class SessionControllerTest < ActionController::TestCase
16
16
  assert_select 'a[href="/session"][data-method="delete"]', 'Log out'
17
17
  end
18
18
 
19
+ test "user login works and purges old sessions" do
20
+ old_token = credentials(:jane_session_token)
21
+ old_token.updated_at = Time.now - 1.year
22
+ old_token.save!
23
+ post :create, :email => @email_credential.email, :password => 'password'
24
+ assert_equal @user, session_current_user, 'session'
25
+ assert_redirected_to session_url
26
+ assert_nil Credentials::Token.with_code(old_token.code),
27
+ 'old session not purged'
28
+ end
29
+
19
30
  test "user logged in JSON request" do
20
31
  set_session_current_user @user
21
32
  get :show, :format => 'json'
@@ -37,7 +48,7 @@ class SessionControllerTest < ActionController::TestCase
37
48
  assert_equal({}, ActiveSupport::JSON.decode(response.body))
38
49
  end
39
50
 
40
- test "user signup page" do
51
+ test "user login page" do
41
52
  get :new
42
53
  assert_template :new
43
54
 
@@ -63,7 +74,6 @@ class SessionControllerTest < ActionController::TestCase
63
74
  'Password not cleared'
64
75
  end
65
76
 
66
-
67
77
  test "password change form" do
68
78
  set_session_current_user @user
69
79
  get :password_change
@@ -5,17 +5,17 @@ class SessionMailer < ActionMailer::Base
5
5
  # Consider replacing the hostname with a user-friendly application name.
6
6
  "#{server_hostname} e-mail verification"
7
7
  end
8
-
8
+
9
9
  def email_verification_from(token, server_hostname, protocol)
10
10
  # You most likely need to replace the e-mail address below.
11
11
  %Q|"#{server_hostname} staff" <admin@#{server_hostname}>|
12
- end
12
+ end
13
13
 
14
14
  def reset_password_subject(token, server_hostname, protocol)
15
15
  # Consider replacing the hostname with a user-friendly application name.
16
16
  "#{server_hostname} password reset"
17
17
  end
18
-
18
+
19
19
  def reset_password_from(token, server_hostname, protocol)
20
20
  # You most likely need to replace the e-mail address below.
21
21
  %Q|"#{server_hostname} staff" <admin@#{server_hostname}>|
@@ -2,21 +2,21 @@
2
2
  <html>
3
3
  <body>
4
4
  <h3>Dear <%= @token.email %>,</h3>
5
-
5
+
6
6
  <p>
7
7
  You are receiving this e-mail because someone (hopefully you) registered
8
8
  an account at
9
9
  <%= link_to @host, root_url(:host => @host, :protocol => @protocol) %>
10
10
  using your e-mail address.
11
11
  </p>
12
-
12
+
13
13
  <p>
14
14
  Please go
15
15
  <%= link_to 'here', token_session_url(@token, :host => @host,
16
16
  :protocol => @protocol) %>
17
17
  to confirm your e-mail address.
18
18
  </p>
19
-
19
+
20
20
  <p>
21
21
  If you haven't registered an account, please ignore this e-mail. Someone
22
22
  most likely mistyped their e-mail.
@@ -2,21 +2,21 @@
2
2
  <html>
3
3
  <body>
4
4
  <h3>Dear <%= @email %>,</h3>
5
-
5
+
6
6
  <p>
7
7
  You are receiving this e-mail because someone (hopefully you) requested a
8
8
  password reset for your
9
9
  <%= link_to @host, root_url(:host => @host, :protocol => @protocol) %>
10
10
  account.
11
11
  </p>
12
-
12
+
13
13
  <p>
14
14
  Please go
15
15
  <%= link_to 'here', token_session_url(@token, :host => @host,
16
16
  :protocol => @protocol) %>
17
17
  to reset your password.
18
18
  </p>
19
-
19
+
20
20
  <p>
21
21
  If you haven't requested a password reset, please ignore this e-mail.
22
22
  Someone most likely mistyped their e-mail.
@@ -13,25 +13,25 @@ class SessionMailerTest < ActionMailer::TestCase
13
13
  email = SessionMailer.email_verification_email(@verification_token,
14
14
  @root_url).deliver
15
15
  assert !ActionMailer::Base.deliveries.empty?
16
-
16
+
17
17
  assert_equal 'test.host e-mail verification', email.subject
18
18
  assert_equal ['admin@test.host'], email.from
19
19
  assert_equal '"test.host staff" <admin@test.host>', email['from'].to_s
20
20
  assert_equal [@verification_email], email.to
21
21
  assert_match @verification_token.code, email.encoded
22
22
  assert_match @root_url, email.encoded
23
- end
23
+ end
24
24
 
25
25
  test 'password reset email' do
26
26
  email = SessionMailer.reset_password_email(@reset_email, @reset_token,
27
27
  @root_url).deliver
28
28
  assert !ActionMailer::Base.deliveries.empty?
29
-
29
+
30
30
  assert_equal 'test.host password reset', email.subject
31
31
  assert_equal ['admin@test.host'], email.from
32
32
  assert_equal '"test.host staff" <admin@test.host>', email['from'].to_s
33
33
  assert_equal [@reset_email], email.to
34
34
  assert_match @reset_token.code, email.encoded
35
35
  assert_match @root_url, email.encoded
36
- end
36
+ end
37
37
  end
@@ -21,24 +21,24 @@ module MapperMixin
21
21
  controller = options[:controller] || 'session'
22
22
  paths = options[:paths] || controller
23
23
  methods = options[:method_names] || 'session'
24
-
24
+
25
25
  get "/#{paths}/token/:code", :controller => controller, :action => 'token',
26
26
  :as => :"token_#{methods}"
27
-
27
+
28
28
  get "/#{paths}", :controller => controller, :action => 'show',
29
29
  :as => :"#{methods}"
30
30
  get "/#{paths}/new", :controller => controller, :action => 'new',
31
31
  :as => :"new_#{methods}"
32
32
  post "/#{paths}", :controller => controller, :action => 'create'
33
33
  delete "/#{paths}", :controller => controller, :action => 'destroy'
34
-
34
+
35
35
  get "/#{paths}/change_password", :controller => controller,
36
36
  :action => 'password_change',
37
37
  :as => "change_password_#{methods}"
38
38
  post "/#{paths}/change_password", :controller => controller,
39
39
  :action => 'change_password'
40
40
  post "/#{paths}/reset_password", :controller => controller,
41
- :action => 'reset_password',
41
+ :action => 'reset_password',
42
42
  :as => "reset_password_#{methods}"
43
43
  end
44
44
  end
@@ -2,16 +2,16 @@ require 'action_controller'
2
2
 
3
3
  # :nodoc: adds authenticates_using_session
4
4
  class ActionController::Base
5
- # Keeps track of the currently authenticated user via the session.
5
+ # Keeps track of the currently authenticated user via the session.
6
6
  #
7
7
  # Assumes the existence of a User model. A bare ActiveModel model will do the
8
8
  # trick. Model instances must implement id, and the model class must implement
9
9
  # find_by_id.
10
10
  def self.authenticates_using_session(options = {})
11
11
  include Authpwn::ControllerInstanceMethods
12
- before_filter :authenticate_using_session, options
12
+ before_filter :authenticate_using_session, options
13
13
  end
14
-
14
+
15
15
  # True for controllers belonging to the authentication implementation.
16
16
  #
17
17
  # Controllers that return true here are responsible for performing their own
@@ -28,6 +28,29 @@ module Authpwn
28
28
  module ControllerInstanceMethods
29
29
  include Authpwn::CurrentUser
30
30
 
31
+ # Sets up the session so that it will authenticate the given user.
32
+ def set_session_current_user(user)
33
+ # Try to reuse existing sessions.
34
+ if session[:authpwn_suid]
35
+ token = Tokens::SessionUid.with_code session[:authpwn_suid]
36
+ if token
37
+ if token.user == user
38
+ token.touch
39
+ return user
40
+ else
41
+ token.destroy
42
+ end
43
+ end
44
+ end
45
+ if user
46
+ session[:authpwn_suid] = Tokens::SessionUid.random_for(user,
47
+ request.remote_ip, request.user_agent).suid
48
+ else
49
+ session.delete :authpwn_suid
50
+ end
51
+ self.current_user = user
52
+ end
53
+
31
54
  # Filter that implements authenticates_using_session.
32
55
  #
33
56
  # If your ApplicationController contains authenticates_using_session, you
@@ -36,17 +59,17 @@ module ControllerInstanceMethods
36
59
  # skip_before_filter :authenticate_using_session
37
60
  def authenticate_using_session
38
61
  return if current_user
39
- user_param = session[:user_exuid]
40
- user = user_param && User.find_by_param(user_param)
41
- self.current_user = user if user
62
+ session_uid = session[:authpwn_suid]
63
+ user = session_uid && Tokens::SessionUid.authenticate(session_uid)
64
+ self.current_user = user if user && !user.instance_of?(Symbol)
42
65
  end
43
66
  private :authenticate_using_session
44
-
67
+
45
68
  # Inform the user that their request is forbidden.
46
69
  #
47
70
  # If a user is logged on, this renders the session/forbidden view with a HTTP
48
71
  # 403 code.
49
- #
72
+ #
50
73
  # If no user is logged in, the user is redirected to session/new, and the
51
74
  # current request's URL is saved in flash[:auth_redirect_url].
52
75
  def bounce_user(redirect_url = request.url)
@@ -9,10 +9,14 @@ module Authpwn
9
9
  # Session.
10
10
  module SessionController
11
11
  extend ActiveSupport::Concern
12
-
12
+
13
13
  included do
14
14
  skip_filter :authenticate_using_session
15
15
  authenticates_using_session :except => [:create, :reset_password, :token]
16
+
17
+ # If set, every successful login will cause a database purge.
18
+ class_attribute :auto_purge_sessions
19
+ self.auto_purge_sessions = true
16
20
  end
17
21
 
18
22
  # GET /session/new
@@ -33,7 +37,7 @@ module SessionController
33
37
  format.json { render :json => {} }
34
38
  end
35
39
  end
36
- else
40
+ else
37
41
  home
38
42
  unless performed?
39
43
  respond_to do |format|
@@ -48,17 +52,20 @@ module SessionController
48
52
  end
49
53
  end
50
54
  end
51
-
55
+
52
56
  # POST /session
53
57
  def create
54
58
  # Workaround for lack of browser support for the formaction attribute.
55
59
  return reset_password if params[:reset_password]
56
-
60
+
57
61
  @redirect_url = params[:redirect_url] || session_url
58
62
  @email = params[:email]
59
63
  auth = User.authenticate_signin @email, params[:password]
60
- self.current_user = auth unless auth.kind_of? Symbol
61
-
64
+ unless auth.kind_of? Symbol
65
+ self.set_session_current_user auth
66
+ Tokens::SessionUid.remove_expired if auto_purge_sessions
67
+ end
68
+
62
69
  respond_to do |format|
63
70
  if current_user
64
71
  format.html { redirect_to @redirect_url }
@@ -80,17 +87,17 @@ module SessionController
80
87
  end
81
88
  end
82
89
  end
83
-
90
+
84
91
  # POST /session/reset_password
85
92
  def reset_password
86
93
  @email = params[:email]
87
94
  credential = Credentials::Email.with @email
88
-
95
+
89
96
  if user = (credential && credential.user)
90
97
  token = Tokens::PasswordReset.random_for user
91
98
  ::SessionMailer.reset_password_email(@email, token, root_url).deliver
92
99
  end
93
-
100
+
94
101
  respond_to do |format|
95
102
  if user
96
103
  format.html do
@@ -109,7 +116,7 @@ module SessionController
109
116
  end
110
117
  end
111
118
  end
112
-
119
+
113
120
  # GET /session/token/token-code
114
121
  def token
115
122
  if token = Credentials::Token.with_code(params[:code])
@@ -117,7 +124,7 @@ module SessionController
117
124
  else
118
125
  auth = :invalid
119
126
  end
120
-
127
+
121
128
  if auth.is_a? Symbol
122
129
  error_text = bounce_notice_text auth
123
130
  respond_to do |format|
@@ -128,7 +135,7 @@ module SessionController
128
135
  format.json { render :json => { :error => auth, :text => error_text } }
129
136
  end
130
137
  else
131
- self.current_user = auth
138
+ self.set_session_current_user auth
132
139
  home_with_token token
133
140
  unless performed?
134
141
  respond_to do |format|
@@ -148,7 +155,7 @@ module SessionController
148
155
 
149
156
  # DELETE /session
150
157
  def destroy
151
- self.current_user = nil
158
+ self.set_session_current_user nil
152
159
  respond_to do |format|
153
160
  format.html { redirect_to session_url }
154
161
  format.json { head :ok }
@@ -174,14 +181,14 @@ module SessionController
174
181
  end
175
182
  end
176
183
  end
177
-
184
+
178
185
  # POST /session/change_password
179
186
  def change_password
180
187
  unless current_user
181
188
  bounce_user
182
189
  return
183
190
  end
184
-
191
+
185
192
  @credential = current_user.credentials.
186
193
  find { |c| c.is_a? Credentials::Password }
187
194
  if @credential
@@ -222,22 +229,24 @@ module SessionController
222
229
  def home
223
230
  end
224
231
  private :home
225
-
232
+
226
233
  # Hook for setting up the welcome view.
227
234
  def welcome
228
235
  end
229
236
  private :welcome
230
-
237
+
231
238
  # Hook for setting up the home view after token-based authentication.
232
239
  def home_with_token(token)
233
240
  end
234
241
  private :home_with_token
235
242
 
236
- # Hook for customizing the bounce notification text.
243
+ # Hook for customizing the bounce notification text.
237
244
  def bounce_notice_text(reason)
238
245
  case reason
239
246
  when :invalid
240
247
  'Invalid e-mail or password'
248
+ when :expired
249
+ 'Password expired. Please click "Forget password"'
241
250
  when :blocked
242
251
  'Account blocked. Please verify your e-mail address'
243
252
  end