rails_jwt_auth 1.3.1 → 1.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ce38e7a38fa015a6dbf8b1504e41fd273cf3646d0b2d9053c63476d55b3c729
4
- data.tar.gz: 9581d2075661754ed5d43f3a344c8d3fd0da631b9b5c8ea8e51b65fa14bb2c33
3
+ metadata.gz: 69a475be8d5dab54fa47660b2f4b8679cae86ad9b55c94aaa03c203eed2ea77c
4
+ data.tar.gz: bfb0807d0ac1ae764ce96da16343e793585d69379cc262be1082cbe67658d166
5
5
  SHA512:
6
- metadata.gz: f0bdc862727abcdc1db5d1a3e00bd124b7ac15e45d47e483b8487ba46854dfc68edda3a1dcef1d5cda55fb733840f7679f2bbd03996cae3ee4e775d0f12f5a5d
7
- data.tar.gz: 58cd478adf9a9145fb33e7f531e8010faa6e68f86243478515333acc9aa5578531f047ca059620ae1ce867cff8fbbf7a03de5ceb81e30ede35b9d8360e5392b6
6
+ metadata.gz: 65c67921e7cc2a63f219b583716a75b7b1a6fbf8d149f25a7e4163414fc1a9243c3d43b10c028b8020ae6944833cfd649aec5f4bd6f6b3bb71d0852c8832f294
7
+ data.tar.gz: 13dfac135db5c7bd74c6bb49bfe0bff51196190037e11c500e1fa7ad9531d3b4cabfb1aa5b5d208e962db3b058539eb8ae22db526343042d2f814d9c4b871f10
data/README.md CHANGED
@@ -78,11 +78,17 @@ You can edit configuration options into `config/initializers/auth_token_auth.rb`
78
78
  | confirmations_url | nil | Url used to create email link with confirmation token |
79
79
  | reset_passwords_url | nil | Url used to create email link with reset password token |
80
80
  | set_passwords_url | nil | Url used to create email link with set password token |
81
- | invitationss_url | nil | Url used to create email link with invitation token |
81
+ | invitations_url | nil | Url used to create email link with invitation token |
82
+ | maximum_attempts | 3 | Number of failed login attempts before locking an account |
83
+ | lock_strategy | :none | Strategy to be used to lock an account: `:none` or `:failed_attempts` |
84
+ | unlock_strategy | :time | Strategy to use when unlocking accounts: `:time`, `:email` or `:both` |
85
+ | unlock_in | 60.minutes | Interval to unlock an account if `unlock_strategy` is `:time` |
86
+ | reset_attempts_in | 60.minutes | Interval after which to reset failed attempts counter of an account |
87
+ | unlock_url | nil | Url used to create email link with unlock token |
82
88
 
83
89
  ## Modules
84
90
 
85
- It's composed of 5 modules:
91
+ It's composed of 6 modules:
86
92
 
87
93
  | Module | Description |
88
94
  | ------------- | --------------------------------------------------------------------------------------------------------------- |
@@ -91,6 +97,7 @@ It's composed of 5 modules:
91
97
  | Recoverable | Resets the user password and sends reset instructions |
92
98
  | Trackable | Tracks sign in timestamps and IP address |
93
99
  | Invitable | Allows you to invite an user to your application sending an invitation mail |
100
+ | Lockable | Locks the user after a specified number of failed sign in attempts |
94
101
 
95
102
  ## ORMs support
96
103
 
@@ -108,6 +115,7 @@ class User < ApplicationRecord
108
115
  include RailsJwtAuth::Recoverable
109
116
  include RailsJwtAuth::Trackable
110
117
  include RailsJwtAuth::Invitable
118
+ include RailsJwtAuth::Lockable
111
119
 
112
120
  validates :email, presence: true,
113
121
  uniqueness: true,
@@ -127,6 +135,7 @@ class User
127
135
  include RailsJwtAuth::Recoverable
128
136
  include RailsJwtAuth::Trackable
129
137
  include RailsJwtAuth::Invitable
138
+ include RailsJwtAuth::Lockable
130
139
 
131
140
  field :email, type: String
132
141
 
@@ -159,7 +168,7 @@ end
159
168
  end
160
169
  ```
161
170
 
162
- This helper expect that token has been into **AUTHORIZATION** header.
171
+ This helper expect that token has been into **AUTHORIZATION** header.
163
172
  Raises `RailsJwtAuth::NotAuthorized` exception when it fails.
164
173
 
165
174
  - **authenticate**
@@ -342,6 +351,19 @@ Invitations api is provided by `RailsJwtAuth::InvitationsController`.
342
351
 
343
352
  Note: To add more fields, see "Custom strong parameters" below.
344
353
 
354
+ ### Unlocks
355
+
356
+ Unlock api is provided by `RailsJwtAuth::UnlocksController`.
357
+
358
+ 1. Unlock user:
359
+
360
+ ```js
361
+ {
362
+ url: host/unlocks/:unlock_token,
363
+ method: PUT
364
+ }
365
+ ```
366
+
345
367
  ## Customize
346
368
 
347
369
  RailsJwtAuth offers an easy way to customize certain parts.
@@ -10,10 +10,10 @@ module RailsJwtAuth
10
10
  render_422 session: [{error: :invalid_session}]
11
11
  elsif user.respond_to?('confirmed?') && !user.confirmed?
12
12
  render_422 session: [{error: :unconfirmed}]
13
- elsif user.authenticate(session_create_params[:password])
13
+ elsif user.authentication?(session_create_params[:password])
14
14
  render_session generate_jwt(user), user
15
15
  else
16
- render_422 session: [{error: :invalid_session}]
16
+ render_422 session: [user.unauthenticated_error]
17
17
  end
18
18
  end
19
19
 
@@ -0,0 +1,14 @@
1
+ module RailsJwtAuth
2
+ class UnlocksController < ApplicationController
3
+ include ParamsHelper
4
+ include RenderHelper
5
+
6
+ def update
7
+ return render_404 unless
8
+ params[:id] &&
9
+ (user = RailsJwtAuth.model.where(unlock_token: params[:id]).first)
10
+
11
+ user.unlock_access! ? render_204 : render_422(user.errors.details)
12
+ end
13
+ end
14
+ end
@@ -6,10 +6,11 @@ if defined?(ActionMailer)
6
6
  raise RailsJwtAuth::NotConfirmationsUrl unless RailsJwtAuth.confirmations_url.present?
7
7
  @user = user
8
8
 
9
- url, params = RailsJwtAuth.confirmations_url.split('?')
10
- params = params ? params.split('&') : []
11
- params.push("confirmation_token=#{@user.confirmation_token}")
12
- @confirmations_url = "#{url}?#{params.join('&')}"
9
+ @confirmations_url = add_param_to_url(
10
+ RailsJwtAuth.confirmations_url,
11
+ 'confirmation_token',
12
+ @user.confirmation_token
13
+ )
13
14
 
14
15
  subject = I18n.t('rails_jwt_auth.mailer.confirmation_instructions.subject')
15
16
  mail(to: @user.unconfirmed_email || @user[RailsJwtAuth.email_field_name], subject: subject)
@@ -25,10 +26,11 @@ if defined?(ActionMailer)
25
26
  raise RailsJwtAuth::NotResetPasswordsUrl unless RailsJwtAuth.reset_passwords_url.present?
26
27
  @user = user
27
28
 
28
- url, params = RailsJwtAuth.reset_passwords_url.split('?')
29
- params = params ? params.split('&') : []
30
- params.push("reset_password_token=#{@user.reset_password_token}")
31
- @reset_passwords_url = "#{url}?#{params.join('&')}"
29
+ @reset_passwords_url = add_param_to_url(
30
+ RailsJwtAuth.reset_passwords_url,
31
+ 'reset_password_token',
32
+ @user.reset_password_token
33
+ )
32
34
 
33
35
  subject = I18n.t('rails_jwt_auth.mailer.reset_password_instructions.subject')
34
36
  mail(to: @user[RailsJwtAuth.email_field_name], subject: subject)
@@ -38,10 +40,11 @@ if defined?(ActionMailer)
38
40
  raise RailsJwtAuth::NotSetPasswordsUrl unless RailsJwtAuth.set_passwords_url.present?
39
41
  @user = user
40
42
 
41
- url, params = RailsJwtAuth.set_passwords_url.split('?')
42
- params = params ? params.split('&') : []
43
- params.push("reset_password_token=#{@user.reset_password_token}")
44
- @reset_passwords_url = "#{url}?#{params.join('&')}"
43
+ @reset_passwords_url = add_param_to_url(
44
+ RailsJwtAuth.set_passwords_url,
45
+ 'reset_password_token',
46
+ @user.reset_password_token
47
+ )
45
48
 
46
49
  subject = I18n.t('rails_jwt_auth.mailer.set_password_instructions.subject')
47
50
  mail(to: @user[RailsJwtAuth.email_field_name], subject: subject)
@@ -51,13 +54,32 @@ if defined?(ActionMailer)
51
54
  raise RailsJwtAuth::NotInvitationsUrl unless RailsJwtAuth.invitations_url.present?
52
55
  @user = user
53
56
 
54
- url, params = RailsJwtAuth.invitations_url.split '?'
55
- params = params ? params.split('&') : []
56
- params.push("invitation_token=#{@user.invitation_token}")
57
- @invitations_url = "#{url}?#{params.join('&')}"
57
+ @invitations_url = add_param_to_url(
58
+ RailsJwtAuth.invitations_url,
59
+ 'invitation_token',
60
+ @user.invitation_token
61
+ )
58
62
 
59
63
  subject = I18n.t('rails_jwt_auth.mailer.send_invitation.subject')
60
64
  mail(to: @user[RailsJwtAuth.email_field_name], subject: subject)
61
65
  end
66
+
67
+ def send_unlock_instructions(user)
68
+ @user = user
69
+ subject = I18n.t('rails_jwt_auth.mailer.send_unlock_instructions.subject')
70
+
71
+ @unlock_url = add_param_to_url(RailsJwtAuth.unlock_url, 'unlock_token', @user.unlock_token)
72
+
73
+ mail(to: @user[RailsJwtAuth.email_field_name], subject: subject)
74
+ end
75
+
76
+ protected
77
+
78
+ def add_param_to_url(url, param_name, param_value)
79
+ path, params = url.split '?'
80
+ params = params ? params.split('&') : []
81
+ params.push("#{param_name}=#{param_value}")
82
+ "#{path}?#{params.join('&')}"
83
+ end
62
84
  end
63
85
  end
@@ -65,6 +65,14 @@ module RailsJwtAuth
65
65
  end
66
66
  end
67
67
 
68
+ def authentication?(pass)
69
+ authenticate(pass)
70
+ end
71
+
72
+ def unauthenticated_error
73
+ {error: :invalid_session}
74
+ end
75
+
68
76
  module ClassMethods
69
77
  def from_token_payload(payload)
70
78
  if RailsJwtAuth.simultaneous_sessions > 0
@@ -0,0 +1,115 @@
1
+ module RailsJwtAuth
2
+ module Lockable
3
+ BOTH_UNLOCK_STRATEGIES = %i[time email].freeze
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ if defined?(Mongoid) && ancestors.include?(Mongoid::Document)
8
+ field :failed_attempts, type: Integer
9
+ field :unlock_token, type: String
10
+ field :first_failed_attempt_at, type: Time
11
+ field :locked_at, type: Time
12
+ end
13
+ end
14
+ end
15
+
16
+ def lock_access!
17
+ self.locked_at = Time.now.utc
18
+ save(validate: false).tap do |result|
19
+ send_unlock_instructions if result && unlock_strategy_enabled?(:email)
20
+ end
21
+ end
22
+
23
+ def unlock_access!
24
+ self.locked_at = nil
25
+ self.failed_attempts = 0
26
+ self.first_failed_attempt_at = nil
27
+ self.unlock_token = nil
28
+ save(validate: false)
29
+ end
30
+
31
+ def reset_attempts!
32
+ self.failed_attempts = 0
33
+ self.first_failed_attempt_at = nil
34
+ save(validate: false)
35
+ end
36
+
37
+ def authentication?(pass)
38
+ return super(pass) unless lock_strategy_enabled?(:failed_attempts)
39
+
40
+ reset_attempts! if !access_locked? && attempts_expired?
41
+ unlock_access! if lock_expired?
42
+
43
+ if access_locked?
44
+ false
45
+ elsif super(pass)
46
+ unlock_access!
47
+ self
48
+ else
49
+ failed_attempt!
50
+ lock_access! if attempts_exceeded?
51
+ false
52
+ end
53
+ end
54
+
55
+ def unauthenticated_error
56
+ return super unless lock_strategy_enabled?(:failed_attempts)
57
+
58
+ if access_locked?
59
+ {error: :locked}
60
+ else
61
+ {error: :invalid_session, remaining_attempts: remaining_attempts}
62
+ end
63
+ end
64
+
65
+ protected
66
+
67
+ def send_unlock_instructions
68
+ self.unlock_token = SecureRandom.base58(24)
69
+ save(validate: false)
70
+
71
+ mailer = Mailer.send_unlock_instructions(self)
72
+ RailsJwtAuth.deliver_later ? mailer.deliver_later : mailer.deliver
73
+ end
74
+
75
+ def access_locked?
76
+ locked_at && !lock_expired?
77
+ end
78
+
79
+ def lock_expired?
80
+ if unlock_strategy_enabled?(:time)
81
+ locked_at && locked_at < RailsJwtAuth.unlock_in.ago
82
+ else
83
+ false
84
+ end
85
+ end
86
+
87
+ def failed_attempt!
88
+ self.failed_attempts ||= 0
89
+ self.failed_attempts += 1
90
+ self.first_failed_attempt_at = Time.now.utc if failed_attempts == 1
91
+ save(validate: false)
92
+ end
93
+
94
+ def attempts_exceeded?
95
+ failed_attempts && failed_attempts >= RailsJwtAuth.maximum_attempts
96
+ end
97
+
98
+ def remaining_attempts
99
+ RailsJwtAuth.maximum_attempts - failed_attempts.to_i
100
+ end
101
+
102
+ def attempts_expired?
103
+ first_failed_attempt_at && first_failed_attempt_at < RailsJwtAuth.reset_attempts_in.ago
104
+ end
105
+
106
+ def lock_strategy_enabled?(strategy)
107
+ RailsJwtAuth.lock_strategy == strategy
108
+ end
109
+
110
+ def unlock_strategy_enabled?(strategy)
111
+ RailsJwtAuth.unlock_strategy == strategy ||
112
+ (RailsJwtAuth.unlock_strategy == :both && BOTH_UNLOCK_STRATEGIES.include?(strategy))
113
+ end
114
+ end
115
+ end
@@ -27,6 +27,12 @@ module RailsJwtAuth
27
27
  return false
28
28
  end
29
29
 
30
+ if self.class.ancestors.include?(RailsJwtAuth::Lockable) &&
31
+ lock_strategy_enabled?(:failed_attempts) && access_locked?
32
+ errors.add(email_field, :locked)
33
+ return false
34
+ end
35
+
30
36
  self.reset_password_token = SecureRandom.base58(24)
31
37
  self.reset_password_sent_at = Time.current
32
38
  return false unless save
@@ -0,0 +1,7 @@
1
+ <p>Hello <%= @user[RailsJwtAuth.email_field_name] %>!</p>
2
+
3
+ <p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
4
+
5
+ <p>Click the link below to unlock your account:</p>
6
+
7
+ <p><%= link_to 'Unlock my account', @unlock_url.html_safe %></p>
@@ -11,3 +11,5 @@ en:
11
11
  subject: "Someone has sent you an invitation!"
12
12
  email_changed:
13
13
  subject: "Email changed"
14
+ send_unlock_instructions:
15
+ subject: "Unlock instructions"
@@ -12,5 +12,6 @@ class RailsJwtAuth::InstallGenerator < Rails::Generators::Base
12
12
  route "resources :confirmations, controller: 'rails_jwt_auth/confirmations', only: [:create, :update]"
13
13
  route "resources :passwords, controller: 'rails_jwt_auth/passwords', only: [:create, :update]"
14
14
  route "resources :invitations, controller: 'rails_jwt_auth/invitations', only: [:create, :update]"
15
+ route "resources :unlocks, controller: 'rails_jwt_auth/unlocks', only: %i[update]"
15
16
  end
16
17
  end
@@ -44,4 +44,22 @@ RailsJwtAuth.setup do |config|
44
44
 
45
45
  # uses deliver_later to send emails instead of deliver method
46
46
  #config.deliver_later = false
47
+
48
+ # maximum login attempts before locking an account
49
+ #config.maximum_attempts = 3
50
+
51
+ # strategy to lock an account: :none or :failed_attempts
52
+ #config.lock_strategy = :failed_attempts
53
+
54
+ # strategy to use when unlocking accounts: :time, :email or :both
55
+ #config.unlock_strategy = :time
56
+
57
+ # interval to unlock an account if unlock_strategy is :time
58
+ #config.unlock_in = 60.minutes
59
+
60
+ # interval after which to reset failed attempts counter of an account
61
+ #config.reset_attempts_in = 60.minutes
62
+
63
+ # url used to create email link with unlock token
64
+ #config.unlock_url = 'http://frontend.com/unlock-account'
47
65
  end
@@ -24,6 +24,12 @@ class Create<%= RailsJwtAuth.model_name.pluralize %> < ActiveRecord::Migration<%
24
24
  # t.datetime :invitation_sent_at
25
25
  # t.datetime :invitation_accepted_at
26
26
  # t.datetime :invitation_created_at
27
+
28
+ ## Lockable
29
+ # t.integer :failed_attempts
30
+ # t.string :unlock_token
31
+ # t.datetime :first_failed_attempt_at
32
+ # t.datetime :locked_at
27
33
  end
28
34
  end
29
35
  end
@@ -59,6 +59,24 @@ module RailsJwtAuth
59
59
  mattr_accessor :deliver_later
60
60
  self.deliver_later = false
61
61
 
62
+ mattr_accessor :maximum_attempts
63
+ self.maximum_attempts = 3
64
+
65
+ mattr_accessor :lock_strategy
66
+ self.lock_strategy = :none
67
+
68
+ mattr_accessor :unlock_strategy
69
+ self.unlock_strategy = :time
70
+
71
+ mattr_accessor :unlock_in
72
+ self.unlock_in = 60.minutes
73
+
74
+ mattr_accessor :reset_attempts_in
75
+ self.unlock_in = 60.minutes
76
+
77
+ mattr_accessor :unlock_url
78
+ self.unlock_url = nil
79
+
62
80
  def self.model
63
81
  model_name.constantize
64
82
  end
@@ -1,3 +1,3 @@
1
1
  module RailsJwtAuth
2
- VERSION = '1.3.1'
2
+ VERSION = '1.4.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_jwt_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rjurado
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-19 00:00:00.000000000 Z
11
+ date: 2019-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt
@@ -77,17 +77,20 @@ files:
77
77
  - app/controllers/rails_jwt_auth/passwords_controller.rb
78
78
  - app/controllers/rails_jwt_auth/registrations_controller.rb
79
79
  - app/controllers/rails_jwt_auth/sessions_controller.rb
80
+ - app/controllers/rails_jwt_auth/unlocks_controller.rb
80
81
  - app/controllers/unauthorized_controller.rb
81
82
  - app/mailers/rails_jwt_auth/mailer.rb
82
83
  - app/models/concerns/rails_jwt_auth/authenticatable.rb
83
84
  - app/models/concerns/rails_jwt_auth/confirmable.rb
84
85
  - app/models/concerns/rails_jwt_auth/invitable.rb
86
+ - app/models/concerns/rails_jwt_auth/lockable.rb
85
87
  - app/models/concerns/rails_jwt_auth/recoverable.rb
86
88
  - app/models/concerns/rails_jwt_auth/trackable.rb
87
89
  - app/views/rails_jwt_auth/mailer/confirmation_instructions.html.erb
88
90
  - app/views/rails_jwt_auth/mailer/email_changed.html.erb
89
91
  - app/views/rails_jwt_auth/mailer/reset_password_instructions.html.erb
90
92
  - app/views/rails_jwt_auth/mailer/send_invitation.html.erb
93
+ - app/views/rails_jwt_auth/mailer/send_unlock_instructions.html.erb
91
94
  - app/views/rails_jwt_auth/mailer/set_password_instructions.html.erb
92
95
  - config/locales/en.yml
93
96
  - lib/generators/rails_jwt_auth/install_generator.rb