rails_jwt_auth 1.7.2 → 2.0.3

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 (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/{email_changed.html.erb → email_change_requested_notification.html.erb} +0 -0
  21. data/app/views/rails_jwt_auth/mailer/{send_invitation.html.erb → invitation_instructions.html.erb} +1 -1
  22. data/app/views/rails_jwt_auth/mailer/password_changed_notification.html.erb +3 -0
  23. data/app/views/rails_jwt_auth/mailer/reset_password_instructions.html.erb +1 -1
  24. data/app/views/rails_jwt_auth/mailer/{send_unlock_instructions.html.erb → unlock_instructions.html.erb} +1 -1
  25. data/config/locales/en.yml +6 -6
  26. data/lib/generators/rails_jwt_auth/install_generator.rb +11 -3
  27. data/lib/generators/templates/initializer.rb +43 -29
  28. data/lib/generators/templates/migration.rb +2 -1
  29. data/lib/rails_jwt_auth.rb +46 -47
  30. data/lib/rails_jwt_auth/jwt_manager.rb +2 -4
  31. data/lib/rails_jwt_auth/session.rb +128 -0
  32. data/lib/rails_jwt_auth/version.rb +1 -1
  33. metadata +11 -15
  34. data/app/controllers/rails_jwt_auth/passwords_controller.rb +0 -32
  35. data/app/views/rails_jwt_auth/mailer/set_password_instructions.html.erb +0 -5
@@ -1,9 +1,18 @@
1
1
  module RailsJwtAuth
2
2
  module Trackable
3
- def update_tracked_fields!(request)
3
+ def track_session_info(request)
4
+ return unless request
5
+
4
6
  self.last_sign_in_at = Time.current
5
7
  self.last_sign_in_ip = request.respond_to?(:remote_ip) ? request.remote_ip : request.ip
6
- save(validate: false)
8
+ end
9
+
10
+ def update_tracked_request_info(request)
11
+ return unless request
12
+
13
+ self.last_request_at = Time.current
14
+ self.last_request_ip = request.respond_to?(:remote_ip) ? request.remote_ip : request.ip
15
+ self.save(validate: false)
7
16
  end
8
17
 
9
18
  def self.included(base)
@@ -11,6 +20,8 @@ module RailsJwtAuth
11
20
  if defined?(Mongoid) && ancestors.include?(Mongoid::Document)
12
21
  field :last_sign_in_at, type: Time
13
22
  field :last_sign_in_ip, type: String
23
+ field :last_request_at, type: Time
24
+ field :last_request_ip, type: String
14
25
  end
15
26
  end
16
27
  end
@@ -2,4 +2,4 @@
2
2
 
3
3
  <p>You can confirm your account email through the link below:</p>
4
4
 
5
- <p><%= link_to 'Confirm my account', @confirmations_url.html_safe %></p>
5
+ <p><%= link_to 'Confirm my account', @confirm_email_url.html_safe %></p>
@@ -3,4 +3,4 @@
3
3
  <p>Someone has sent you an invitation to App.</p>
4
4
  <p>To complete registration setting a password, please click the following link.</p>
5
5
 
6
- <p><%= link_to "Accept invitation", @invitations_url.html_safe %></p>
6
+ <p><%= link_to "Accept invitation", @accept_invitation_url.html_safe %></p>
@@ -0,0 +1,3 @@
1
+ <p>Hello <%= @user[RailsJwtAuth.email_field_name] %>!</p>
2
+
3
+ <p>We're contacting you to notify you that your password has been changed.</p>
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p>Someone has requested a link to change your password. You can do this through the link below.</p>
4
4
 
5
- <p><%= link_to 'Change my password', @reset_passwords_url.html_safe %></p>
5
+ <p><%= link_to 'Change my password', @reset_password_url.html_safe %></p>
6
6
 
7
7
  <p>If you didn't request this, please ignore this email.</p>
8
8
  <p>Your password won't change until you access the link above and create a new one.</p>
@@ -4,4 +4,4 @@
4
4
 
5
5
  <p>Click the link below to unlock your account:</p>
6
6
 
7
- <p><%= link_to 'Unlock my account', @unlock_url.html_safe %></p>
7
+ <p><%= link_to 'Unlock my account', @unlock_account_url.html_safe %></p>
@@ -5,11 +5,11 @@ en:
5
5
  subject: "Confirmation instructions"
6
6
  reset_password_instructions:
7
7
  subject: "Reset password instructions"
8
- set_password_instructions:
9
- subject: "Set password instructions"
10
- send_invitation:
8
+ invitation_instructions:
11
9
  subject: "Someone has sent you an invitation!"
12
- email_changed:
13
- subject: "Email changed"
14
- send_unlock_instructions:
10
+ email_change_requested_notification:
11
+ subject: "Email change"
12
+ password_changed_notification:
13
+ subject: "Password changed"
14
+ unlock_instructions:
15
15
  subject: "Unlock instructions"
@@ -8,10 +8,18 @@ class RailsJwtAuth::InstallGenerator < Rails::Generators::Base
8
8
  def create_routes
9
9
  route "resource :session, controller: 'rails_jwt_auth/sessions', only: [:create, :destroy]"
10
10
  route "resource :registration, controller: 'rails_jwt_auth/registrations', only: [:create]"
11
+ route %q(
12
+ resource :profile, controller: 'rails_jwt_auth/profiles', only: %i[show update] do
13
+ collection do
14
+ put :email
15
+ put :password
16
+ end
17
+ end
18
+ )
11
19
 
12
20
  route "resources :confirmations, controller: 'rails_jwt_auth/confirmations', only: [:create, :update]"
13
- route "resources :passwords, controller: 'rails_jwt_auth/passwords', only: [:create, :update]"
14
- route "resources :invitations, controller: 'rails_jwt_auth/invitations', only: [:create, :update]"
15
- route "resources :unlocks, controller: 'rails_jwt_auth/unlocks', only: %i[update]"
21
+ route "resources :reset_passwords, controller: 'rails_jwt_auth/reset_passwords', only: [:show, :create, :update]"
22
+ route "resources :invitations, controller: 'rails_jwt_auth/invitations', only: [:show, :create, :update]"
23
+ route "resources :unlock_accounts, controller: 'rails_jwt_auth/unlock_accounts', only: %i[update]"
16
24
  end
17
25
  end
@@ -1,65 +1,79 @@
1
1
  RailsJwtAuth.setup do |config|
2
2
  # authentication model class name
3
- #config.model_name = 'User'
3
+ # config.model_name = 'User'
4
4
 
5
5
  # field name used to authentication with password
6
- #config.auth_field_name = 'email'
6
+ # config.auth_field_name = 'email'
7
7
 
8
8
  # define email field name used to send emails
9
- #config.email_field_name = 'email'
9
+ # config.email_field_name = 'email'
10
+
11
+ # Regex used to validate email input on requests like reset password
12
+ # config.email_regex = URI::MailTo::EMAIL_REGEXP
13
+
14
+ # apply downcase to auth field when save user and when init session
15
+ # config.downcase_auth_field = false
10
16
 
11
17
  # expiration time for generated tokens
12
- #config.jwt_expiration_time = 7.days
18
+ # config.jwt_expiration_time = 7.days
13
19
 
14
20
  # the "iss" (issuer) claim identifies the principal that issued the JWT
15
- #config.jwt_issuer = 'RailsJwtAuth'
21
+ # config.jwt_issuer = 'RailsJwtAuth'
16
22
 
17
23
  # number of simultaneously sessions for an user
18
- #config.simultaneous_sessions = 2
24
+ # config.simultaneous_sessions = 2
25
+
26
+ # mailer class name
27
+ # config.mailer_name = 'RailsJwtAuth::Mailer'
19
28
 
20
29
  # mailer sender
21
- #config.mailer_sender = 'initialize-mailer_sender@example.com'
30
+ # config.mailer_sender = 'initialize-mailer_sender@example.com'
31
+
32
+ # activate email notification when email is changed
33
+ # config.send_email_change_requested_notification = true
34
+
35
+ # activate email notification when password is changed
36
+ # config.send_password_changed_notification = true
22
37
 
23
38
  # expiration time for confirmation tokens
24
- #config.confirmation_expiration_time = 1.day
39
+ # config.confirmation_expiration_time = 1.day
25
40
 
26
41
  # expiration time for reset password tokens
27
- #config.reset_password_expiration_time = 1.day
42
+ # config.reset_password_expiration_time = 1.day
28
43
 
29
44
  # time an invitation is valid after sent
30
45
  # config.invitation_expiration_time = 2.days
31
46
 
32
- # url used to create email link with confirmation token
33
- #config.confirmations_url = 'http://frontend.com/confirmation'
34
-
35
- # url used to create email link with reset password token
36
- #config.reset_passwords_url = 'http://frontend.com/reset_password'
37
-
38
- # url used to create email link with set password token
39
- # by set_and_send_password_instructions method
40
- #config.set_passwords_url = 'http://frontend.com/set_password'
41
-
42
- # url used to create email link with activation token parameter to accept invitation
43
- #config.invitations_url = 'http://frontend.com/accept_invitation'
44
-
45
47
  # uses deliver_later to send emails instead of deliver method
46
- #config.deliver_later = false
48
+ # config.deliver_later = false
47
49
 
48
50
  # maximum login attempts before locking an account
49
- #config.maximum_attempts = 3
51
+ # config.maximum_attempts = 3
50
52
 
51
53
  # strategy to lock an account: :none or :failed_attempts
52
- #config.lock_strategy = :failed_attempts
54
+ # config.lock_strategy = :failed_attempts
53
55
 
54
56
  # strategy to use when unlocking accounts: :time, :email or :both
55
- #config.unlock_strategy = :time
57
+ # config.unlock_strategy = :time
56
58
 
57
59
  # interval to unlock an account if unlock_strategy is :time
58
- #config.unlock_in = 60.minutes
60
+ # config.unlock_in = 60.minutes
59
61
 
60
62
  # interval after which to reset failed attempts counter of an account
61
- #config.reset_attempts_in = 60.minutes
63
+ # config.reset_attempts_in = 60.minutes
64
+ #
65
+ # url used to create email link with confirmation token
66
+ # config.confirm_email_url = 'http://frontend.com/confirm-email'
67
+
68
+ # url used to create email link with reset password token
69
+ # config.reset_password_url = 'http://frontend.com/reset-password'
70
+
71
+ # url used to create email link with activation token parameter to accept invitation
72
+ # config.accept_invitation_url = 'http://frontend.com/accept-invitation'
62
73
 
63
74
  # url used to create email link with unlock token
64
- #config.unlock_url = 'http://frontend.com/unlock-account'
75
+ # config.unlock_account_url = 'http://frontend.com/unlock-account'
76
+
77
+ # set false to avoid giving clue about the existing emails with errors
78
+ # config.avoid_email_errors = true
65
79
  end
@@ -18,12 +18,13 @@ class Create<%= RailsJwtAuth.model_name.pluralize %> < ActiveRecord::Migration<%
18
18
  ## Trackable
19
19
  # t.string :last_sign_in_ip
20
20
  # t.datetime :last_sign_in_at
21
+ # t.string :last_request_ip
22
+ # t.datetime :last_request_at
21
23
 
22
24
  ## Invitable
23
25
  # t.string :invitation_token
24
26
  # t.datetime :invitation_sent_at
25
27
  # t.datetime :invitation_accepted_at
26
- # t.datetime :invitation_created_at
27
28
 
28
29
  ## Lockable
29
30
  # t.integer :failed_attempts
@@ -1,15 +1,16 @@
1
+ require 'active_support/core_ext/integer/time'
1
2
  require 'bcrypt'
2
3
 
3
4
  require 'rails_jwt_auth/engine'
4
5
  require 'rails_jwt_auth/jwt_manager'
6
+ require 'rails_jwt_auth/session'
5
7
 
6
8
  module RailsJwtAuth
7
- InvalidEmailField = Class.new(StandardError)
8
- InvalidAuthField = Class.new(StandardError)
9
9
  NotConfirmationsUrl = Class.new(StandardError)
10
10
  NotInvitationsUrl = Class.new(StandardError)
11
11
  NotResetPasswordsUrl = Class.new(StandardError)
12
- NotSetPasswordsUrl = Class.new(StandardError)
12
+ NotUnlockUrl = Class.new(StandardError)
13
+ InvalidJwtPayload = Class.new(StandardError)
13
14
 
14
15
  mattr_accessor :model_name
15
16
  self.model_name = 'User'
@@ -20,6 +21,12 @@ module RailsJwtAuth
20
21
  mattr_accessor :email_field_name
21
22
  self.email_field_name = 'email'
22
23
 
24
+ mattr_accessor :email_regex
25
+ self.email_regex = URI::MailTo::EMAIL_REGEXP
26
+
27
+ mattr_accessor :downcase_auth_field
28
+ self.downcase_auth_field = false
29
+
23
30
  mattr_accessor :jwt_expiration_time
24
31
  self.jwt_expiration_time = 7.days
25
32
 
@@ -29,11 +36,17 @@ module RailsJwtAuth
29
36
  mattr_accessor :simultaneous_sessions
30
37
  self.simultaneous_sessions = 2
31
38
 
39
+ mattr_accessor :mailer_name
40
+ self.mailer_name = 'RailsJwtAuth::Mailer'
41
+
32
42
  mattr_accessor :mailer_sender
33
43
  self.mailer_sender = 'initialize-mailer_sender@example.com'
34
44
 
35
- mattr_accessor :send_email_changed_notification
36
- self.send_email_changed_notification = true
45
+ mattr_accessor :send_email_change_requested_notification
46
+ self.send_email_change_requested_notification = true
47
+
48
+ mattr_accessor :send_password_changed_notification
49
+ self.send_password_changed_notification = true
37
50
 
38
51
  mattr_accessor :confirmation_expiration_time
39
52
  self.confirmation_expiration_time = 1.day
@@ -44,18 +57,6 @@ module RailsJwtAuth
44
57
  mattr_accessor :invitation_expiration_time
45
58
  self.invitation_expiration_time = 2.days
46
59
 
47
- mattr_accessor :confirmations_url
48
- self.confirmations_url = nil
49
-
50
- mattr_accessor :reset_passwords_url
51
- self.reset_passwords_url = nil
52
-
53
- mattr_accessor :set_passwords_url
54
- self.set_passwords_url = nil
55
-
56
- mattr_accessor :invitations_url
57
- self.invitations_url = nil
58
-
59
60
  mattr_accessor :deliver_later
60
61
  self.deliver_later = false
61
62
 
@@ -72,51 +73,49 @@ module RailsJwtAuth
72
73
  self.unlock_in = 60.minutes
73
74
 
74
75
  mattr_accessor :reset_attempts_in
75
- self.unlock_in = 60.minutes
76
+ self.reset_attempts_in = 60.minutes
76
77
 
77
- mattr_accessor :unlock_url
78
- self.unlock_url = nil
78
+ mattr_accessor :confirm_email_url
79
+ self.confirm_email_url = nil
79
80
 
80
- def self.model
81
- model_name.constantize
82
- end
81
+ mattr_accessor :reset_password_url
82
+ self.reset_password_url = nil
83
83
 
84
- def self.table_name
85
- model_name.underscore.pluralize
86
- end
84
+ mattr_accessor :accept_invitation_url
85
+ self.accept_invitation_url = nil
86
+
87
+ mattr_accessor :unlock_account_url
88
+ self.unlock_account_url = nil
89
+
90
+ mattr_accessor :avoid_email_errors
91
+ self.avoid_email_errors = true
87
92
 
88
93
  def self.setup
89
94
  yield self
90
95
  end
91
96
 
92
- def self.auth_field_name!
93
- field_name = RailsJwtAuth.auth_field_name
94
- klass = RailsJwtAuth.model
95
-
96
- unless field_name.present? &&
97
- (klass.respond_to?(:column_names) && klass.column_names.include?(field_name) ||
98
- klass.respond_to?(:fields) && klass.fields[field_name])
99
- raise RailsJwtAuth::InvalidAuthField
100
- end
101
-
102
- field_name
97
+ def self.model
98
+ model_name.constantize
103
99
  end
104
100
 
105
- def self.email_field_name!
106
- field_name = RailsJwtAuth.email_field_name
107
- klass = RailsJwtAuth.model
101
+ def self.mailer
102
+ mailer_name.constantize
103
+ end
108
104
 
109
- unless field_name.present? &&
110
- (klass.respond_to?(:column_names) && klass.column_names.include?(field_name) ||
111
- klass.respond_to?(:fields) && klass.fields[field_name])
112
- raise RailsJwtAuth::InvalidEmailField
113
- end
105
+ def self.table_name
106
+ model_name.underscore.pluralize
107
+ end
114
108
 
115
- field_name
109
+ # Thanks to https://github.com/heartcombo/devise/blob/master/lib/devise.rb#L496
110
+ def self.friendly_token(length = 24)
111
+ # To calculate real characters, we must perform this operation.
112
+ # See SecureRandom.urlsafe_base64
113
+ rlength = (length * 3 / 4) - 1
114
+ SecureRandom.urlsafe_base64(rlength, true).tr('lIO0', 'sxyz')
116
115
  end
117
116
 
118
117
  def self.send_email(method, user)
119
- mailer = RailsJwtAuth::Mailer.with(user_id: user.id.to_s).public_send(method)
118
+ mailer = RailsJwtAuth.mailer.with(user_id: user.id.to_s).public_send(method)
120
119
  RailsJwtAuth.deliver_later ? mailer.deliver_later : mailer.deliver
121
120
  end
122
121
  end
@@ -8,6 +8,8 @@ module RailsJwtAuth
8
8
 
9
9
  # Encodes and signs JWT Payload with expiration
10
10
  def self.encode(payload)
11
+ raise InvalidJwtPayload unless payload
12
+
11
13
  payload.reverse_merge!(meta)
12
14
  JWT.encode(payload, secret_key_base)
13
15
  end
@@ -25,9 +27,5 @@ module RailsJwtAuth
25
27
  iss: RailsJwtAuth.jwt_issuer
26
28
  }
27
29
  end
28
-
29
- def self.decode_from_request(request)
30
- decode(request.env['HTTP_AUTHORIZATION']&.split&.last)
31
- end
32
30
  end
33
31
  end
@@ -0,0 +1,128 @@
1
+ module RailsJwtAuth
2
+ class Session
3
+ attr_reader :user, :errors, :jwt
4
+
5
+ Errors = Struct.new :details # simulate ActiveModel::Errors
6
+
7
+ def initialize(params={})
8
+ @auth_field_value = (params[RailsJwtAuth.auth_field_name] || '').strip
9
+ @auth_field_value.downcase! if RailsJwtAuth.downcase_auth_field
10
+ @password = params[:password]
11
+
12
+ find_user if @auth_field_value.present?
13
+ end
14
+
15
+ def valid?
16
+ validate!
17
+
18
+ !errors?
19
+ end
20
+
21
+ def generate!(request)
22
+ if valid?
23
+ user.clean_reset_password if recoverable?
24
+ user.clean_lock if lockable?
25
+ user.track_session_info(request) if trackable?
26
+ user.load_auth_token
27
+
28
+ unless user.save
29
+ add_error(RailsJwtAuth.model_name.underscore, :invalid)
30
+
31
+ return false
32
+ end
33
+
34
+ generate_jwt(request)
35
+
36
+ true
37
+ else
38
+ user.failed_attempt if lockable?
39
+
40
+ false
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def validate!
47
+ # Can't use ActiveModel::Validations since we have dynamic fields
48
+ @errors = Errors.new({})
49
+
50
+ validate_auth_field_presence
51
+ validate_password_presence
52
+ validate_user_exist
53
+ validate_user_is_confirmed if confirmable?
54
+ validate_user_is_not_locked if lockable?
55
+ validate_user_password unless errors?
56
+ validate_custom
57
+ end
58
+
59
+ def find_user
60
+ @user = RailsJwtAuth.model.where(RailsJwtAuth.auth_field_name => @auth_field_value).first
61
+ end
62
+
63
+ def confirmable?
64
+ @user&.kind_of?(RailsJwtAuth::Confirmable)
65
+ end
66
+
67
+ def lockable?
68
+ @user&.kind_of?(RailsJwtAuth::Lockable)
69
+ end
70
+
71
+ def recoverable?
72
+ @user&.kind_of?(RailsJwtAuth::Recoverable)
73
+ end
74
+
75
+ def trackable?
76
+ @user&.kind_of?(RailsJwtAuth::Trackable)
77
+ end
78
+
79
+ def user?
80
+ @user.present?
81
+ end
82
+
83
+ def field_error(field)
84
+ RailsJwtAuth.avoid_email_errors ? :session : field
85
+ end
86
+
87
+ def validate_auth_field_presence
88
+ add_error(RailsJwtAuth.auth_field_name, :blank) if @auth_field_value.blank?
89
+ end
90
+
91
+ def validate_password_presence
92
+ add_error(:password, :blank) if @password.blank?
93
+ end
94
+
95
+ def validate_user_exist
96
+ add_error(field_error(RailsJwtAuth.auth_field_name), :invalid) unless @user
97
+ end
98
+
99
+ def validate_user_password
100
+ add_error(field_error(:password), :invalid) unless @user.authenticate(@password)
101
+ end
102
+
103
+ def validate_user_is_confirmed
104
+ add_error(RailsJwtAuth.email_field_name, :unconfirmed) unless @user.confirmed?
105
+ end
106
+
107
+ def validate_user_is_not_locked
108
+ add_error(RailsJwtAuth.email_field_name, :locked) if @user.access_locked?
109
+ end
110
+
111
+ def validate_custom
112
+ # allow add custom validations overwriting this method
113
+ end
114
+
115
+ def add_error(field, detail)
116
+ @errors.details[field.to_sym] ||= []
117
+ @errors.details[field.to_sym].push({error: detail})
118
+ end
119
+
120
+ def errors?
121
+ @errors.details.any?
122
+ end
123
+
124
+ def generate_jwt(request)
125
+ @jwt = JwtManager.encode(user.to_token_payload(request))
126
+ end
127
+ end
128
+ end