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 +4 -4
- data/README.md +25 -3
- data/app/controllers/rails_jwt_auth/sessions_controller.rb +2 -2
- data/app/controllers/rails_jwt_auth/unlocks_controller.rb +14 -0
- data/app/mailers/rails_jwt_auth/mailer.rb +38 -16
- data/app/models/concerns/rails_jwt_auth/authenticatable.rb +8 -0
- data/app/models/concerns/rails_jwt_auth/lockable.rb +115 -0
- data/app/models/concerns/rails_jwt_auth/recoverable.rb +6 -0
- data/app/views/rails_jwt_auth/mailer/send_unlock_instructions.html.erb +7 -0
- data/config/locales/en.yml +2 -0
- data/lib/generators/rails_jwt_auth/install_generator.rb +1 -0
- data/lib/generators/templates/initializer.rb +18 -0
- data/lib/generators/templates/migration.rb +6 -0
- data/lib/rails_jwt_auth.rb +18 -0
- data/lib/rails_jwt_auth/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 69a475be8d5dab54fa47660b2f4b8679cae86ad9b55c94aaa03c203eed2ea77c
|
4
|
+
data.tar.gz: bfb0807d0ac1ae764ce96da16343e793585d69379cc262be1082cbe67658d166
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
|
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
|
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.
|
13
|
+
elsif user.authentication?(session_create_params[:password])
|
14
14
|
render_session generate_jwt(user), user
|
15
15
|
else
|
16
|
-
render_422 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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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>
|
data/config/locales/en.yml
CHANGED
@@ -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
|
data/lib/rails_jwt_auth.rb
CHANGED
@@ -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
|
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.
|
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-
|
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
|