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
@@ -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