shieldify 0.1.2.pre.alpha
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +375 -0
- data/Rakefile +3 -0
- data/app/controllers/users/access_controller.rb +16 -0
- data/app/controllers/users/emails/reset_passwords_controller.rb +30 -0
- data/app/controllers/users/emails_controller.rb +17 -0
- data/app/models/jwt_session.rb +5 -0
- data/lib/generators/shieldify/USAGE +8 -0
- data/lib/generators/shieldify/install_generator.rb +52 -0
- data/lib/generators/shieldify/templates/initializer.rb.tt +52 -0
- data/lib/generators/shieldify/templates/locales/en.shieldify.yml.tt +58 -0
- data/lib/generators/shieldify/templates/locales/es.shieldify.yml.tt +48 -0
- data/lib/generators/shieldify/templates/mailer_layouts/mailer.html.erb +10 -0
- data/lib/generators/shieldify/templates/mailer_layouts/mailer.text.erb +3 -0
- data/lib/generators/shieldify/templates/mailer_views/email_changed.html.erb +3 -0
- data/lib/generators/shieldify/templates/mailer_views/email_changed.text.erb +5 -0
- data/lib/generators/shieldify/templates/mailer_views/email_confirmation_instructions.html.erb +7 -0
- data/lib/generators/shieldify/templates/mailer_views/email_confirmation_instructions.text.erb +7 -0
- data/lib/generators/shieldify/templates/mailer_views/password_changed.html.erb +3 -0
- data/lib/generators/shieldify/templates/mailer_views/password_changed.text.erb +5 -0
- data/lib/generators/shieldify/templates/mailer_views/reset_email_password_instructions.html.erb +5 -0
- data/lib/generators/shieldify/templates/mailer_views/reset_email_password_instructions.text.erb +9 -0
- data/lib/generators/shieldify/templates/mailer_views/unlock_access_instructions.html.erb +4 -0
- data/lib/generators/shieldify/templates/mailer_views/unlock_access_instructions.text.erb +7 -0
- data/lib/generators/shieldify/templates/migration.rb.tt +28 -0
- data/lib/generators/shieldify/templates/model.rb.tt +2 -0
- data/lib/shieldify/controllers/helpers.rb +29 -0
- data/lib/shieldify/failure_app.rb +8 -0
- data/lib/shieldify/jwt_service.rb +158 -0
- data/lib/shieldify/mailer.rb +44 -0
- data/lib/shieldify/middleware/authentication.rb +27 -0
- data/lib/shieldify/middleware.rb +36 -0
- data/lib/shieldify/model_extensions.rb +73 -0
- data/lib/shieldify/models/email_authenticatable/confirmable.rb +159 -0
- data/lib/shieldify/models/email_authenticatable/registerable.rb +117 -0
- data/lib/shieldify/models/email_authenticatable.rb +41 -0
- data/lib/shieldify/railtie.rb +52 -0
- data/lib/shieldify/strategies/email.rb +48 -0
- data/lib/shieldify/strategies/jwt.rb +78 -0
- data/lib/shieldify/version.rb +3 -0
- data/lib/shieldify.rb +74 -0
- data/lib/tasks/shieldify_tasks.rake +4 -0
- metadata +163 -0
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shieldify
|
4
|
+
module Models
|
5
|
+
module EmailAuthenticatable
|
6
|
+
# This module implements email confirmation using tokens. It generates confirmation tokens
|
7
|
+
# when registering or changing a user's email address, enabling confirmation through a secure process.
|
8
|
+
# Includes functionality to automatically send confirmation instructions and manage email confirmations.
|
9
|
+
# When changing an email, the new one is automatically marked as pending confirmation,
|
10
|
+
# requiring the user to verify their access to the new email to complete confirmation.
|
11
|
+
# Additionally, it provides methods to check the expiration of confirmation tokens.
|
12
|
+
#
|
13
|
+
# @example Including the module in a User model
|
14
|
+
# class User < ApplicationRecord
|
15
|
+
# include Shieldify::Models::EmailAuthenticatable::Confirmable
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# @see #confirm_email
|
19
|
+
# @see #regenerate_and_save_email_confirmation_token
|
20
|
+
# @see #send_email_confirmation_instructions
|
21
|
+
# @see #email_confirmation_token_expired?
|
22
|
+
# @see #pending_email_confirmation?
|
23
|
+
# @see #confirmed?
|
24
|
+
# @see .confirm_email_by_token
|
25
|
+
# @see .resend_email_confirmation_instructions_to
|
26
|
+
# @see .add_error_to_empty_user
|
27
|
+
module Confirmable
|
28
|
+
extend ActiveSupport::Concern
|
29
|
+
|
30
|
+
included do
|
31
|
+
# @!attribute [rw] skip_email_confirmation_callbacks
|
32
|
+
# @return [Boolean] Allows selectively skipping email confirmation callbacks.
|
33
|
+
attr_accessor :skip_email_confirmation_callbacks
|
34
|
+
|
35
|
+
before_save :generate_email_confirmation, unless: -> { skip_email_confirmation_callbacks? || !email_changed? }
|
36
|
+
after_save(
|
37
|
+
:send_email_confirmation_instructions,
|
38
|
+
unless: -> { skip_email_confirmation_callbacks? || !unconfirmed_email? }
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Confirms the email if there is a pending email confirmation.
|
43
|
+
# This method updates the user's email to the unconfirmed email and clears the confirmation tokens.
|
44
|
+
#
|
45
|
+
# @return [Boolean] true if the email was successfully confirmed, false otherwise
|
46
|
+
def confirm_email
|
47
|
+
return false unless pending_email_confirmation?
|
48
|
+
|
49
|
+
self.skip_email_confirmation_callbacks = true
|
50
|
+
|
51
|
+
self.email = unconfirmed_email
|
52
|
+
self.unconfirmed_email = nil
|
53
|
+
self.email_confirmation_token = nil
|
54
|
+
self.email_confirmation_token_generated_at = nil
|
55
|
+
|
56
|
+
save do |result|
|
57
|
+
self.skip_email_confirmation_callbacks = nil
|
58
|
+
|
59
|
+
result
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Regenerates the email confirmation token and saves the user record.
|
64
|
+
#
|
65
|
+
# @return [Boolean] true if the token was successfully regenerated and saved, false otherwise
|
66
|
+
def regenerate_and_save_email_confirmation_token
|
67
|
+
generate_email_confirmation_token && save
|
68
|
+
end
|
69
|
+
|
70
|
+
# Sends email confirmation instructions to the unconfirmed email.
|
71
|
+
#
|
72
|
+
# @return [void]
|
73
|
+
def send_email_confirmation_instructions
|
74
|
+
params = { user: self, email_to: unconfirmed_email, token: email_confirmation_token, action: :email_confirmation_instructions }
|
75
|
+
Shieldify::Mailer.with(params).base_mailer.deliver_now
|
76
|
+
end
|
77
|
+
|
78
|
+
# Checks if the email confirmation token has expired.
|
79
|
+
#
|
80
|
+
# @return [Boolean] true if the token has expired, false otherwise
|
81
|
+
def email_confirmation_token_expired?
|
82
|
+
return true unless email_confirmation_token_generated_at
|
83
|
+
|
84
|
+
email_confirmation_token_generated_at < 24.hours.ago
|
85
|
+
end
|
86
|
+
|
87
|
+
# Checks if there is a pending email confirmation.
|
88
|
+
#
|
89
|
+
# @return [Boolean] true if there is a pending email confirmation, false otherwise
|
90
|
+
def pending_email_confirmation?
|
91
|
+
unconfirmed_email.present? && email_confirmation_token.present?
|
92
|
+
end
|
93
|
+
|
94
|
+
# Checks if the email has been confirmed.
|
95
|
+
#
|
96
|
+
# @return [Boolean] true if the email is confirmed, false otherwise
|
97
|
+
def confirmed?
|
98
|
+
email.present?
|
99
|
+
end
|
100
|
+
|
101
|
+
def skip_email_confirmation_callbacks?
|
102
|
+
@skip_email_confirmation_callbacks
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def generate_email_confirmation
|
108
|
+
self.unconfirmed_email = email
|
109
|
+
self.email = email_was
|
110
|
+
|
111
|
+
generate_email_confirmation_token
|
112
|
+
end
|
113
|
+
|
114
|
+
def generate_email_confirmation_token
|
115
|
+
self.email_confirmation_token = SecureRandom.hex(16)
|
116
|
+
self.email_confirmation_token_generated_at = Time.current
|
117
|
+
end
|
118
|
+
|
119
|
+
class_methods do
|
120
|
+
def confirm_email_by_token(token)
|
121
|
+
user = find_by_email_confirmation_token(token)
|
122
|
+
|
123
|
+
return add_error_to_empty_user(:email_confirmation_token, :invalid) if user.blank?
|
124
|
+
|
125
|
+
if user.email_confirmation_token_expired?
|
126
|
+
msg = I18n.t('shieldify.models.email_authenticatable.confirmable.email_confirmation_token.errors.expired')
|
127
|
+
user.errors.add(:email_confirmation_token, msg)
|
128
|
+
|
129
|
+
return user
|
130
|
+
end
|
131
|
+
|
132
|
+
user.confirm_email
|
133
|
+
user
|
134
|
+
end
|
135
|
+
|
136
|
+
def resend_email_confirmation_instructions_to(email)
|
137
|
+
user = find_by_unconfirmed_email(email)
|
138
|
+
|
139
|
+
return add_error_to_empty_user(:unconfirmed_email, :not_found) if user.nil?
|
140
|
+
|
141
|
+
user.regenerate_and_save_email_confirmation_token
|
142
|
+
user
|
143
|
+
end
|
144
|
+
|
145
|
+
def add_error_to_empty_user(param, error)
|
146
|
+
user = new
|
147
|
+
|
148
|
+
user.errors.add(
|
149
|
+
param.to_sym,
|
150
|
+
I18n.t("shieldify.models.email_authenticatable.confirmable.#{param.to_sym}.errors.#{error.to_sym}")
|
151
|
+
)
|
152
|
+
|
153
|
+
user
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shieldify
|
4
|
+
module Models
|
5
|
+
module EmailAuthenticatable
|
6
|
+
# This module provides registration functionality for users, including email normalization,
|
7
|
+
# validation of email and password, and methods for registering a user and updating their
|
8
|
+
# email and password.
|
9
|
+
#
|
10
|
+
# @example Including the module in a User model
|
11
|
+
# class User < ApplicationRecord
|
12
|
+
# include Shieldify::Models::EmailAuthenticatable::Registerable
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# @see .register
|
16
|
+
# @see #update_password
|
17
|
+
# @see #update_email
|
18
|
+
# @see #send_email_changed_notification
|
19
|
+
# @see #send_password_changed_notification
|
20
|
+
module Registerable
|
21
|
+
extend ActiveSupport::Concern
|
22
|
+
|
23
|
+
included do
|
24
|
+
before_validation :normalize_email
|
25
|
+
|
26
|
+
validates :email, presence: true, if: -> { password.present? && new_record? }
|
27
|
+
validates :email, format: { with: Shieldify::Configuration.email_regexp }, if: -> { email.present? }
|
28
|
+
validates :email, uniqueness: true, if: -> { email.present? }
|
29
|
+
|
30
|
+
validates :password, presence: true, if: -> { email.present? && new_record? }
|
31
|
+
validate :password_complexity, if: -> { password.present? }
|
32
|
+
validates :password, length: { minimum: 8 }, if: -> { password.present? }
|
33
|
+
end
|
34
|
+
|
35
|
+
class_methods do
|
36
|
+
# Registers a new user with the given email and password.
|
37
|
+
#
|
38
|
+
# @param email [String] The email of the user.
|
39
|
+
# @param password [String] The password of the user.
|
40
|
+
# @param password_confirmation [String] The password confirmation.
|
41
|
+
# @return [User] The newly registered user.
|
42
|
+
def register(email:, password:, password_confirmation:)
|
43
|
+
user = new(email: email, password: password, password_confirmation: password_confirmation)
|
44
|
+
user.save
|
45
|
+
user
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Updates the user's password if the current password is valid.
|
50
|
+
#
|
51
|
+
# @param current_password [String] The current password of the user.
|
52
|
+
# @param new_password [String] The new password.
|
53
|
+
# @param password_confirmation [String] The new password confirmation.
|
54
|
+
# @return [User] The user with the updated password, or with errors if the update failed.
|
55
|
+
def update_password(current_password:, new_password:, password_confirmation:)
|
56
|
+
if authenticate(current_password)
|
57
|
+
if update(password: new_password, password_confirmation: password_confirmation)
|
58
|
+
send_password_changed_notification if Shieldify::Configuration.send_password_changed_notification
|
59
|
+
end
|
60
|
+
else
|
61
|
+
errors.add(
|
62
|
+
:current_password,
|
63
|
+
I18n.t("shieldify.models.email_authenticatable.registerable.password.errors.invalid")
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
# Updates the user's email if the current password is valid.
|
71
|
+
#
|
72
|
+
# @param current_password [String] The current password of the user.
|
73
|
+
# @param new_email [String] The new email.
|
74
|
+
# @return [User] The user with the updated email, or with errors if the update failed.
|
75
|
+
def update_email(current_password:, new_email:)
|
76
|
+
if authenticate(current_password)
|
77
|
+
if update(email: new_email)
|
78
|
+
send_email_changed_notification if Shieldify::Configuration.send_email_changed_notification
|
79
|
+
end
|
80
|
+
else
|
81
|
+
errors.add(
|
82
|
+
:password,
|
83
|
+
I18n.t("shieldify.models.email_authenticatable.registerable.password.errors.invalid"))
|
84
|
+
end
|
85
|
+
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def send_email_changed_notification
|
92
|
+
Shieldify::Mailer.with(user: self, email_to: email, action: :email_changed).base_mailer.deliver_now
|
93
|
+
end
|
94
|
+
|
95
|
+
def send_password_changed_notification
|
96
|
+
Shieldify::Mailer.with(user: self, email_to: email, action: :password_changed).base_mailer.deliver_now
|
97
|
+
end
|
98
|
+
|
99
|
+
def normalize_email
|
100
|
+
self.email = email.downcase.strip if email.present?
|
101
|
+
end
|
102
|
+
|
103
|
+
def password_complexity
|
104
|
+
return if password.blank?
|
105
|
+
regex = Shieldify::Configuration.password_complexity
|
106
|
+
|
107
|
+
unless password.match?(regex)
|
108
|
+
errors.add(
|
109
|
+
:password,
|
110
|
+
I18n.t("shieldify.models.email_authenticatable.registerable.password_complexity.format")
|
111
|
+
)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shieldify
|
4
|
+
module Models
|
5
|
+
# This module provides email authentication functionality for users. It includes methods to authenticate
|
6
|
+
# users by their email and password, and uses `has_secure_password` for secure password handling.
|
7
|
+
#
|
8
|
+
# @example Including the module in a User model
|
9
|
+
# class User < ApplicationRecord
|
10
|
+
# include Shieldify::Models::EmailAuthenticatable
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# @see .authenticate_by_email
|
14
|
+
module EmailAuthenticatable
|
15
|
+
extend ActiveSupport::Concern
|
16
|
+
|
17
|
+
included do
|
18
|
+
# Adds methods to set and authenticate against a BCrypt password. This mechanism requires you to have a
|
19
|
+
# password_digest attribute.
|
20
|
+
has_secure_password(validations: false)
|
21
|
+
end
|
22
|
+
|
23
|
+
class_methods do
|
24
|
+
# Authenticates a user by their email and password.
|
25
|
+
#
|
26
|
+
# @param email [String] The email of the user.
|
27
|
+
# @param password [String] The password of the user.
|
28
|
+
# @return [User] The authenticated user if the credentials are correct, or a new user object with errors if not.
|
29
|
+
def authenticate_by_email(email:, password:)
|
30
|
+
user = find_by(email: email)
|
31
|
+
|
32
|
+
return user if user&.authenticate(password)
|
33
|
+
|
34
|
+
user ||= new
|
35
|
+
user.errors.add(:email, "invalid email or password")
|
36
|
+
user
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Shieldify
|
2
|
+
class Railtie < ::Rails::Railtie
|
3
|
+
initializer 'shieldify.add_routes' do |app|
|
4
|
+
app.routes.prepend do
|
5
|
+
get 'shfy/users/email/:token/confirm', to: 'users/emails#show', as: :users_email_confirmation
|
6
|
+
# post 'shfy/users/email/reset_password', to: 'users/emails/reset_passwords#create'
|
7
|
+
# put 'shfy/users/email/:token/reset_password', to: 'users/emails/reset_passwords#update'
|
8
|
+
# get 'shfy/users/access/:token/unlock', to: 'users/access#show'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer 'shieldify.configure_warden' do |app|
|
13
|
+
app.middleware.use Warden::Manager do |manager|
|
14
|
+
manager.strategies.add(:email, Shieldify::Strategies::Email)
|
15
|
+
manager.strategies.add(:jwt, Shieldify::Strategies::Jwt)
|
16
|
+
|
17
|
+
manager.default_strategies :email, :jwt
|
18
|
+
|
19
|
+
manager.default_strategies :email
|
20
|
+
manager.scope_defaults :default, store: false
|
21
|
+
manager.failure_app = ->(env) { Shieldify::FailureApp.call(env) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
initializer 'shieldify.insert_middleware', after: :load_config_initializers do |app|
|
26
|
+
app.middleware.insert_after Warden::Manager, Shieldify::Middleware
|
27
|
+
end
|
28
|
+
|
29
|
+
initializer 'shieldify.require' do
|
30
|
+
require_relative '../../app/models/jwt_session'
|
31
|
+
require_relative '../../app/controllers/users/emails_controller'
|
32
|
+
end
|
33
|
+
|
34
|
+
initializer 'shieldify.active_record' do
|
35
|
+
ActiveSupport.on_load(:active_record) do
|
36
|
+
include Shieldify::ModelExtensions
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
initializer 'shieldify.action_mailer' do |app|
|
41
|
+
ActiveSupport.on_load(:action_mailer) do
|
42
|
+
include app.routes.url_helpers
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
initializer 'shieldify.include_controller_helpers' do
|
47
|
+
ActiveSupport.on_load(:action_controller_api) do
|
48
|
+
include Shieldify::Controllers::Helpers
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Shieldify
|
2
|
+
module Strategies
|
3
|
+
class Email < Warden::Strategies::Base
|
4
|
+
def valid?
|
5
|
+
json_body['email'].present? && json_body['password'].present?
|
6
|
+
end
|
7
|
+
|
8
|
+
def authenticate!
|
9
|
+
user = User.authenticate_by_email(email: json_body['email'], password: json_body['password'])
|
10
|
+
return fail!("Unauthorized: #{user.errors.full_messages.join(", ")}") unless user.errors.empty?
|
11
|
+
|
12
|
+
handle_user_authentication(user)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def json_body
|
18
|
+
request.body.rewind
|
19
|
+
JSON.parse(request.body.read) rescue {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def handle_user_authentication(user)
|
23
|
+
JwtService.encode(user.id) do |success, token, jti, error|
|
24
|
+
if success
|
25
|
+
process_jwt_token(user, token, jti)
|
26
|
+
else
|
27
|
+
fail!("JWT token generation failed: #{error}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_jwt_token(user, token, jti)
|
33
|
+
jwt_token = user.jwt_sessions.create(jti: jti)
|
34
|
+
if jwt_token.persisted?
|
35
|
+
set_jwt_to_header(token)
|
36
|
+
success!(user)
|
37
|
+
else
|
38
|
+
fail!("Failed to save JWT token: #{jwt_token.errors.full_messages.join(", ")}")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_jwt_to_header(token)
|
43
|
+
env['auth.jwt'] ||= {}
|
44
|
+
env['auth.jwt'] = token
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Shieldify
|
2
|
+
module Strategies
|
3
|
+
class Jwt < Warden::Strategies::Base
|
4
|
+
def valid?
|
5
|
+
request.env['HTTP_AUTHORIZATION'].present?
|
6
|
+
end
|
7
|
+
|
8
|
+
def authenticate!
|
9
|
+
token = extract_token_from_header
|
10
|
+
return fail!("Authorization token not provided") unless token
|
11
|
+
|
12
|
+
JwtService.decode(token) do |success, payload, error|
|
13
|
+
if success
|
14
|
+
authenticate_with_payload(payload)
|
15
|
+
else
|
16
|
+
fail!("Invalid token: #{error}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def extract_token_from_header
|
24
|
+
request.env['HTTP_AUTHORIZATION']&.split(' ')&.last
|
25
|
+
end
|
26
|
+
|
27
|
+
def authenticate_with_payload(payload)
|
28
|
+
user = find_user(payload['sub'])
|
29
|
+
return unless user
|
30
|
+
|
31
|
+
jwt_session = find_jwt_session(user, payload['jti'])
|
32
|
+
return unless jwt_session
|
33
|
+
|
34
|
+
if token_expired_halfway?(payload['exp'])
|
35
|
+
refresh_token(user, jwt_session)
|
36
|
+
else
|
37
|
+
success!(user)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_user(user_id)
|
42
|
+
user = User.find_by(id: user_id)
|
43
|
+
fail!("Invalid token: user not found") unless user
|
44
|
+
user
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_jwt_session(user, jti)
|
48
|
+
jwt_session = user.jwt_sessions.find_by(jti: jti)
|
49
|
+
fail!("Invalid token: session not found") unless jwt_session
|
50
|
+
jwt_session
|
51
|
+
end
|
52
|
+
|
53
|
+
def token_expired_halfway?(exp)
|
54
|
+
halfway_time = exp - (exp - Time.now.to_i) / 2
|
55
|
+
Time.now.to_i >= halfway_time
|
56
|
+
end
|
57
|
+
|
58
|
+
def refresh_token(user, jwt_session)
|
59
|
+
JwtService.encode(user.id) do |success, new_token, new_jti, error|
|
60
|
+
if success
|
61
|
+
jwt_session.update!(jti: new_jti)
|
62
|
+
set_jwt_to_header(new_token)
|
63
|
+
success!(user)
|
64
|
+
else
|
65
|
+
fail!("JWT token generation failed: #{error}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def set_jwt_to_header(token)
|
71
|
+
env['auth.jwt'] ||= {}
|
72
|
+
env['auth.jwt'] = token
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
data/lib/shieldify.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require "warden"
|
2
|
+
require "shieldify/failure_app"
|
3
|
+
require "shieldify/model_extensions"
|
4
|
+
require "shieldify/controllers/helpers"
|
5
|
+
require "shieldify/strategies/email"
|
6
|
+
require "shieldify/strategies/jwt"
|
7
|
+
require "shieldify/middleware/authentication"
|
8
|
+
require "shieldify/middleware"
|
9
|
+
require "shieldify/version"
|
10
|
+
require "shieldify/railtie"
|
11
|
+
|
12
|
+
module Shieldify
|
13
|
+
class Configuration
|
14
|
+
include Singleton
|
15
|
+
|
16
|
+
# Default mailer sender.
|
17
|
+
mattr_accessor :mailer_sender
|
18
|
+
@@mailer_sender = "shieldify@example.com"
|
19
|
+
|
20
|
+
# Default mailer sender.
|
21
|
+
mattr_accessor :reply_to
|
22
|
+
@@reply_to = "shieldify@example.com"
|
23
|
+
|
24
|
+
# Email regex used to validate email formats.
|
25
|
+
mattr_accessor :email_regexp
|
26
|
+
@@email_regexp = URI::MailTo::EMAIL_REGEXP
|
27
|
+
|
28
|
+
# Password complexity regex
|
29
|
+
# Explanation of the regular expression:
|
30
|
+
# - (?=.*\d) ensures there is at least one digit
|
31
|
+
# - (?=.*[a-z]) ensures there is at least one lowercase letter
|
32
|
+
# - (?=.*[A-Z]) ensures there is at least one uppercase letter
|
33
|
+
# - (?=.*[@$#!%*?&]) ensures there is at least one of these special characters
|
34
|
+
# - \S{8,} ensures the password is at least 8 characters in length and contains no spaces
|
35
|
+
mattr_accessor :password_complexity
|
36
|
+
@@password_complexity = /\A(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@$#!%*?&])\S{6,}\z/
|
37
|
+
|
38
|
+
# Used to send notification to the original user email when their email is changed.
|
39
|
+
mattr_accessor :send_email_changed_notification
|
40
|
+
@@send_email_changed_notification = true
|
41
|
+
|
42
|
+
# Used to enable sending notification to user when their password is changed.
|
43
|
+
mattr_accessor :send_password_changed_notification
|
44
|
+
@@send_password_changed_notification = true
|
45
|
+
|
46
|
+
# Number of authentication tries before locking an account.
|
47
|
+
mattr_accessor :maximum_attempts
|
48
|
+
@@maximum_attempts = 20
|
49
|
+
|
50
|
+
# The parent mailer for internal mailers.
|
51
|
+
mattr_accessor :parent_mailer
|
52
|
+
@@parent_mailer = "ActionMailer::Base"
|
53
|
+
|
54
|
+
# JWT related
|
55
|
+
mattr_accessor :jwt_secret
|
56
|
+
@@jwt_secret = "whatever"
|
57
|
+
|
58
|
+
mattr_accessor :jwt_issuer
|
59
|
+
@@jwt_issuer = "Shieldify"
|
60
|
+
|
61
|
+
mattr_accessor :jwt_exp
|
62
|
+
@@jwt_exp = 24.hours.from_now.to_i
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.setup
|
66
|
+
yield Configuration.instance
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
require "shieldify/models/email_authenticatable"
|
71
|
+
require "shieldify/models/email_authenticatable/registerable"
|
72
|
+
require "shieldify/models/email_authenticatable/confirmable"
|
73
|
+
require "shieldify/jwt_service"
|
74
|
+
require "shieldify/mailer"
|