rails_jwt_auth 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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