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.
- data/.travis.yml +7 -2
- data/VERSION +1 -1
- data/app/models/credentials/password.rb +16 -8
- data/app/models/credentials/token.rb +8 -0
- data/app/models/tokens/email_verification.rb +3 -0
- data/app/models/tokens/password_reset.rb +5 -2
- data/app/models/tokens/session_uid.rb +54 -0
- data/authpwn_rails.gemspec +8 -2
- data/lib/authpwn_rails.rb +3 -2
- data/lib/authpwn_rails/current_user.rb +1 -10
- data/lib/authpwn_rails/engine.rb +2 -2
- data/lib/authpwn_rails/expires.rb +23 -0
- data/lib/authpwn_rails/generators/all_generator.rb +9 -4
- data/lib/authpwn_rails/generators/templates/credential.rb +1 -1
- data/lib/authpwn_rails/generators/templates/credentials.yml +16 -0
- data/lib/authpwn_rails/generators/templates/initializer.rb +18 -0
- data/lib/authpwn_rails/generators/templates/session/forbidden.html.erb +1 -1
- data/lib/authpwn_rails/generators/templates/session/home.html.erb +1 -1
- data/lib/authpwn_rails/generators/templates/session/new.html.erb +3 -3
- data/lib/authpwn_rails/generators/templates/session/welcome.html.erb +1 -1
- data/lib/authpwn_rails/generators/templates/session_controller.rb +13 -4
- data/lib/authpwn_rails/generators/templates/session_controller_test.rb +12 -2
- data/lib/authpwn_rails/generators/templates/session_mailer.rb +3 -3
- data/lib/authpwn_rails/generators/templates/session_mailer/email_verification_email.html.erb +3 -3
- data/lib/authpwn_rails/generators/templates/session_mailer/reset_password_email.html.erb +3 -3
- data/lib/authpwn_rails/generators/templates/session_mailer_test.rb +4 -4
- data/lib/authpwn_rails/routes.rb +4 -4
- data/lib/authpwn_rails/session.rb +31 -8
- data/lib/authpwn_rails/session_controller.rb +27 -18
- data/lib/authpwn_rails/test_extensions.rb +16 -6
- data/lib/authpwn_rails/user_model.rb +10 -10
- data/test/cookie_controller_test.rb +165 -16
- data/test/credentials/email_verification_token_test.rb +11 -11
- data/test/credentials/password_credential_test.rb +31 -12
- data/test/credentials/session_uid_token_test.rb +98 -0
- data/test/credentials/token_crendential_test.rb +46 -12
- data/test/helpers/db_setup.rb +6 -5
- data/test/helpers/routes.rb +5 -2
- data/test/initializer_test.rb +18 -0
- data/test/session_controller_api_test.rb +127 -53
- data/test/test_extensions_test.rb +41 -0
- data/test/test_helper.rb +3 -0
- data/test/user_test.rb +11 -10
- 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,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
|
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}>|
|
data/lib/authpwn_rails/generators/templates/session_mailer/email_verification_email.html.erb
CHANGED
@@ -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
|
data/lib/authpwn_rails/routes.rb
CHANGED
@@ -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
|
-
|
40
|
-
user =
|
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
|
-
|
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.
|
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.
|
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
|