rails_jwt_auth 1.6.1 → 2.0.1

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 +187 -88
  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 +26 -9
  8. data/app/controllers/rails_jwt_auth/profiles_controller.rb +50 -0
  9. data/app/controllers/rails_jwt_auth/reset_passwords_controller.rb +65 -0
  10. data/app/controllers/rails_jwt_auth/sessions_controller.rb +5 -21
  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 +29 -42
  13. data/app/models/concerns/rails_jwt_auth/authenticatable.rb +60 -19
  14. data/app/models/concerns/rails_jwt_auth/confirmable.rb +52 -33
  15. data/app/models/concerns/rails_jwt_auth/invitable.rb +42 -78
  16. data/app/models/concerns/rails_jwt_auth/lockable.rb +28 -46
  17. data/app/models/concerns/rails_jwt_auth/recoverable.rb +21 -31
  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 +48 -45
  30. data/lib/rails_jwt_auth/jwt_manager.rb +2 -4
  31. data/lib/rails_jwt_auth/session.rb +132 -0
  32. data/lib/rails_jwt_auth/version.rb +1 -1
  33. metadata +10 -8
  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
@@ -2,14 +2,14 @@ require 'bcrypt'
2
2
 
3
3
  require 'rails_jwt_auth/engine'
4
4
  require 'rails_jwt_auth/jwt_manager'
5
+ require 'rails_jwt_auth/session'
5
6
 
6
7
  module RailsJwtAuth
7
- InvalidEmailField = Class.new(StandardError)
8
- InvalidAuthField = Class.new(StandardError)
9
8
  NotConfirmationsUrl = Class.new(StandardError)
10
9
  NotInvitationsUrl = Class.new(StandardError)
11
10
  NotResetPasswordsUrl = Class.new(StandardError)
12
- NotSetPasswordsUrl = Class.new(StandardError)
11
+ NotUnlockUrl = Class.new(StandardError)
12
+ InvalidJwtPayload = Class.new(StandardError)
13
13
 
14
14
  mattr_accessor :model_name
15
15
  self.model_name = 'User'
@@ -20,6 +20,12 @@ module RailsJwtAuth
20
20
  mattr_accessor :email_field_name
21
21
  self.email_field_name = 'email'
22
22
 
23
+ mattr_accessor :email_regex
24
+ self.email_regex = URI::MailTo::EMAIL_REGEXP
25
+
26
+ mattr_accessor :downcase_auth_field
27
+ self.downcase_auth_field = false
28
+
23
29
  mattr_accessor :jwt_expiration_time
24
30
  self.jwt_expiration_time = 7.days
25
31
 
@@ -29,11 +35,17 @@ module RailsJwtAuth
29
35
  mattr_accessor :simultaneous_sessions
30
36
  self.simultaneous_sessions = 2
31
37
 
38
+ mattr_accessor :mailer_name
39
+ self.mailer_name = 'RailsJwtAuth::Mailer'
40
+
32
41
  mattr_accessor :mailer_sender
33
42
  self.mailer_sender = 'initialize-mailer_sender@example.com'
34
43
 
35
- mattr_accessor :send_email_changed_notification
36
- self.send_email_changed_notification = true
44
+ mattr_accessor :send_email_change_requested_notification
45
+ self.send_email_change_requested_notification = true
46
+
47
+ mattr_accessor :send_password_changed_notification
48
+ self.send_password_changed_notification = true
37
49
 
38
50
  mattr_accessor :confirmation_expiration_time
39
51
  self.confirmation_expiration_time = 1.day
@@ -44,18 +56,6 @@ module RailsJwtAuth
44
56
  mattr_accessor :invitation_expiration_time
45
57
  self.invitation_expiration_time = 2.days
46
58
 
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
59
  mattr_accessor :deliver_later
60
60
  self.deliver_later = false
61
61
 
@@ -72,46 +72,49 @@ module RailsJwtAuth
72
72
  self.unlock_in = 60.minutes
73
73
 
74
74
  mattr_accessor :reset_attempts_in
75
- self.unlock_in = 60.minutes
75
+ self.reset_attempts_in = 60.minutes
76
76
 
77
- mattr_accessor :unlock_url
78
- self.unlock_url = nil
77
+ mattr_accessor :confirm_email_url
78
+ self.confirm_email_url = nil
79
79
 
80
- def self.model
81
- model_name.constantize
82
- end
80
+ mattr_accessor :reset_password_url
81
+ self.reset_password_url = nil
83
82
 
84
- def self.table_name
85
- model_name.underscore.pluralize
86
- end
83
+ mattr_accessor :accept_invitation_url
84
+ self.accept_invitation_url = nil
85
+
86
+ mattr_accessor :unlock_account_url
87
+ self.unlock_account_url = nil
88
+
89
+ mattr_accessor :avoid_email_errors
90
+ self.avoid_email_errors = true
87
91
 
88
92
  def self.setup
89
93
  yield self
90
94
  end
91
95
 
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
96
+ def self.model
97
+ model_name.constantize
98
+ end
101
99
 
102
- field_name
100
+ def self.mailer
101
+ mailer_name.constantize
103
102
  end
104
103
 
105
- def self.email_field_name!
106
- field_name = RailsJwtAuth.email_field_name
107
- klass = RailsJwtAuth.model
104
+ def self.table_name
105
+ model_name.underscore.pluralize
106
+ end
108
107
 
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
108
+ # Thanks to https://github.com/heartcombo/devise/blob/master/lib/devise.rb#L496
109
+ def self.friendly_token(length = 24)
110
+ # To calculate real characters, we must perform this operation.
111
+ # See SecureRandom.urlsafe_base64
112
+ rlength = (length * 3 / 4) - 1
113
+ SecureRandom.urlsafe_base64(rlength, true).tr('lIO0', 'sxyz')
114
+ end
114
115
 
115
- field_name
116
+ def self.send_email(method, user)
117
+ mailer = RailsJwtAuth.mailer.with(user_id: user.id.to_s).public_send(method)
118
+ RailsJwtAuth.deliver_later ? mailer.deliver_later : mailer.deliver
116
119
  end
117
120
  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,132 @@
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_custom
104
+ # allow add custom validation overwriting this method
105
+ end
106
+
107
+ def validate_user_is_confirmed
108
+ add_error(RailsJwtAuth.email_field_name, :unconfirmed) unless @user.confirmed?
109
+ end
110
+
111
+ def validate_user_is_not_locked
112
+ add_error(RailsJwtAuth.email_field_name, :locked) if @user.access_locked?
113
+ end
114
+
115
+ def validate_custom
116
+ # allow add custom validations overwriting this method
117
+ end
118
+
119
+ def add_error(field, detail)
120
+ @errors.details[field.to_sym] ||= []
121
+ @errors.details[field.to_sym].push({error: detail})
122
+ end
123
+
124
+ def errors?
125
+ @errors.details.any?
126
+ end
127
+
128
+ def generate_jwt(request)
129
+ @jwt = JwtManager.encode(user.to_token_payload(request))
130
+ end
131
+ end
132
+ end