rails_jwt_auth 1.7.3 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +188 -89
  3. data/app/controllers/concerns/rails_jwt_auth/authenticable_helper.rb +15 -7
  4. data/app/controllers/concerns/rails_jwt_auth/params_helper.rb +18 -4
  5. data/app/controllers/concerns/rails_jwt_auth/render_helper.rb +10 -2
  6. data/app/controllers/rails_jwt_auth/confirmations_controller.rb +48 -10
  7. data/app/controllers/rails_jwt_auth/invitations_controller.rb +27 -9
  8. data/app/controllers/rails_jwt_auth/profiles_controller.rb +51 -0
  9. data/app/controllers/rails_jwt_auth/reset_passwords_controller.rb +65 -0
  10. data/app/controllers/rails_jwt_auth/sessions_controller.rb +7 -22
  11. data/app/controllers/rails_jwt_auth/{unlocks_controller.rb → unlock_accounts_controller.rb} +2 -2
  12. data/app/mailers/rails_jwt_auth/mailer.rb +23 -28
  13. data/app/models/concerns/rails_jwt_auth/authenticatable.rb +60 -19
  14. data/app/models/concerns/rails_jwt_auth/confirmable.rb +49 -39
  15. data/app/models/concerns/rails_jwt_auth/invitable.rb +46 -72
  16. data/app/models/concerns/rails_jwt_auth/lockable.rb +38 -46
  17. data/app/models/concerns/rails_jwt_auth/recoverable.rb +27 -26
  18. data/app/models/concerns/rails_jwt_auth/trackable.rb +13 -2
  19. data/app/views/rails_jwt_auth/mailer/confirmation_instructions.html.erb +1 -1
  20. data/app/views/rails_jwt_auth/mailer/{send_invitation.html.erb → invitation_instructions.html.erb} +1 -1
  21. data/app/views/rails_jwt_auth/mailer/password_changed_notification.html.erb +3 -0
  22. data/app/views/rails_jwt_auth/mailer/reset_password_instructions.html.erb +1 -1
  23. data/app/views/rails_jwt_auth/mailer/{send_unlock_instructions.html.erb → unlock_instructions.html.erb} +1 -1
  24. data/config/locales/en.yml +6 -6
  25. data/lib/generators/rails_jwt_auth/install_generator.rb +11 -3
  26. data/lib/generators/templates/initializer.rb +43 -29
  27. data/lib/generators/templates/migration.rb +2 -1
  28. data/lib/rails_jwt_auth/jwt_manager.rb +2 -4
  29. data/lib/rails_jwt_auth/session.rb +128 -0
  30. data/lib/rails_jwt_auth/version.rb +1 -1
  31. data/lib/rails_jwt_auth.rb +46 -47
  32. metadata +11 -9
  33. data/app/controllers/rails_jwt_auth/passwords_controller.rb +0 -32
  34. data/app/views/rails_jwt_auth/mailer/set_password_instructions.html.erb +0 -5
  35. /data/app/views/rails_jwt_auth/mailer/{email_changed.html.erb → email_change_requested_notification.html.erb} +0 -0
@@ -0,0 +1,51 @@
1
+ module RailsJwtAuth
2
+ class ProfilesController < ApplicationController
3
+ include AuthenticableHelper
4
+ include ParamsHelper
5
+ include RenderHelper
6
+
7
+ PASSWORD_PARAMS = %i[current_password password password_confirmation].freeze
8
+
9
+ before_action :authenticate!
10
+
11
+ def show
12
+ render_profile current_user
13
+ end
14
+
15
+ def update
16
+ if current_user.update(profile_update_params)
17
+ render_204
18
+ else
19
+ render_422(current_user.errors.details)
20
+ end
21
+ end
22
+
23
+ def password
24
+ if current_user.update_password(update_password_params)
25
+ render_204
26
+ else
27
+ render_422(current_user.errors.details)
28
+ end
29
+ end
30
+
31
+ def email
32
+ return update unless current_user.is_a?(RailsJwtAuth::Confirmable)
33
+
34
+ if current_user.update_email(profile_update_email_params)
35
+ render_204
36
+ else
37
+ render_422(current_user.errors.details)
38
+ end
39
+ end
40
+
41
+ protected
42
+
43
+ def changing_password?
44
+ profile_update_params.values_at(*PASSWORD_PARAMS).any?(&:present?)
45
+ end
46
+
47
+ def update_password_params
48
+ profile_update_password_params.merge(current_auth_token: jwt_payload['auth_token'])
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,65 @@
1
+ module RailsJwtAuth
2
+ class ResetPasswordsController < ApplicationController
3
+ include ParamsHelper
4
+ include RenderHelper
5
+
6
+ before_action :set_user_from_token, only: [:show, :update]
7
+ before_action :set_user_from_email, only: [:create]
8
+
9
+ # used to verify token
10
+ def show
11
+ return render_404 unless @user
12
+
13
+ if @user.reset_password_sent_at < RailsJwtAuth.reset_password_expiration_time.ago
14
+ return render_410
15
+ end
16
+
17
+ render_204
18
+ end
19
+
20
+ # used to request restore password
21
+ def create
22
+ unless @user
23
+ if RailsJwtAuth.avoid_email_errors
24
+ return render_204
25
+ else
26
+ return render_422(RailsJwtAuth.email_field_name => [{error: :not_found}])
27
+ end
28
+ end
29
+
30
+ @user.send_reset_password_instructions ? render_204 : render_422(@user.errors.details)
31
+ end
32
+
33
+ # used to set new password
34
+ def update
35
+ return render_404 unless @user
36
+
37
+ if @user.set_reset_password(reset_password_update_params)
38
+ render_204
39
+ else
40
+ render_422(@user.errors.details)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def set_user_from_token
47
+ return if params[:id].blank?
48
+
49
+ @user = RailsJwtAuth.model.where(reset_password_token: params[:id]).first
50
+ end
51
+
52
+ def set_user_from_email
53
+ email = (reset_password_create_params[RailsJwtAuth.email_field_name] || '').strip
54
+ email.downcase! if RailsJwtAuth.downcase_auth_field
55
+
56
+ if email.blank?
57
+ return render_422(RailsJwtAuth.email_field_name => [{error: :blank}])
58
+ elsif !email.match?(RailsJwtAuth.email_regex)
59
+ return render_422(RailsJwtAuth.email_field_name => [{error: :format}])
60
+ end
61
+
62
+ @user = RailsJwtAuth.model.where(RailsJwtAuth.email_field_name => email).first
63
+ end
64
+ end
65
+ end
@@ -1,40 +1,25 @@
1
1
  module RailsJwtAuth
2
2
  class SessionsController < ApplicationController
3
+ include AuthenticableHelper
3
4
  include ParamsHelper
4
5
  include RenderHelper
5
6
 
6
7
  def create
7
- user = find_user
8
+ se = Session.new(session_create_params)
8
9
 
9
- if !user
10
- render_422 session: [{error: :invalid_session}]
11
- elsif user.respond_to?('confirmed?') && !user.confirmed?
12
- render_422 session: [{error: :unconfirmed}]
13
- elsif user.authentication?(session_create_params[:password])
14
- render_session generate_jwt(user), user
10
+ if se.generate!(request)
11
+ render_session se.jwt, se.user
15
12
  else
16
- render_422 session: [user.unauthenticated_error]
13
+ render_422 se.errors.details
17
14
  end
18
15
  end
19
16
 
20
17
  def destroy
21
- return render_404 unless RailsJwtAuth.simultaneous_sessions > 0
18
+ return render_404 unless RailsJwtAuth.simultaneous_sessions.positive?
22
19
 
23
20
  authenticate!
24
- payload = JwtManager.decode_from_request(request)&.first
25
- current_user.destroy_auth_token payload['auth_token']
21
+ current_user.destroy_auth_token @jwt_payload['auth_token']
26
22
  render_204
27
23
  end
28
-
29
- private
30
-
31
- def generate_jwt(user)
32
- JwtManager.encode(user.to_token_payload(request))
33
- end
34
-
35
- def find_user
36
- auth_field = RailsJwtAuth.auth_field_name!
37
- RailsJwtAuth.model.where(auth_field => session_create_params[auth_field]).first
38
- end
39
24
  end
40
25
  end
@@ -1,5 +1,5 @@
1
1
  module RailsJwtAuth
2
- class UnlocksController < ApplicationController
2
+ class UnlockAccountsController < ApplicationController
3
3
  include ParamsHelper
4
4
  include RenderHelper
5
5
 
@@ -8,7 +8,7 @@ module RailsJwtAuth
8
8
  params[:id] &&
9
9
  (user = RailsJwtAuth.model.where(unlock_token: params[:id]).first)
10
10
 
11
- user.unlock_access! ? render_204 : render_422(user.errors.details)
11
+ user.unlock_access ? render_204 : render_422(user.errors.details)
12
12
  end
13
13
  end
14
14
  end
@@ -4,65 +4,60 @@ if defined?(ActionMailer)
4
4
 
5
5
  before_action do
6
6
  @user = RailsJwtAuth.model.find(params[:user_id])
7
+ @to = @user[RailsJwtAuth.email_field_name]
7
8
  @subject = I18n.t("rails_jwt_auth.mailer.#{action_name}.subject")
8
9
  end
9
10
 
10
11
  def confirmation_instructions
11
- raise RailsJwtAuth::NotConfirmationsUrl unless RailsJwtAuth.confirmations_url.present?
12
+ raise RailsJwtAuth::NotConfirmationsUrl unless RailsJwtAuth.confirm_email_url.present?
12
13
 
13
- @confirmations_url = add_param_to_url(
14
- RailsJwtAuth.confirmations_url,
14
+ @confirm_email_url = add_param_to_url(
15
+ RailsJwtAuth.confirm_email_url,
15
16
  'confirmation_token',
16
17
  @user.confirmation_token
17
18
  )
18
19
 
19
- mail(to: @user.unconfirmed_email || @user[RailsJwtAuth.email_field_name], subject: @subject)
20
+ mail(to: @user.unconfirmed_email || @to, subject: @subject)
20
21
  end
21
22
 
22
- def email_changed
23
- mail(to: @user[RailsJwtAuth.email_field_name!], subject: @subject)
23
+ def email_change_requested_notification
24
+ mail(to: @to, subject: @subject)
24
25
  end
25
26
 
26
27
  def reset_password_instructions
27
- raise RailsJwtAuth::NotResetPasswordsUrl unless RailsJwtAuth.reset_passwords_url.present?
28
+ raise RailsJwtAuth::NotResetPasswordsUrl unless RailsJwtAuth.reset_password_url.present?
28
29
 
29
- @reset_passwords_url = add_param_to_url(
30
- RailsJwtAuth.reset_passwords_url,
30
+ @reset_password_url = add_param_to_url(
31
+ RailsJwtAuth.reset_password_url,
31
32
  'reset_password_token',
32
33
  @user.reset_password_token
33
34
  )
34
35
 
35
- mail(to: @user[RailsJwtAuth.email_field_name], subject: @subject)
36
+ mail(to: @to, subject: @subject)
36
37
  end
37
38
 
38
- def set_password_instructions
39
- raise RailsJwtAuth::NotSetPasswordsUrl unless RailsJwtAuth.set_passwords_url.present?
40
-
41
- @reset_passwords_url = add_param_to_url(
42
- RailsJwtAuth.set_passwords_url,
43
- 'reset_password_token',
44
- @user.reset_password_token
45
- )
46
-
47
- mail(to: @user[RailsJwtAuth.email_field_name], subject: @subject)
39
+ def password_changed_notification
40
+ mail(to: @to, subject: @subject)
48
41
  end
49
42
 
50
- def send_invitation
51
- raise RailsJwtAuth::NotInvitationsUrl unless RailsJwtAuth.invitations_url.present?
43
+ def invitation_instructions
44
+ raise RailsJwtAuth::NotInvitationsUrl unless RailsJwtAuth.accept_invitation_url.present?
52
45
 
53
- @invitations_url = add_param_to_url(
54
- RailsJwtAuth.invitations_url,
46
+ @accept_invitation_url = add_param_to_url(
47
+ RailsJwtAuth.accept_invitation_url,
55
48
  'invitation_token',
56
49
  @user.invitation_token
57
50
  )
58
51
 
59
- mail(to: @user[RailsJwtAuth.email_field_name], subject: @subject)
52
+ mail(to: @to, subject: @subject)
60
53
  end
61
54
 
62
- def send_unlock_instructions
63
- @unlock_url = add_param_to_url(RailsJwtAuth.unlock_url, 'unlock_token', @user.unlock_token)
55
+ def unlock_instructions
56
+ raise RailsJwtAuth::NotUnlockUrl unless RailsJwtAuth.unlock_account_url.present?
57
+
58
+ @unlock_account_url = add_param_to_url(RailsJwtAuth.unlock_account_url, 'unlock_token', @user.unlock_token)
64
59
 
65
- mail(to: @user[RailsJwtAuth.email_field_name], subject: @subject)
60
+ mail(to: @to, subject: @subject)
66
61
  end
67
62
 
68
63
  protected
@@ -8,28 +8,41 @@ module RailsJwtAuth
8
8
  base.class_eval do
9
9
  if defined?(Mongoid) && ancestors.include?(Mongoid::Document)
10
10
  field :password_digest, type: String
11
- field :auth_tokens, type: Array if RailsJwtAuth.simultaneous_sessions > 0
11
+ field :auth_tokens, type: Array, default: [] if RailsJwtAuth.simultaneous_sessions > 0
12
12
  elsif defined?(ActiveRecord) && ancestors.include?(ActiveRecord::Base)
13
13
  serialize :auth_tokens, Array
14
14
  end
15
15
 
16
16
  has_secure_password
17
+
18
+ before_validation do
19
+ if RailsJwtAuth.downcase_auth_field &&
20
+ public_send("#{RailsJwtAuth.auth_field_name}_changed?")
21
+ self[RailsJwtAuth.auth_field_name]&.downcase!
22
+ end
23
+ end
17
24
  end
18
25
  end
19
26
 
20
- def regenerate_auth_token(token = nil)
27
+ def load_auth_token
21
28
  new_token = SecureRandom.base58(24)
22
29
 
23
30
  if RailsJwtAuth.simultaneous_sessions > 1
24
- tokens = ((auth_tokens || []) - [token]).last(RailsJwtAuth.simultaneous_sessions - 1)
25
- update_attribute(:auth_tokens, (tokens + [new_token]).uniq)
31
+ tokens = (auth_tokens || []).last(RailsJwtAuth.simultaneous_sessions - 1)
32
+ self.auth_tokens = (tokens + [new_token]).uniq
26
33
  else
27
- update_attribute(:auth_tokens, [new_token])
34
+ self.auth_tokens = [new_token]
28
35
  end
29
36
 
30
37
  new_token
31
38
  end
32
39
 
40
+ def regenerate_auth_token(token=nil)
41
+ self.auth_tokens -= [token] if token
42
+ token = load_auth_token
43
+ save ? token : false
44
+ end
45
+
33
46
  def destroy_auth_token(token)
34
47
  if RailsJwtAuth.simultaneous_sessions > 1
35
48
  tokens = auth_tokens || []
@@ -39,7 +52,34 @@ module RailsJwtAuth
39
52
  end
40
53
  end
41
54
 
42
- def update_with_password(params)
55
+ def to_token_payload(_request=nil)
56
+ if RailsJwtAuth.simultaneous_sessions > 0
57
+ auth_tokens&.last ? {auth_token: auth_tokens.last} : false
58
+ else
59
+ {id: id.to_s}
60
+ end
61
+ end
62
+
63
+ def save_without_password
64
+ # when set password to nil only password_digest is setted to nil
65
+ # https://github.com/rails/rails/blob/master/activemodel/lib/active_model/secure_password.rb#L97
66
+ instance_variable_set("@password", nil)
67
+ self.password_confirmation = nil
68
+ self.password_digest = nil
69
+
70
+ return false unless valid_without_password?
71
+
72
+ save(validate: false)
73
+ end
74
+
75
+ def valid_without_password?
76
+ valid?
77
+ errors.delete(:password) # allow register without pass
78
+ errors.delete(:password_confirmation)
79
+ errors.empty?
80
+ end
81
+
82
+ def update_password(params)
43
83
  current_password_error = if (current_password = params.delete(:current_password)).blank?
44
84
  'blank'
45
85
  elsif !authenticate(current_password)
@@ -51,28 +91,29 @@ module RailsJwtAuth
51
91
  self.reset_password_token = self.reset_password_sent_at = nil
52
92
  end
53
93
 
94
+ # close all sessions or other sessions when pass current_auth_token
95
+ current_auth_token = params.delete :current_auth_token
96
+ self.auth_tokens = current_auth_token ? [current_auth_token] : []
97
+
54
98
  assign_attributes(params)
55
99
  valid? # validates first other fields
56
100
  errors.add(:current_password, current_password_error) if current_password_error
57
101
  errors.add(:password, 'blank') if params[:password].blank?
58
102
 
59
- errors.empty? ? save : false
60
- end
103
+ return false unless errors.empty?
104
+ return false unless save
61
105
 
62
- def to_token_payload(_request=nil)
63
- if RailsJwtAuth.simultaneous_sessions > 0
64
- {auth_token: regenerate_auth_token}
65
- else
66
- {id: id.to_s}
67
- end
68
- end
106
+ deliver_password_changed_notification
69
107
 
70
- def authentication?(pass)
71
- authenticate(pass)
108
+ true
72
109
  end
73
110
 
74
- def unauthenticated_error
75
- {error: :invalid_session}
111
+ protected
112
+
113
+ def deliver_password_changed_notification
114
+ return unless RailsJwtAuth.send_password_changed_notification
115
+
116
+ RailsJwtAuth.send_email(:password_changed_notification, self)
76
117
  end
77
118
 
78
119
  module ClassMethods
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RailsJwtAuth
2
4
  module Confirmable
3
5
  def self.included(base)
@@ -20,47 +22,16 @@ module RailsJwtAuth
20
22
  send_confirmation_instructions
21
23
  end
22
24
  end
23
-
24
- before_update do
25
- email_field = RailsJwtAuth.email_field_name!
26
-
27
- if public_send("#{email_field}_changed?") &&
28
- public_send("#{email_field}_was") &&
29
- !confirmed_at_changed? &&
30
- !self['invitation_token']
31
- self.unconfirmed_email = self[email_field]
32
- self[email_field] = public_send("#{email_field}_was")
33
-
34
- self.confirmation_token = SecureRandom.base58(24)
35
- self.confirmation_sent_at = Time.current
36
- end
37
- end
38
-
39
- if defined?(ActiveRecord) && ancestors.include?(ActiveRecord::Base)
40
- after_commit do
41
- if unconfirmed_email && saved_change_to_unconfirmed_email?
42
- deliver_email_changed_emails
43
- end
44
- end
45
- elsif defined?(Mongoid) && ancestors.include?(Mongoid::Document)
46
- after_update do
47
- if unconfirmed_email && unconfirmed_email_changed?
48
- deliver_email_changed_emails
49
- end
50
- end
51
- end
52
25
  end
53
26
  end
54
27
 
55
28
  def send_confirmation_instructions
56
- email_field = RailsJwtAuth.email_field_name!
57
-
58
29
  if confirmed? && !unconfirmed_email
59
- errors.add(email_field, :already_confirmed)
30
+ errors.add(RailsJwtAuth.email_field_name, :already_confirmed)
60
31
  return false
61
32
  end
62
33
 
63
- self.confirmation_token = SecureRandom.base58(24)
34
+ self.confirmation_token = generate_confirmation_token
64
35
  self.confirmation_sent_at = Time.current
65
36
  return false unless save
66
37
 
@@ -72,12 +43,12 @@ module RailsJwtAuth
72
43
  confirmed_at.present?
73
44
  end
74
45
 
75
- def confirm!
46
+ def confirm
76
47
  self.confirmed_at = Time.current
77
48
  self.confirmation_token = nil
78
49
 
79
50
  if unconfirmed_email
80
- email_field = RailsJwtAuth.email_field_name!
51
+ email_field = RailsJwtAuth.email_field_name
81
52
 
82
53
  self[email_field] = unconfirmed_email
83
54
  self.unconfirmed_email = nil
@@ -91,17 +62,56 @@ module RailsJwtAuth
91
62
  save
92
63
  end
93
64
 
94
- def skip_confirmation!
65
+ def skip_confirmation
95
66
  self.confirmed_at = Time.current
96
67
  self.confirmation_token = nil
97
68
  end
98
69
 
70
+ def update_email(params)
71
+ email_field = RailsJwtAuth.email_field_name.to_sym
72
+ params = HashWithIndifferentAccess.new(params)
73
+
74
+ # email change must be protected by password
75
+ password_error = if (password = params[:password]).blank?
76
+ :blank
77
+ elsif !authenticate(password)
78
+ :invalid
79
+ end
80
+
81
+ self.email = params[email_field]
82
+ self.confirmation_token = generate_confirmation_token
83
+ self.confirmation_sent_at = Time.current
84
+
85
+ valid? # validates first other fields
86
+ errors.add(:password, password_error) if password_error
87
+ errors.add(email_field, :not_change) unless email_changed?
88
+
89
+ return false unless errors.empty?
90
+
91
+ # move email to unconfirmed_email field and restore
92
+ self.unconfirmed_email = email
93
+ self.email = email_was
94
+
95
+ return false unless save
96
+
97
+ deliver_email_changed_emails
98
+
99
+ true
100
+ end
101
+
99
102
  protected
100
103
 
104
+ def generate_confirmation_token
105
+ loop do
106
+ token = RailsJwtAuth.friendly_token
107
+ return token unless self.class.where(confirmation_token: token).exists?
108
+ end
109
+ end
110
+
101
111
  def validate_confirmation
102
112
  return true unless confirmed_at
103
113
 
104
- email_field = RailsJwtAuth.email_field_name!
114
+ email_field = RailsJwtAuth.email_field_name
105
115
 
106
116
  if confirmed_at_was && !public_send("#{email_field}_changed?")
107
117
  errors.add(email_field, :already_confirmed)
@@ -116,8 +126,8 @@ module RailsJwtAuth
116
126
  RailsJwtAuth.send_email(:confirmation_instructions, self)
117
127
 
118
128
  # send notify to old email
119
- if RailsJwtAuth.send_email_changed_notification
120
- RailsJwtAuth.send_email(:email_changed, self)
129
+ if RailsJwtAuth.send_email_change_requested_notification
130
+ RailsJwtAuth.send_email(:email_change_requested_notification, self)
121
131
  end
122
132
  end
123
133
  end