sorcery 0.11.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/ISSUE_TEMPLATE.md +20 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +5 -0
- data/.github/workflows/ruby.yml +23 -0
- data/.rubocop.yml +55 -0
- data/.rubocop_todo.yml +155 -0
- data/.travis.yml +11 -51
- data/CHANGELOG.md +75 -0
- data/CODE_OF_CONDUCT.md +14 -0
- data/Gemfile +2 -2
- data/{LICENSE.txt → LICENSE.md} +1 -1
- data/README.md +34 -7
- data/SECURITY.md +18 -0
- data/gemfiles/rails_52.gemfile +7 -0
- data/gemfiles/rails_60.gemfile +7 -0
- data/lib/generators/sorcery/USAGE +1 -1
- data/lib/generators/sorcery/helpers.rb +4 -0
- data/lib/generators/sorcery/install_generator.rb +21 -21
- data/lib/generators/sorcery/templates/initializer.rb +176 -69
- data/lib/generators/sorcery/templates/migration/activity_logging.rb +5 -5
- data/lib/generators/sorcery/templates/migration/brute_force_protection.rb +4 -4
- data/lib/generators/sorcery/templates/migration/core.rb +4 -4
- data/lib/generators/sorcery/templates/migration/external.rb +3 -3
- data/lib/generators/sorcery/templates/migration/magic_login.rb +9 -0
- data/lib/generators/sorcery/templates/migration/remember_me.rb +3 -3
- data/lib/generators/sorcery/templates/migration/reset_password.rb +5 -4
- data/lib/generators/sorcery/templates/migration/user_activation.rb +4 -4
- data/lib/sorcery.rb +2 -0
- data/lib/sorcery/adapters/active_record_adapter.rb +4 -3
- data/lib/sorcery/adapters/mongoid_adapter.rb +23 -11
- data/lib/sorcery/controller.rb +26 -15
- data/lib/sorcery/controller/config.rb +7 -5
- data/lib/sorcery/controller/submodules/activity_logging.rb +9 -3
- data/lib/sorcery/controller/submodules/external.rb +52 -33
- data/lib/sorcery/controller/submodules/http_basic_auth.rb +2 -0
- data/lib/sorcery/controller/submodules/remember_me.rb +3 -8
- data/lib/sorcery/controller/submodules/session_timeout.rb +28 -5
- data/lib/sorcery/crypto_providers/aes256.rb +2 -1
- data/lib/sorcery/crypto_providers/bcrypt.rb +8 -2
- data/lib/sorcery/engine.rb +16 -3
- data/lib/sorcery/model.rb +14 -10
- data/lib/sorcery/model/config.rb +12 -4
- data/lib/sorcery/model/submodules/brute_force_protection.rb +6 -7
- data/lib/sorcery/model/submodules/external.rb +19 -3
- data/lib/sorcery/model/submodules/magic_login.rb +130 -0
- data/lib/sorcery/model/submodules/reset_password.rb +25 -2
- data/lib/sorcery/model/submodules/user_activation.rb +1 -1
- data/lib/sorcery/model/temporary_token.rb +3 -1
- data/lib/sorcery/protocols/oauth.rb +1 -0
- data/lib/sorcery/providers/auth0.rb +46 -0
- data/lib/sorcery/providers/battlenet.rb +51 -0
- data/lib/sorcery/providers/discord.rb +52 -0
- data/lib/sorcery/providers/heroku.rb +1 -0
- data/lib/sorcery/providers/instagram.rb +73 -0
- data/lib/sorcery/providers/line.rb +63 -0
- data/lib/sorcery/providers/linkedin.rb +45 -36
- data/lib/sorcery/providers/vk.rb +5 -4
- data/lib/sorcery/providers/wechat.rb +8 -6
- data/lib/sorcery/test_helpers/internal.rb +5 -4
- data/lib/sorcery/test_helpers/internal/rails.rb +11 -11
- data/lib/sorcery/test_helpers/rails/request.rb +20 -0
- data/lib/sorcery/version.rb +1 -1
- data/sorcery.gemspec +26 -10
- data/spec/active_record/user_activation_spec.rb +2 -2
- data/spec/active_record/user_activity_logging_spec.rb +2 -2
- data/spec/active_record/user_brute_force_protection_spec.rb +2 -2
- data/spec/active_record/user_magic_login_spec.rb +15 -0
- data/spec/active_record/user_oauth_spec.rb +2 -2
- data/spec/active_record/user_remember_me_spec.rb +2 -2
- data/spec/active_record/user_reset_password_spec.rb +2 -2
- data/spec/active_record/user_spec.rb +0 -10
- data/spec/controllers/controller_http_basic_auth_spec.rb +1 -1
- data/spec/controllers/controller_oauth2_spec.rb +230 -123
- data/spec/controllers/controller_oauth_spec.rb +13 -7
- data/spec/controllers/controller_remember_me_spec.rb +16 -8
- data/spec/controllers/controller_session_timeout_spec.rb +90 -3
- data/spec/controllers/controller_spec.rb +13 -3
- data/spec/orm/active_record.rb +2 -2
- data/spec/providers/example_provider_spec.rb +17 -0
- data/spec/providers/example_spec.rb +17 -0
- data/spec/providers/vk_spec.rb +42 -0
- data/spec/rails_app/app/assets/config/manifest.js +1 -0
- data/spec/rails_app/app/controllers/application_controller.rb +2 -0
- data/spec/rails_app/app/controllers/sorcery_controller.rb +152 -33
- data/spec/rails_app/app/mailers/sorcery_mailer.rb +7 -0
- data/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb +13 -0
- data/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb +6 -0
- data/spec/rails_app/config/application.rb +8 -3
- data/spec/rails_app/config/boot.rb +1 -1
- data/spec/rails_app/config/environment.rb +1 -1
- data/spec/rails_app/config/routes.rb +17 -0
- data/spec/rails_app/config/secrets.yml +4 -0
- data/spec/rails_app/db/migrate/activity_logging/20101224223624_add_activity_logging_to_users.rb +2 -2
- data/spec/rails_app/db/migrate/invalidate_active_sessions/20180221093235_add_invalidate_active_sessions_before_to_users.rb +9 -0
- data/spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb +17 -0
- data/spec/rails_app/db/migrate/reset_password/20101224223622_add_reset_password_to_users.rb +2 -0
- data/spec/rails_app/db/schema.rb +7 -9
- data/spec/shared_examples/user_magic_login_shared_examples.rb +150 -0
- data/spec/shared_examples/user_oauth_shared_examples.rb +1 -1
- data/spec/shared_examples/user_remember_me_shared_examples.rb +1 -1
- data/spec/shared_examples/user_reset_password_shared_examples.rb +37 -5
- data/spec/shared_examples/user_shared_examples.rb +104 -43
- data/spec/sorcery_crypto_providers_spec.rb +61 -1
- data/spec/sorcery_temporary_token_spec.rb +27 -0
- data/spec/spec.opts +1 -1
- data/spec/spec_helper.rb +2 -2
- data/spec/support/migration_helper.rb +29 -0
- data/spec/support/providers/example.rb +11 -0
- data/spec/support/providers/example_provider.rb +11 -0
- metadata +92 -29
- data/gemfiles/active_record-rails40.gemfile +0 -7
- data/gemfiles/active_record-rails41.gemfile +0 -7
- data/gemfiles/active_record-rails42.gemfile +0 -7
- data/spec/rails_app/config/initializers/secret_token.rb +0 -7
@@ -19,6 +19,7 @@ module Sorcery
|
|
19
19
|
end
|
20
20
|
merge_http_basic_auth_defaults!
|
21
21
|
end
|
22
|
+
|
22
23
|
Config.login_sources << :login_from_basic_auth
|
23
24
|
end
|
24
25
|
|
@@ -57,6 +58,7 @@ module Sorcery
|
|
57
58
|
while current_controller != ActionController::Base
|
58
59
|
result = Config.controller_to_realm_map[current_controller.controller_name]
|
59
60
|
return result if result
|
61
|
+
|
60
62
|
current_controller = current_controller.superclass
|
61
63
|
end
|
62
64
|
nil
|
@@ -17,8 +17,8 @@ module Sorcery
|
|
17
17
|
end
|
18
18
|
merge_remember_me_defaults!
|
19
19
|
end
|
20
|
+
|
20
21
|
Config.login_sources << :login_from_cookie
|
21
|
-
Config.after_login << :remember_me_if_asked_to
|
22
22
|
Config.before_logout << :forget_me!
|
23
23
|
end
|
24
24
|
|
@@ -51,20 +51,15 @@ module Sorcery
|
|
51
51
|
|
52
52
|
protected
|
53
53
|
|
54
|
-
# calls remember_me! if a third credential was passed to the login method.
|
55
|
-
# Runs as a hook after login.
|
56
|
-
def remember_me_if_asked_to(_user, credentials)
|
57
|
-
remember_me! if credentials.size == 3 && credentials[2] && credentials[2] != '0'
|
58
|
-
end
|
59
|
-
|
60
54
|
# Checks the cookie for a remember me token, tried to find a user with that token
|
61
55
|
# and logs the user in if found.
|
62
56
|
# Runs as a login source. See 'current_user' method for how it is used.
|
63
57
|
def login_from_cookie
|
64
|
-
user = cookies.signed[:remember_me_token] && user_class.sorcery_adapter.find_by_remember_me_token(cookies.signed[:remember_me_token])
|
58
|
+
user = cookies.signed[:remember_me_token] && user_class.sorcery_adapter.find_by_remember_me_token(cookies.signed[:remember_me_token]) if defined? cookies
|
65
59
|
if user && user.has_remember_me_token?
|
66
60
|
set_remember_me_cookie!(user)
|
67
61
|
session[:user_id] = user.id.to_s
|
62
|
+
after_remember_me!(user)
|
68
63
|
@current_user = user
|
69
64
|
else
|
70
65
|
@current_user = false
|
@@ -12,24 +12,38 @@ module Sorcery
|
|
12
12
|
attr_accessor :session_timeout
|
13
13
|
# use the last action as the beginning of session timeout.
|
14
14
|
attr_accessor :session_timeout_from_last_action
|
15
|
+
# allow users to invalidate active sessions
|
16
|
+
attr_accessor :session_timeout_invalidate_active_sessions_enabled
|
15
17
|
|
16
18
|
def merge_session_timeout_defaults!
|
17
|
-
@defaults.merge!(:@session_timeout
|
18
|
-
:@session_timeout_from_last_action
|
19
|
+
@defaults.merge!(:@session_timeout => 3600, # 1.hour
|
20
|
+
:@session_timeout_from_last_action => false,
|
21
|
+
:@session_timeout_invalidate_active_sessions_enabled => false)
|
19
22
|
end
|
20
23
|
end
|
21
24
|
merge_session_timeout_defaults!
|
22
25
|
end
|
26
|
+
|
23
27
|
Config.after_login << :register_login_time
|
28
|
+
Config.after_remember_me << :register_login_time
|
29
|
+
|
24
30
|
base.prepend_before_action :validate_session
|
25
31
|
end
|
26
32
|
|
27
33
|
module InstanceMethods
|
34
|
+
def invalidate_active_sessions!
|
35
|
+
return unless Config.session_timeout_invalidate_active_sessions_enabled
|
36
|
+
return unless current_user.present?
|
37
|
+
|
38
|
+
current_user.send(:invalidate_sessions_before=, Time.now.in_time_zone)
|
39
|
+
current_user.save
|
40
|
+
end
|
41
|
+
|
28
42
|
protected
|
29
43
|
|
30
44
|
# Registers last login to be used as the timeout starting point.
|
31
45
|
# Runs as a hook after a successful login.
|
32
|
-
def register_login_time(_user, _credentials)
|
46
|
+
def register_login_time(_user, _credentials = nil)
|
33
47
|
session[:login_time] = session[:last_action_time] = Time.now.in_time_zone
|
34
48
|
end
|
35
49
|
|
@@ -37,9 +51,9 @@ module Sorcery
|
|
37
51
|
# To be used as a before_action, before require_login
|
38
52
|
def validate_session
|
39
53
|
session_to_use = Config.session_timeout_from_last_action ? session[:last_action_time] : session[:login_time]
|
40
|
-
if session_to_use && sorcery_session_expired?(session_to_use.to_time)
|
54
|
+
if (session_to_use && sorcery_session_expired?(session_to_use.to_time)) || sorcery_session_invalidated?
|
41
55
|
reset_sorcery_session
|
42
|
-
|
56
|
+
remove_instance_variable :@current_user if defined? @current_user
|
43
57
|
else
|
44
58
|
session[:last_action_time] = Time.now.in_time_zone
|
45
59
|
end
|
@@ -48,6 +62,15 @@ module Sorcery
|
|
48
62
|
def sorcery_session_expired?(time)
|
49
63
|
Time.now.in_time_zone - time > Config.session_timeout
|
50
64
|
end
|
65
|
+
|
66
|
+
# Use login time if present, otherwise use last action time.
|
67
|
+
def sorcery_session_invalidated?
|
68
|
+
return false unless Config.session_timeout_invalidate_active_sessions_enabled
|
69
|
+
return false unless current_user.present? && current_user.try(:invalidate_sessions_before).present?
|
70
|
+
|
71
|
+
time = session[:login_time] || session[:last_action_time] || Time.now.in_time_zone
|
72
|
+
time < current_user.invalidate_sessions_before
|
73
|
+
end
|
51
74
|
end
|
52
75
|
end
|
53
76
|
end
|
@@ -29,7 +29,7 @@ module Sorcery
|
|
29
29
|
|
30
30
|
def matches?(crypted, *tokens)
|
31
31
|
decrypt(crypted) == tokens.join
|
32
|
-
rescue OpenSSL::CipherError
|
32
|
+
rescue OpenSSL::Cipher::CipherError
|
33
33
|
false
|
34
34
|
end
|
35
35
|
|
@@ -43,6 +43,7 @@ module Sorcery
|
|
43
43
|
|
44
44
|
def aes
|
45
45
|
raise ArgumentError, "#{name} expects a 32 bytes long key. Please use Sorcery::Model::Config.encryption_key to set it." if @key.nil? || @key == ''
|
46
|
+
|
46
47
|
@aes ||= OpenSSL::Cipher.new('AES-256-ECB')
|
47
48
|
end
|
48
49
|
end
|
@@ -40,6 +40,10 @@ module Sorcery
|
|
40
40
|
# You are good to go!
|
41
41
|
class BCrypt
|
42
42
|
class << self
|
43
|
+
# Setting the option :pepper allows users to append an app-specific secret token.
|
44
|
+
# Basically it's equivalent to :salt_join_token option, but have a different name to ensure
|
45
|
+
# backward compatibility in generating/matching passwords.
|
46
|
+
attr_accessor :pepper
|
43
47
|
# This is the :cost option for the BCrpyt library.
|
44
48
|
# The higher the cost the more secure it is and the longer is take the generate a hash. By default this is 10.
|
45
49
|
# Set this to whatever you want, play around with it to get that perfect balance between
|
@@ -60,6 +64,7 @@ module Sorcery
|
|
60
64
|
def matches?(hash, *tokens)
|
61
65
|
hash = new_from_hash(hash)
|
62
66
|
return false if hash.nil? || hash == {}
|
67
|
+
|
63
68
|
hash == join_tokens(tokens)
|
64
69
|
end
|
65
70
|
|
@@ -76,18 +81,19 @@ module Sorcery
|
|
76
81
|
|
77
82
|
def reset!
|
78
83
|
@cost = 10
|
84
|
+
@pepper = ''
|
79
85
|
end
|
80
86
|
|
81
87
|
private
|
82
88
|
|
83
89
|
def join_tokens(tokens)
|
84
|
-
tokens.flatten.join
|
90
|
+
tokens.flatten.join.concat(pepper.to_s) # make sure to add pepper in case tokens have only one element
|
85
91
|
end
|
86
92
|
|
87
93
|
def new_from_hash(hash)
|
88
94
|
::BCrypt::Password.new(hash)
|
89
95
|
rescue ::BCrypt::Errors::InvalidHash
|
90
|
-
|
96
|
+
nil
|
91
97
|
end
|
92
98
|
end
|
93
99
|
end
|
data/lib/sorcery/engine.rb
CHANGED
@@ -7,10 +7,23 @@ module Sorcery
|
|
7
7
|
class Engine < Rails::Engine
|
8
8
|
config.sorcery = ::Sorcery::Controller::Config
|
9
9
|
|
10
|
+
# TODO: Should this include a modified version of the helper methods?
|
10
11
|
initializer 'extend Controller with sorcery' do
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
# FIXME: on_load is needed to fix Rails 6 deprecations, but it breaks
|
13
|
+
# applications due to undefined method errors.
|
14
|
+
# ActiveSupport.on_load(:action_controller_api) do
|
15
|
+
if defined?(ActionController::API)
|
16
|
+
ActionController::API.send(:include, Sorcery::Controller)
|
17
|
+
end
|
18
|
+
|
19
|
+
# FIXME: on_load is needed to fix Rails 6 deprecations, but it breaks
|
20
|
+
# applications due to undefined method errors.
|
21
|
+
# ActiveSupport.on_load(:action_controller_base) do
|
22
|
+
if defined?(ActionController::Base)
|
23
|
+
ActionController::Base.send(:include, Sorcery::Controller)
|
24
|
+
ActionController::Base.helper_method :current_user
|
25
|
+
ActionController::Base.helper_method :logged_in?
|
26
|
+
end
|
14
27
|
end
|
15
28
|
end
|
16
29
|
end
|
data/lib/sorcery/model.rb
CHANGED
@@ -47,12 +47,15 @@ module Sorcery
|
|
47
47
|
class_eval do
|
48
48
|
@sorcery_config.submodules = ::Sorcery::Controller::Config.submodules
|
49
49
|
@sorcery_config.submodules.each do |mod|
|
50
|
+
# TODO: Is there a cleaner way to handle missing submodules?
|
51
|
+
# rubocop:disable Lint/HandleExceptions
|
50
52
|
begin
|
51
53
|
include Submodules.const_get(mod.to_s.split('_').map(&:capitalize).join)
|
52
54
|
rescue NameError
|
53
55
|
# don't stop on a missing submodule. Needed because some submodules are only defined
|
54
56
|
# in the controller side.
|
55
57
|
end
|
58
|
+
# rubocop:enable Lint/HandleExceptions
|
56
59
|
end
|
57
60
|
end
|
58
61
|
end
|
@@ -99,10 +102,6 @@ module Sorcery
|
|
99
102
|
|
100
103
|
set_encryption_attributes
|
101
104
|
|
102
|
-
unless user.valid_password?(credentials[1])
|
103
|
-
return authentication_response(user: user, failure: :invalid_password, &block)
|
104
|
-
end
|
105
|
-
|
106
105
|
if user.respond_to?(:active_for_authentication?) && !user.active_for_authentication?
|
107
106
|
return authentication_response(user: user, failure: :inactive, &block)
|
108
107
|
end
|
@@ -115,6 +114,10 @@ module Sorcery
|
|
115
114
|
end
|
116
115
|
end
|
117
116
|
|
117
|
+
unless user.valid_password?(credentials[1])
|
118
|
+
return authentication_response(user: user, failure: :invalid_password, &block)
|
119
|
+
end
|
120
|
+
|
118
121
|
authentication_response(user: user, return_value: user, &block)
|
119
122
|
end
|
120
123
|
|
@@ -139,6 +142,7 @@ module Sorcery
|
|
139
142
|
def set_encryption_attributes
|
140
143
|
@sorcery_config.encryption_provider.stretches = @sorcery_config.stretches if @sorcery_config.encryption_provider.respond_to?(:stretches) && @sorcery_config.stretches
|
141
144
|
@sorcery_config.encryption_provider.join_token = @sorcery_config.salt_join_token if @sorcery_config.encryption_provider.respond_to?(:join_token) && @sorcery_config.salt_join_token
|
145
|
+
@sorcery_config.encryption_provider.pepper = @sorcery_config.pepper if @sorcery_config.encryption_provider.respond_to?(:pepper) && @sorcery_config.pepper
|
142
146
|
end
|
143
147
|
|
144
148
|
def add_config_inheritance
|
@@ -192,9 +196,9 @@ module Sorcery
|
|
192
196
|
config = sorcery_config
|
193
197
|
send(:"#{config.password_attribute_name}=", nil)
|
194
198
|
|
195
|
-
|
196
|
-
|
197
|
-
|
199
|
+
return unless respond_to?(:"#{config.password_attribute_name}_confirmation=")
|
200
|
+
|
201
|
+
send(:"#{config.password_attribute_name}_confirmation=", nil)
|
198
202
|
end
|
199
203
|
|
200
204
|
# calls the requested email method on the configured mailer
|
@@ -202,9 +206,9 @@ module Sorcery
|
|
202
206
|
def generic_send_email(method, mailer)
|
203
207
|
config = sorcery_config
|
204
208
|
mail = config.send(mailer).send(config.send(method), self)
|
205
|
-
|
206
|
-
|
207
|
-
|
209
|
+
return unless mail.respond_to?(config.email_delivery_method)
|
210
|
+
|
211
|
+
mail.send(config.email_delivery_method)
|
208
212
|
end
|
209
213
|
end
|
210
214
|
end
|
data/lib/sorcery/model/config.rb
CHANGED
@@ -4,8 +4,6 @@
|
|
4
4
|
module Sorcery
|
5
5
|
module Model
|
6
6
|
class Config
|
7
|
-
# change default username attribute, for example, to use :email as the login.
|
8
|
-
attr_accessor :username_attribute_names
|
9
7
|
# change *virtual* password attribute, the one which is used until an encrypted one is generated.
|
10
8
|
attr_accessor :password_attribute_name
|
11
9
|
# change default email attribute.
|
@@ -14,7 +12,11 @@ module Sorcery
|
|
14
12
|
attr_accessor :downcase_username_before_authenticating
|
15
13
|
# change default crypted_password attribute.
|
16
14
|
attr_accessor :crypted_password_attribute_name
|
15
|
+
# application-specific secret token that is joined with the password and its salt.
|
16
|
+
# Currently available with BCrypt (default crypt provider) only.
|
17
|
+
attr_accessor :pepper
|
17
18
|
# what pattern to use to join the password with the salt
|
19
|
+
# APPLICABLE TO MD5, SHA1, SHA256, SHA512. Other crypt providers (incl. BCrypt) ignore this parameter.
|
18
20
|
attr_accessor :salt_join_token
|
19
21
|
# change default salt attribute.
|
20
22
|
attr_accessor :salt_attribute_name
|
@@ -35,7 +37,11 @@ module Sorcery
|
|
35
37
|
attr_accessor :email_delivery_method
|
36
38
|
# an array of method names to call after configuration by user. used internally.
|
37
39
|
attr_accessor :after_config
|
40
|
+
# Set token randomness
|
41
|
+
attr_accessor :token_randomness
|
38
42
|
|
43
|
+
# change default username attribute, for example, to use :email as the login. See 'username_attribute_names=' below.
|
44
|
+
attr_reader :username_attribute_names
|
39
45
|
# change default encryption_provider.
|
40
46
|
attr_reader :encryption_provider
|
41
47
|
# use an external encryption class.
|
@@ -55,13 +61,15 @@ module Sorcery
|
|
55
61
|
:@encryption_provider => CryptoProviders::BCrypt,
|
56
62
|
:@custom_encryption_provider => nil,
|
57
63
|
:@encryption_key => nil,
|
64
|
+
:@pepper => '',
|
58
65
|
:@salt_join_token => '',
|
59
66
|
:@salt_attribute_name => :salt,
|
60
67
|
:@stretches => nil,
|
61
68
|
:@subclasses_inherit_config => false,
|
62
69
|
:@before_authenticate => [],
|
63
70
|
:@after_config => [],
|
64
|
-
:@email_delivery_method => default_email_delivery_method
|
71
|
+
:@email_delivery_method => default_email_delivery_method,
|
72
|
+
:@token_randomness => 15
|
65
73
|
}
|
66
74
|
reset!
|
67
75
|
end
|
@@ -93,7 +101,7 @@ module Sorcery
|
|
93
101
|
when :bcrypt then CryptoProviders::BCrypt
|
94
102
|
when :custom then @custom_encryption_provider
|
95
103
|
else raise ArgumentError, "Encryption algorithm supplied, #{algo}, is invalid"
|
96
|
-
|
104
|
+
end
|
97
105
|
end
|
98
106
|
|
99
107
|
private
|
@@ -14,7 +14,6 @@ module Sorcery
|
|
14
14
|
:consecutive_login_retries_amount_limit, # how many failed logins allowed.
|
15
15
|
:login_lock_time_period, # how long the user should be banned.
|
16
16
|
# in seconds. 0 for permanent.
|
17
|
-
|
18
17
|
:unlock_token_attribute_name, # Unlock token attribute name
|
19
18
|
:unlock_token_email_method_name, # Mailer method name
|
20
19
|
:unlock_token_mailer_disabled, # When true, dont send unlock token via email
|
@@ -70,9 +69,9 @@ module Sorcery
|
|
70
69
|
|
71
70
|
sorcery_adapter.increment(config.failed_logins_count_attribute_name)
|
72
71
|
|
73
|
-
|
74
|
-
|
75
|
-
|
72
|
+
return unless send(config.failed_logins_count_attribute_name) >= config.consecutive_login_retries_amount_limit
|
73
|
+
|
74
|
+
login_lock!
|
76
75
|
end
|
77
76
|
|
78
77
|
# /!\
|
@@ -98,9 +97,9 @@ module Sorcery
|
|
98
97
|
config.unlock_token_attribute_name => TemporaryToken.generate_random_token }
|
99
98
|
sorcery_adapter.update_attributes(attributes)
|
100
99
|
|
101
|
-
|
102
|
-
|
103
|
-
|
100
|
+
return if config.unlock_token_mailer_disabled || config.unlock_token_mailer.nil?
|
101
|
+
|
102
|
+
send_unlock_token_email!
|
104
103
|
end
|
105
104
|
|
106
105
|
def login_unlocked?
|
@@ -40,12 +40,13 @@ module Sorcery
|
|
40
40
|
def load_from_provider(provider, uid)
|
41
41
|
config = sorcery_config
|
42
42
|
authentication = config.authentications_class.sorcery_adapter.find_by_oauth_credentials(provider, uid)
|
43
|
-
|
43
|
+
# Return user if matching authentication found
|
44
|
+
sorcery_adapter.find_by_id(authentication.send(config.authentications_user_id_attribute_name)) if authentication
|
44
45
|
end
|
45
46
|
|
46
47
|
def create_and_validate_from_provider(provider, uid, attrs)
|
47
48
|
user = new(attrs)
|
48
|
-
user.send(sorcery_config.authentications_class.
|
49
|
+
user.send(sorcery_config.authentications_class.name.demodulize.underscore.pluralize).build(
|
49
50
|
sorcery_config.provider_uid_attribute_name => uid,
|
50
51
|
sorcery_config.provider_attribute_name => provider
|
51
52
|
)
|
@@ -73,11 +74,26 @@ module Sorcery
|
|
73
74
|
end
|
74
75
|
user
|
75
76
|
end
|
77
|
+
|
78
|
+
# NOTE: Should this build the authentication as well and return [user, auth]?
|
79
|
+
# Currently, users call this function for the user and call add_provider_to_user after saving
|
80
|
+
def build_from_provider(attrs)
|
81
|
+
user = new
|
82
|
+
attrs.each do |k, v|
|
83
|
+
user.send(:"#{k}=", v)
|
84
|
+
end
|
85
|
+
|
86
|
+
if block_given?
|
87
|
+
return false unless yield user
|
88
|
+
end
|
89
|
+
|
90
|
+
user
|
91
|
+
end
|
76
92
|
end
|
77
93
|
|
78
94
|
module InstanceMethods
|
79
95
|
def add_provider_to_user(provider, uid)
|
80
|
-
authentications = sorcery_config.authentications_class.name.underscore.pluralize
|
96
|
+
authentications = sorcery_config.authentications_class.name.demodulize.underscore.pluralize
|
81
97
|
# first check to see if user has a particular authentication already
|
82
98
|
if sorcery_adapter.find_authentication_by_oauth_credentials(authentications, provider, uid).nil?
|
83
99
|
user = send(authentications).build(sorcery_config.provider_uid_attribute_name => uid,
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Sorcery
|
2
|
+
module Model
|
3
|
+
module Submodules
|
4
|
+
# This submodule adds the ability to login via email without password.
|
5
|
+
# When the user requests an email is sent to him with a url.
|
6
|
+
# The url includes a token, which is also saved with the user's record in the db.
|
7
|
+
# The token has configurable expiration.
|
8
|
+
# When the user clicks the url in the email, providing the token has not yet expired,
|
9
|
+
# he will be able to login.
|
10
|
+
#
|
11
|
+
# When using this submodule, supplying a mailer is mandatory.
|
12
|
+
module MagicLogin
|
13
|
+
def self.included(base)
|
14
|
+
base.sorcery_config.class_eval do
|
15
|
+
attr_accessor :magic_login_token_attribute_name, # magic login code attribute name.
|
16
|
+
:magic_login_token_expires_at_attribute_name, # expires at attribute name.
|
17
|
+
:magic_login_email_sent_at_attribute_name, # when was email sent, used for hammering
|
18
|
+
# protection.
|
19
|
+
:magic_login_mailer_class, # mailer class. Needed.
|
20
|
+
:magic_login_mailer_disabled, # when true sorcery will not automatically
|
21
|
+
# email magic login details and allow you to
|
22
|
+
# manually handle how and when email is sent
|
23
|
+
:magic_login_email_method_name, # magic login email method on your
|
24
|
+
# mailer class.
|
25
|
+
:magic_login_expiration_period, # how many seconds before the request
|
26
|
+
# expires. nil for never expires.
|
27
|
+
:magic_login_time_between_emails # hammering protection, how long to wait
|
28
|
+
# before allowing another email to be sent.
|
29
|
+
end
|
30
|
+
|
31
|
+
base.sorcery_config.instance_eval do
|
32
|
+
@defaults.merge!(:@magic_login_token_attribute_name => :magic_login_token,
|
33
|
+
:@magic_login_token_expires_at_attribute_name => :magic_login_token_expires_at,
|
34
|
+
:@magic_login_email_sent_at_attribute_name => :magic_login_email_sent_at,
|
35
|
+
:@magic_login_mailer_class => nil,
|
36
|
+
:@magic_login_mailer_disabled => true,
|
37
|
+
:@magic_login_email_method_name => :magic_login_email,
|
38
|
+
:@magic_login_expiration_period => 15 * 60,
|
39
|
+
:@magic_login_time_between_emails => 5 * 60)
|
40
|
+
|
41
|
+
reset!
|
42
|
+
end
|
43
|
+
|
44
|
+
base.extend(ClassMethods)
|
45
|
+
|
46
|
+
base.sorcery_config.after_config << :validate_mailer_defined
|
47
|
+
base.sorcery_config.after_config << :define_magic_login_fields
|
48
|
+
|
49
|
+
base.send(:include, InstanceMethods)
|
50
|
+
end
|
51
|
+
|
52
|
+
module ClassMethods
|
53
|
+
# Find user by token, also checks for expiration.
|
54
|
+
# Returns the user if token found and is valid.
|
55
|
+
def load_from_magic_login_token(token, &block)
|
56
|
+
load_from_token(
|
57
|
+
token,
|
58
|
+
@sorcery_config.magic_login_token_attribute_name,
|
59
|
+
@sorcery_config.magic_login_token_expires_at_attribute_name,
|
60
|
+
&block
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
# This submodule requires the developer to define his own mailer class to be used by it
|
67
|
+
# when magic_login_mailer_disabled is false
|
68
|
+
def validate_mailer_defined
|
69
|
+
msg = 'To use magic_login submodule, you must define a mailer (config.magic_login_mailer_class = YourMailerClass).'
|
70
|
+
raise ArgumentError, msg if @sorcery_config.magic_login_mailer_class.nil? && @sorcery_config.magic_login_mailer_disabled == false
|
71
|
+
end
|
72
|
+
|
73
|
+
def define_magic_login_fields
|
74
|
+
sorcery_adapter.define_field sorcery_config.magic_login_token_attribute_name, String
|
75
|
+
sorcery_adapter.define_field sorcery_config.magic_login_token_expires_at_attribute_name, Time
|
76
|
+
sorcery_adapter.define_field sorcery_config.magic_login_email_sent_at_attribute_name, Time
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module InstanceMethods
|
81
|
+
# generates a reset code with expiration
|
82
|
+
def generate_magic_login_token!
|
83
|
+
config = sorcery_config
|
84
|
+
attributes = {
|
85
|
+
config.magic_login_token_attribute_name => TemporaryToken.generate_random_token,
|
86
|
+
config.magic_login_email_sent_at_attribute_name => Time.now.in_time_zone
|
87
|
+
}
|
88
|
+
attributes[config.magic_login_token_expires_at_attribute_name] = Time.now.in_time_zone + config.magic_login_expiration_period if config.magic_login_expiration_period
|
89
|
+
|
90
|
+
sorcery_adapter.update_attributes(attributes)
|
91
|
+
end
|
92
|
+
|
93
|
+
# generates a magic login code with expiration and sends an email to the user.
|
94
|
+
def deliver_magic_login_instructions!
|
95
|
+
mail = false
|
96
|
+
config = sorcery_config
|
97
|
+
# hammering protection
|
98
|
+
return false if !config.magic_login_time_between_emails.nil? &&
|
99
|
+
send(config.magic_login_email_sent_at_attribute_name) &&
|
100
|
+
send(config.magic_login_email_sent_at_attribute_name) > config.magic_login_time_between_emails.seconds.ago
|
101
|
+
|
102
|
+
self.class.sorcery_adapter.transaction do
|
103
|
+
generate_magic_login_token!
|
104
|
+
unless config.magic_login_mailer_disabled
|
105
|
+
send_magic_login_email!
|
106
|
+
mail = true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
mail
|
110
|
+
end
|
111
|
+
|
112
|
+
# Clears the token.
|
113
|
+
def clear_magic_login_token!
|
114
|
+
config = sorcery_config
|
115
|
+
sorcery_adapter.update_attributes(
|
116
|
+
config.magic_login_token_attribute_name => nil,
|
117
|
+
config.magic_login_token_expires_at_attribute_name => nil
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
protected
|
122
|
+
|
123
|
+
def send_magic_login_email!
|
124
|
+
generic_send_email(:magic_login_email_method_name, :magic_login_mailer_class)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|