authkit 0.2.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (23) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +144 -34
  3. data/Rakefile +8 -0
  4. data/lib/authkit/version.rb +1 -1
  5. data/lib/generators/authkit/install_generator.rb +0 -3
  6. data/lib/generators/authkit/templates/app/controllers/application_controller.rb +20 -13
  7. data/lib/generators/authkit/templates/app/controllers/email_confirmation_controller.rb +21 -3
  8. data/lib/generators/authkit/templates/app/controllers/password_change_controller.rb +37 -5
  9. data/lib/generators/authkit/templates/app/controllers/sessions_controller.rb +3 -1
  10. data/lib/generators/authkit/templates/app/controllers/signup_controller.rb +3 -1
  11. data/lib/generators/authkit/templates/app/models/user.rb +48 -52
  12. data/lib/generators/authkit/templates/app/views/password_change/show.html.erb +1 -0
  13. data/lib/generators/authkit/templates/app/views/sessions/new.html.erb +4 -0
  14. data/lib/generators/authkit/templates/app/views/signup/new.html.erb +5 -1
  15. data/lib/generators/authkit/templates/app/views/users/edit.html.erb +1 -1
  16. data/lib/generators/authkit/templates/spec/controllers/application_controller_spec.rb +26 -26
  17. data/lib/generators/authkit/templates/spec/controllers/email_confirmation_controller_spec.rb +28 -10
  18. data/lib/generators/authkit/templates/spec/controllers/password_change_controller_spec.rb +71 -21
  19. data/lib/generators/authkit/templates/spec/controllers/sessions_controller_spec.rb +14 -0
  20. data/lib/generators/authkit/templates/spec/controllers/signup_controller_spec.rb +14 -0
  21. data/lib/generators/authkit/templates/spec/forms/signup_spec.rb +3 -0
  22. data/lib/generators/authkit/templates/spec/models/user_spec.rb +63 -66
  23. metadata +2 -2
@@ -7,10 +7,12 @@ class SignupController < ApplicationController
7
7
  end
8
8
 
9
9
  def create
10
+ remember = params[:remember_me] == "1"
11
+
10
12
  @signup = Signup.new(signup_params)
11
13
 
12
14
  if @signup.save
13
- login(@signup.user)
15
+ login(@signup.user, remember)
14
16
  respond_to do |format|
15
17
  format.json { head :no_content }
16
18
  format.html {
@@ -31,52 +31,6 @@ class User < ActiveRecord::Base
31
31
  # Confirm emails check for existing emails for uniqueness as a convenience
32
32
  validate :confirmation_email_uniqueness, if: :confirmation_email_set?
33
33
 
34
- def self.user_from_token(token)
35
- verifier = ActiveSupport::MessageVerifier.new(Rails.application.config.secret_key_base)
36
- id = verifier.verify(token)
37
- User.where(id: id).first
38
- rescue ActiveSupport::MessageVerifier::InvalidSignature
39
- nil
40
- end
41
-
42
- # The tokens created by this method have unique indexes but they are digests of the
43
- # id which is unique. Because of this we shouldn't see a conflict. If we do, however
44
- # we want the ActiveRecord::StatementInvalid or ActiveRecord::RecordNotUnique exeception
45
- # to bubble up.
46
- def set_token(field)
47
- return unless self.persisted?
48
- verifier = ActiveSupport::MessageVerifier.new(Rails.application.config.secret_key_base)
49
- self.send("#{field}_created_at=", Time.now)
50
- self.send("#{field}=", verifier.generate(self.id))
51
- self.save
52
- end
53
-
54
- # These methods are a little redundant, but give you the opportunity to
55
- # insert expiry for any of these token based authentication strategies.
56
- # For example:
57
- #
58
- # def self.user_from_remember_token(token)
59
- # user = user_from_token(token)
60
- # user = nil if user && user.remember_token_created_at < 30.days.ago
61
- # user
62
- # end
63
- #
64
- def self.user_from_remember_token(token)
65
- user_from_token(token)
66
- end
67
-
68
- def self.user_from_reset_password_token(token)
69
- user_from_token(token)
70
- end
71
-
72
- def self.user_from_confirmation_token(token)
73
- user_from_token(token)
74
- end
75
-
76
- def self.user_from_unlock_token(token)
77
- user_from_token(token)
78
- end
79
-
80
34
  def display_name
81
35
  [first_name, last_name].compact.join(" ")
82
36
  end
@@ -90,31 +44,72 @@ class User < ActiveRecord::Base
90
44
  self.save
91
45
  end
92
46
 
93
- def send_welcome
94
- # TODO: insert your mailer logic here
95
- true
47
+ # The tokens created by this method have unique indexes but collisions are very
48
+ # unlikely (1/64^32). Because of this there shouldn't be a conflict. If one occurs
49
+ # the ActiveRecord::StatementInvalid or ActiveRecord::RecordNotUnique exeception
50
+ # should bubble up.
51
+ def set_remember_token
52
+ self.remember_token = SecureRandom.urlsafe_base64(32)
53
+ self.remember_token_created_at = Time.now
54
+ self.save!
96
55
  end
97
56
 
98
57
  def clear_remember_token
99
58
  self.remember_token = nil
100
59
  self.remember_token_created_at = nil
101
- self.save
60
+ self.save!
61
+ end
62
+
63
+ def reset_password_token_expired?
64
+ # TODO reset password tokens expire in 1 day by default
65
+ self.reset_password_token_created_at.blank? || self.reset_password_token_created_at <= 1.day.ago
66
+ end
67
+
68
+ def confirmation_token_expired?
69
+ # TODO confirmation tokens expire in 3 days by default
70
+ self.confirmation_token_created_at.blank? || self.confirmation_token_created_at <= 3.days.ago
71
+ end
72
+
73
+ def remember_token_expired?
74
+ # TODO remember tokens expire in 1 year by default
75
+ self.remember_token_created_at.blank? || self.remember_token_created_at <= 1.year.ago
102
76
  end
103
77
 
78
+ def send_welcome
79
+ # TODO insert your mailer logic here
80
+ true
81
+ end
82
+
83
+ # The tokens created by this method have unique indexes but collisions are very
84
+ # unlikely (1/64^32). Because of this there shouldn't be a conflict. If one occurs
85
+ # the ActiveRecord::StatementInvalid or ActiveRecord::RecordNotUnique exeception
86
+ # should bubble up.
104
87
  def send_reset_password
105
- return false unless set_token(:reset_password_token)
88
+ self.reset_password_token = SecureRandom.urlsafe_base64(32)
89
+ self.reset_password_token_created_at = Time.now
90
+ self.save!
106
91
 
107
92
  # TODO: insert your mailer logic here
108
93
  true
109
94
  end
110
95
 
96
+ # The tokens created by this method have unique indexes but collisions are very
97
+ # unlikely (1/64^32). Because of this there shouldn't be a conflict. If one occurs
98
+ # the ActiveRecord::StatementInvalid or ActiveRecord::RecordNotUnique exeception
99
+ # should bubble up.
111
100
  def send_confirmation
112
- return false unless set_token(:confirmation_token)
101
+ self.confirmation_token = SecureRandom.urlsafe_base64(32)
102
+ self.confirmation_token_created_at = Time.now
103
+ self.save!
113
104
 
114
105
  # TODO: insert your mailer logic here
115
106
  true
116
107
  end
117
108
 
109
+ def pending_confirmation?
110
+ self.confirmation_token.present?
111
+ end
112
+
118
113
  def email_confirmed
119
114
  return false if self.confirmation_token.blank? || self.confirmation_email.blank?
120
115
 
@@ -172,4 +167,5 @@ class User < ActiveRecord::Base
172
167
  def confirmation_email_uniqueness
173
168
  errors.add(:confirmation_email, :taken, value: email) if User.where('email = ?', confirmation_email).count > 0
174
169
  end
170
+
175
171
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  <%= form_tag(password_change_path, method: "post") do %>
4
4
  <%= hidden_field_tag :token, params[:token] %>
5
+ <%= hidden_field_tag :email, params[:email] %>
5
6
 
6
7
  <div class="field">
7
8
  <%= label_tag "password" %>
@@ -9,5 +9,9 @@
9
9
  <%= label_tag "password" %>
10
10
  <%= password_field_tag "password" %>
11
11
  </div>
12
+ <div class="field">
13
+ <%= check_box_tag :remember_me, "1", true %>
14
+ <label for="remember_me">Keep me signed in on this computer</label>
15
+ </div>
12
16
  <%= submit_tag "Login" %>
13
17
  <% end %>
@@ -35,7 +35,7 @@
35
35
  <%= f.text_field "phone_number" %>
36
36
  </div>
37
37
  <div class="field">
38
- <%= f.label "phone_number" %>
38
+ <%= f.label "time_zone" %>
39
39
  <%= f.time_zone_select('time_zone', ActiveSupport::TimeZone.us_zones, :default => "Pacific Time (US & Canada)") %>
40
40
  </div>
41
41
  <div class="field">
@@ -54,5 +54,9 @@
54
54
  <%= f.label "password_confirmation" %>
55
55
  <%= f.password_field "password_confirmation" %>
56
56
  </div>
57
+ <div class="field">
58
+ <%= check_box_tag :remember_me, "1", true %>
59
+ <label for="remember_me">Keep me signed in on this computer</label>
60
+ </div>
57
61
  <%= f.submit "Sign up" %>
58
62
  <% end %>
@@ -35,7 +35,7 @@
35
35
  <%= f.text_field "phone_number" %>
36
36
  </div>
37
37
  <div class="field">
38
- <%= f.label "phone_number" %>
38
+ <%= f.label "time_zone" %>
39
39
  <%= f.time_zone_select('time_zone', ActiveSupport::TimeZone.us_zones, :default => "Pacific Time (US & Canada)") %>
40
40
  </div>
41
41
  <div class="field">
@@ -11,7 +11,6 @@ describe ApplicationController do
11
11
 
12
12
  controller do
13
13
  before_filter :require_login, only: [:index]
14
- before_filter :require_token, only: [:show]
15
14
 
16
15
  def new
17
16
  head :ok
@@ -46,15 +45,30 @@ describe ApplicationController do
46
45
  end
47
46
 
48
47
  it "finds the current user from the remember cookie" do
48
+ user.save
49
+ user.set_remember_token
49
50
  # Need to sign the cookie
50
51
  request.env["action_dispatch.secret_token"] = "SECRET"
51
52
  verifier = ActiveSupport::MessageVerifier.new(request.env["action_dispatch.secret_token".freeze])
52
- request.cookies[:remember] = verifier.generate("TOKEN")
53
- User.should_receive(:user_from_remember_token).with("TOKEN").and_return(user)
53
+ request.cookies[:remember] = verifier.generate(user.remember_token)
54
54
  get :index
55
55
  controller.send(:current_user).should == user
56
56
  end
57
57
 
58
+ it "doesn't find the current user from the remember cookie if it is expired" do
59
+ # Setup expired token
60
+ user.set_remember_token
61
+ user.remember_token_created_at = 1.year.ago
62
+ user.save
63
+
64
+ # Need to sign the cookie
65
+ request.env["action_dispatch.secret_token"] = "SECRET"
66
+ verifier = ActiveSupport::MessageVerifier.new(request.env["action_dispatch.secret_token".freeze])
67
+ request.cookies[:remember] = verifier.generate(user.remember_token)
68
+ get :index
69
+ controller.send(:current_user).should be_nil
70
+ end
71
+
58
72
  it "sets the time zone" do
59
73
  User.any_instance.should_receive(:time_zone).and_return("Pacific Time (US & Canada)")
60
74
  get :index, {}, logged_in_session
@@ -111,27 +125,6 @@ describe ApplicationController do
111
125
  end
112
126
  end
113
127
 
114
- describe "tokens" do
115
- it "requires a user token" do
116
- User.should_receive(:user_from_token).with('testtoken').and_return(user)
117
- get 'show', {id: '1', token: 'testtoken'}
118
- end
119
-
120
- it "returns an error if there is no user token" do
121
- User.should_receive(:user_from_token).with('testtoken').and_return(nil)
122
- controller.should_receive(:deny_user)
123
- get 'show', {id: '1', token: 'testtoken'}
124
- end
125
-
126
- it "verifies the token" do
127
- request.env["action_dispatch.secret_token"] = "SECRET"
128
- verifier = ActiveSupport::MessageVerifier.new(request.env["action_dispatch.secret_token".freeze])
129
- token = verifier.generate("TOKEN")
130
- User.should_receive(:user_from_token).with(token).and_return(user)
131
- get 'show', {id: '1', token: token}
132
- end
133
- end
134
-
135
128
  describe "login" do
136
129
  it "tracks the login" do
137
130
  get :new
@@ -142,8 +135,15 @@ describe ApplicationController do
142
135
  it "remembers the user using a token and cookie" do
143
136
  get :new
144
137
  controller.should_receive(:set_remember_cookie)
145
- user.should_receive(:set_token).with(:remember_token).and_return(:true)
146
- controller.send(:login, user)
138
+ user.should_receive(:set_remember_token)
139
+ controller.send(:login, user, true)
140
+ end
141
+
142
+ it "does not remember the user using a token and cookie when not requested" do
143
+ get :new
144
+ controller.should_not_receive(:set_remember_cookie)
145
+ user.should_not_receive(:set_remember_token)
146
+ controller.send(:login, user, false)
147
147
  end
148
148
 
149
149
  it "resets the session" do
@@ -7,8 +7,25 @@ describe EmailConfirmationController do
7
7
  let(:token) { "TOKEN" }
8
8
 
9
9
  describe "GET 'show'" do
10
+ it "requires a login" do
11
+ controller.stub(:current_user).and_return(nil)
12
+ get 'show', token: token
13
+ response.should be_redirect
14
+ flash[:error].should_not be_empty
15
+ end
16
+
10
17
  it "requires a valid token" do
11
- User.should_receive(:user_from_token).with(token).and_return(nil)
18
+ user.confirmation_token = "OTHER TOKEN"
19
+ controller.stub(:current_user).and_return(user)
20
+ get 'show', token: token
21
+ response.should be_redirect
22
+ flash[:error].should_not be_empty
23
+ end
24
+
25
+ it "requires an unexpired token" do
26
+ user.confirmation_token = token
27
+ user.confirmation_token_created_at = 4.days.ago
28
+ controller.stub(:current_user).and_return(user)
12
29
  get 'show', token: token
13
30
  response.should be_redirect
14
31
  flash[:error].should_not be_empty
@@ -18,36 +35,37 @@ describe EmailConfirmationController do
18
35
  before(:each) do
19
36
  user.confirmation_email = "new@example.com"
20
37
  user.confirmation_token = token
38
+ user.confirmation_token_created_at = Time.now
21
39
  end
22
40
 
23
41
  describe "when the confirmation is successful" do
24
42
  it "confirms the user email" do
25
- User.should_receive(:user_from_token).with(token).and_return(user)
43
+ controller.stub(:current_user).and_return(user)
26
44
  user.should_receive(:email_confirmed).and_return(true)
27
45
  get 'show', token: token
28
46
  end
29
47
 
30
- it "signs the user in" do
31
- User.should_receive(:user_from_token).with(token).and_return(user)
32
- controller.should_receive(:login).with(user)
48
+ it "does not sign the user in" do
49
+ controller.stub(:current_user).and_return(user)
50
+ controller.should_not_receive(:login)
33
51
  get 'show', token: token
34
52
  end
35
53
 
36
54
  it "sets the flash" do
37
- User.should_receive(:user_from_token).with(token).and_return(user)
55
+ controller.stub(:current_user).and_return(user)
38
56
  get 'show', token: token
39
57
  flash[:notice].should_not be_nil
40
58
  end
41
59
 
42
60
  it "redirects the user" do
43
- User.should_receive(:user_from_token).with(token).and_return(user)
61
+ controller.stub(:current_user).and_return(user)
44
62
  get 'show', token: token
45
63
  response.should be_redirect
46
64
  end
47
65
 
48
66
  describe "from json" do
49
67
  it "returns http success" do
50
- User.should_receive(:user_from_token).with(token).and_return(user)
68
+ controller.stub(:current_user).and_return(user)
51
69
  get 'show', token: token, format: 'json'
52
70
  response.should be_success
53
71
  end
@@ -57,7 +75,7 @@ describe EmailConfirmationController do
57
75
 
58
76
  describe "when the confirmation is not successful" do
59
77
  it "handles invalid confirmations" do
60
- User.should_receive(:user_from_token).with(token).and_return(user)
78
+ controller.stub(:current_user).and_return(user)
61
79
  user.should_receive(:email_confirmed).and_return(false)
62
80
  get 'show', token: token
63
81
  flash[:error].should_not be_empty
@@ -66,7 +84,7 @@ describe EmailConfirmationController do
66
84
 
67
85
  describe "from json" do
68
86
  it "returns a 422" do
69
- User.should_receive(:user_from_token).with(token).and_return(user)
87
+ controller.stub(:current_user).and_return(user)
70
88
  user.should_receive(:email_confirmed).and_return(false)
71
89
  get 'show', token: token, format: 'json'
72
90
  response.code.should == '422'
@@ -3,63 +3,113 @@ require 'spec_helper'
3
3
  describe PasswordChangeController do
4
4
  render_views
5
5
 
6
- let(:user) { build(:user) }
7
6
  let(:token) { "TOKEN" }
7
+ let(:user) { build(:user, reset_password_token: token, reset_password_token_created_at: Time.now) }
8
+ let(:valid_params) { {token: token, email: user.email} }
9
+ let(:password_params) { valid_params.merge(password: 'newpassword', password_confirmation: 'newpassword') }
8
10
 
9
11
  describe "GET 'show'" do
12
+ it "requires no user" do
13
+ controller.stub(:email_user).and_return(user)
14
+ controller.should_receive(:logout)
15
+ get 'show', valid_params
16
+ end
17
+
18
+ it "requires an email user" do
19
+ user.save
20
+ get 'show', valid_params
21
+ assigns(:user).id.should == user.id
22
+ end
23
+
24
+ it "redirects if there is no email user" do
25
+ user.save
26
+ expect {
27
+ get 'show', {token: token, email: "invalid@example.com"}
28
+ }.to raise_error(ActiveRecord::RecordNotFound)
29
+ end
30
+
10
31
  it "requires a valid token" do
11
- User.should_receive(:user_from_token).with(token).and_return(nil)
12
- get 'show', token: token
32
+ controller.stub(:email_user).and_return(user)
33
+ user.reset_password_token = "OTHER TOKEN"
34
+ get 'show', valid_params
35
+ response.should be_redirect
36
+ flash[:error].should_not be_empty
37
+ end
38
+
39
+ it "requires an unexpired token" do
40
+ controller.stub(:email_user).and_return(user)
41
+ user.reset_password_token_created_at = 1.year.ago
42
+ get 'show', valid_params
13
43
  response.should be_redirect
14
44
  flash[:error].should_not be_empty
15
45
  end
16
46
 
17
47
  it "returns http success" do
18
- User.should_receive(:user_from_token).with(token).and_return(user)
19
- get 'show', token: token
48
+ controller.stub(:email_user).and_return(user)
49
+ get 'show', valid_params
20
50
  response.should be_success
21
51
  end
22
52
  end
23
53
 
24
54
  describe "POST 'create'" do
55
+ it "requires no user" do
56
+ controller.stub(:email_user).and_return(user)
57
+ controller.should_receive(:logout)
58
+ get 'show', valid_params
59
+ end
60
+
61
+ it "requires an email user" do
62
+ user.save
63
+ post 'create', password_params
64
+ assigns(:user).id.should == user.id
65
+ end
66
+
67
+ it "redirects if there is no email user" do
68
+ user.save
69
+ expect {
70
+ get 'show', {token: token, email: "invalid@example.com"}
71
+ }.to raise_error(ActiveRecord::RecordNotFound)
72
+ end
73
+
25
74
  it "requires a valid token" do
26
- User.should_receive(:user_from_token).with(token).and_return(nil)
27
- post 'create', {token: token, password: 'newpassword', password_confirmation: 'newpassword'}
75
+ controller.stub(:email_user).and_return(user)
76
+ user.reset_password_token = "OTHER TOKEN"
77
+ post 'create', password_params
28
78
  response.should be_redirect
29
79
  flash[:error].should_not be_empty
30
80
  end
31
81
 
32
82
  describe "with valid params" do
33
83
  before(:each) do
34
- User.should_receive(:user_from_token).with(token).and_return(user)
84
+ controller.stub(:email_user).and_return(user)
35
85
  end
36
86
 
37
87
  it "changes the password" do
38
88
  expect {
39
- post 'create', {token: token, password: 'newpassword', password_confirmation: 'newpassword'}
89
+ post 'create', password_params
40
90
  }.to change(user, :password_digest)
41
91
 
42
92
  user.should be_valid
43
93
  end
44
94
 
45
- it "signs the user in" do
46
- controller.should_receive(:login).with(user)
47
- post 'create', {token: token, password: 'newpassword', password_confirmation: 'newpassword'}
95
+ it "does not sign the user in" do
96
+ controller.should_not_receive(:login)
97
+ post 'create', password_params
48
98
  end
49
99
 
50
100
  it "redirects the user" do
51
- post 'create', {token: token, password: 'newpassword', password_confirmation: 'newpassword'}
101
+ post 'create', password_params
52
102
  response.should be_redirect
53
103
  end
54
104
 
55
105
  it "sets the flash" do
56
- post 'create', {token: token, password: 'newpassword', password_confirmation: 'newpassword'}
106
+ post 'create', password_params
57
107
  flash[:notice].should =~ /successfully/i
58
108
  end
59
109
 
60
110
  describe "from json" do
61
111
  it "returns http success" do
62
- post 'create', {token: token, password: 'newpassword', password_confirmation: 'newpassword', format: 'json'}
112
+ post 'create', password_params.merge(format: 'json')
63
113
  response.should be_success
64
114
  end
65
115
  end
@@ -67,27 +117,27 @@ describe PasswordChangeController do
67
117
 
68
118
  describe "with invalid params" do
69
119
  before(:each) do
70
- User.should_receive(:user_from_token).with(token).and_return(user)
120
+ controller.stub(:email_user).and_return(user)
71
121
  end
72
122
 
73
123
  it "doesn't sign the user in" do
74
124
  controller.should_not_receive(:login)
75
- post 'create', {token: token, password: 'newpassword', password_confirmation: 'invalid'}
125
+ post 'create', {token: token, email: user.email, password: 'newpassword', password_confirmation: 'invalid'}
76
126
  end
77
127
 
78
128
  it "renders the show template" do
79
- post 'create', {token: token, password: 'newpassword', password_confirmation: 'invalid'}
129
+ post 'create', {token: token, email: user.email, password: 'newpassword', password_confirmation: 'invalid'}
80
130
  response.should render_template(:show)
81
131
  end
82
132
 
83
133
  it "has errors" do
84
- post 'create', {token: token, password: 'newpassword', password_confirmation: 'invalid'}
85
- assigns(:user).should have(2).errors_on(:password_confirmation)
134
+ post 'create', {token: token, email: user.email, password: 'newpassword', password_confirmation: 'invalid'}
135
+ user.should have(2).errors_on(:password_confirmation)
86
136
  end
87
137
 
88
138
  describe "from json" do
89
139
  it "returns an error" do
90
- post 'create', {token: token, password: 'newpassword', password_confirmation: 'invalid', format: 'json'}
140
+ post 'create', {token: token, email: user.email, password: 'newpassword', password_confirmation: 'invalid', format: 'json'}
91
141
  response.code.should == '422'
92
142
  response.body.should =~ /doesn't match/i
93
143
  end