sorcery 0.11.0 → 0.15.1

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.
Files changed (106) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE.md +20 -0
  3. data/.rubocop.yml +55 -0
  4. data/.rubocop_todo.yml +145 -0
  5. data/.travis.yml +3 -52
  6. data/CHANGELOG.md +69 -0
  7. data/Gemfile +3 -3
  8. data/{LICENSE.txt → LICENSE.md} +1 -1
  9. data/README.md +34 -7
  10. data/lib/generators/sorcery/USAGE +1 -1
  11. data/lib/generators/sorcery/install_generator.rb +21 -21
  12. data/lib/generators/sorcery/templates/initializer.rb +164 -69
  13. data/lib/generators/sorcery/templates/migration/activity_logging.rb +4 -4
  14. data/lib/generators/sorcery/templates/migration/brute_force_protection.rb +3 -3
  15. data/lib/generators/sorcery/templates/migration/core.rb +2 -2
  16. data/lib/generators/sorcery/templates/migration/external.rb +3 -3
  17. data/lib/generators/sorcery/templates/migration/magic_login.rb +9 -0
  18. data/lib/generators/sorcery/templates/migration/remember_me.rb +2 -2
  19. data/lib/generators/sorcery/templates/migration/reset_password.rb +4 -3
  20. data/lib/generators/sorcery/templates/migration/user_activation.rb +3 -3
  21. data/lib/sorcery.rb +2 -0
  22. data/lib/sorcery/adapters/active_record_adapter.rb +3 -2
  23. data/lib/sorcery/adapters/mongoid_adapter.rb +23 -11
  24. data/lib/sorcery/controller.rb +26 -15
  25. data/lib/sorcery/controller/config.rb +2 -0
  26. data/lib/sorcery/controller/submodules/activity_logging.rb +14 -3
  27. data/lib/sorcery/controller/submodules/brute_force_protection.rb +7 -3
  28. data/lib/sorcery/controller/submodules/external.rb +48 -33
  29. data/lib/sorcery/controller/submodules/http_basic_auth.rb +5 -1
  30. data/lib/sorcery/controller/submodules/remember_me.rb +9 -10
  31. data/lib/sorcery/controller/submodules/session_timeout.rb +32 -6
  32. data/lib/sorcery/crypto_providers/aes256.rb +2 -1
  33. data/lib/sorcery/crypto_providers/bcrypt.rb +8 -2
  34. data/lib/sorcery/engine.rb +16 -3
  35. data/lib/sorcery/model.rb +14 -10
  36. data/lib/sorcery/model/config.rb +12 -4
  37. data/lib/sorcery/model/submodules/brute_force_protection.rb +6 -7
  38. data/lib/sorcery/model/submodules/external.rb +19 -3
  39. data/lib/sorcery/model/submodules/magic_login.rb +130 -0
  40. data/lib/sorcery/model/submodules/reset_password.rb +25 -2
  41. data/lib/sorcery/model/submodules/user_activation.rb +1 -1
  42. data/lib/sorcery/model/temporary_token.rb +3 -1
  43. data/lib/sorcery/protocols/oauth.rb +1 -0
  44. data/lib/sorcery/providers/auth0.rb +46 -0
  45. data/lib/sorcery/providers/discord.rb +52 -0
  46. data/lib/sorcery/providers/heroku.rb +1 -0
  47. data/lib/sorcery/providers/instagram.rb +73 -0
  48. data/lib/sorcery/providers/line.rb +47 -0
  49. data/lib/sorcery/providers/linkedin.rb +45 -36
  50. data/lib/sorcery/providers/vk.rb +5 -4
  51. data/lib/sorcery/providers/wechat.rb +8 -6
  52. data/lib/sorcery/test_helpers/internal.rb +5 -4
  53. data/lib/sorcery/test_helpers/internal/rails.rb +11 -11
  54. data/lib/sorcery/test_helpers/rails/request.rb +20 -0
  55. data/lib/sorcery/version.rb +1 -1
  56. data/sorcery.gemspec +28 -11
  57. data/spec/active_record/user_activation_spec.rb +2 -2
  58. data/spec/active_record/user_activity_logging_spec.rb +2 -2
  59. data/spec/active_record/user_brute_force_protection_spec.rb +2 -2
  60. data/spec/active_record/user_magic_login_spec.rb +15 -0
  61. data/spec/active_record/user_oauth_spec.rb +2 -2
  62. data/spec/active_record/user_remember_me_spec.rb +2 -2
  63. data/spec/active_record/user_reset_password_spec.rb +2 -2
  64. data/spec/active_record/user_spec.rb +0 -10
  65. data/spec/controllers/controller_http_basic_auth_spec.rb +1 -1
  66. data/spec/controllers/controller_oauth2_spec.rb +212 -123
  67. data/spec/controllers/controller_oauth_spec.rb +7 -7
  68. data/spec/controllers/controller_remember_me_spec.rb +16 -8
  69. data/spec/controllers/controller_session_timeout_spec.rb +90 -3
  70. data/spec/controllers/controller_spec.rb +13 -3
  71. data/spec/orm/active_record.rb +2 -2
  72. data/spec/providers/example_provider_spec.rb +17 -0
  73. data/spec/providers/example_spec.rb +17 -0
  74. data/spec/providers/vk_spec.rb +42 -0
  75. data/spec/rails_app/app/assets/config/manifest.js +1 -0
  76. data/spec/rails_app/app/controllers/sorcery_controller.rb +131 -32
  77. data/spec/rails_app/app/mailers/sorcery_mailer.rb +7 -0
  78. data/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb +13 -0
  79. data/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb +6 -0
  80. data/spec/rails_app/config/application.rb +8 -3
  81. data/spec/rails_app/config/boot.rb +1 -1
  82. data/spec/rails_app/config/environment.rb +1 -1
  83. data/spec/rails_app/config/routes.rb +14 -0
  84. data/spec/rails_app/config/secrets.yml +4 -0
  85. data/spec/rails_app/db/migrate/activity_logging/20101224223624_add_activity_logging_to_users.rb +2 -2
  86. data/spec/rails_app/db/migrate/invalidate_active_sessions/20180221093235_add_invalidate_active_sessions_before_to_users.rb +9 -0
  87. data/spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb +17 -0
  88. data/spec/rails_app/db/migrate/reset_password/20101224223622_add_reset_password_to_users.rb +2 -0
  89. data/spec/rails_app/db/schema.rb +7 -9
  90. data/spec/shared_examples/user_magic_login_shared_examples.rb +150 -0
  91. data/spec/shared_examples/user_oauth_shared_examples.rb +1 -1
  92. data/spec/shared_examples/user_remember_me_shared_examples.rb +1 -1
  93. data/spec/shared_examples/user_reset_password_shared_examples.rb +37 -5
  94. data/spec/shared_examples/user_shared_examples.rb +104 -43
  95. data/spec/sorcery_crypto_providers_spec.rb +61 -1
  96. data/spec/sorcery_temporary_token_spec.rb +27 -0
  97. data/spec/spec.opts +1 -1
  98. data/spec/spec_helper.rb +2 -2
  99. data/spec/support/migration_helper.rb +19 -0
  100. data/spec/support/providers/example.rb +11 -0
  101. data/spec/support/providers/example_provider.rb +11 -0
  102. metadata +89 -33
  103. data/gemfiles/active_record-rails40.gemfile +0 -7
  104. data/gemfiles/active_record-rails41.gemfile +0 -7
  105. data/gemfiles/active_record-rails42.gemfile +0 -7
  106. data/spec/rails_app/config/initializers/secret_token.rb +0 -7
@@ -19,7 +19,10 @@ module Sorcery
19
19
  end
20
20
  merge_http_basic_auth_defaults!
21
21
  end
22
- Config.login_sources << :login_from_basic_auth
22
+ # FIXME: There is likely a more elegant way to safeguard these callbacks.
23
+ unless Config.login_sources.include?(:login_from_basic_auth)
24
+ Config.login_sources << :login_from_basic_auth
25
+ end
23
26
  end
24
27
 
25
28
  module InstanceMethods
@@ -57,6 +60,7 @@ module Sorcery
57
60
  while current_controller != ActionController::Base
58
61
  result = Config.controller_to_realm_map[current_controller.controller_name]
59
62
  return result if result
63
+
60
64
  current_controller = current_controller.superclass
61
65
  end
62
66
  nil
@@ -17,9 +17,13 @@ module Sorcery
17
17
  end
18
18
  merge_remember_me_defaults!
19
19
  end
20
- Config.login_sources << :login_from_cookie
21
- Config.after_login << :remember_me_if_asked_to
22
- Config.before_logout << :forget_me!
20
+ # FIXME: There is likely a more elegant way to safeguard these callbacks.
21
+ unless Config.login_sources.include?(:login_from_cookie)
22
+ Config.login_sources << :login_from_cookie
23
+ end
24
+ unless Config.before_logout.include?(:forget_me!)
25
+ Config.before_logout << :forget_me!
26
+ end
23
27
  end
24
28
 
25
29
  module InstanceMethods
@@ -51,20 +55,15 @@ module Sorcery
51
55
 
52
56
  protected
53
57
 
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
58
  # Checks the cookie for a remember me token, tried to find a user with that token
61
59
  # and logs the user in if found.
62
60
  # Runs as a login source. See 'current_user' method for how it is used.
63
61
  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])
62
+ user = cookies.signed[:remember_me_token] && user_class.sorcery_adapter.find_by_remember_me_token(cookies.signed[:remember_me_token]) if defined? cookies
65
63
  if user && user.has_remember_me_token?
66
64
  set_remember_me_cookie!(user)
67
65
  session[:user_id] = user.id.to_s
66
+ after_remember_me!(user)
68
67
  @current_user = user
69
68
  else
70
69
  @current_user = false
@@ -12,24 +12,41 @@ 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 => 3600, # 1.hour
18
- :@session_timeout_from_last_action => false)
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
23
- Config.after_login << :register_login_time
26
+ # FIXME: There is likely a more elegant way to safeguard these callbacks.
27
+ unless Config.after_login.include?(:register_login_time)
28
+ Config.after_login << :register_login_time
29
+ end
30
+ unless Config.after_remember_me.include?(:register_login_time)
31
+ Config.after_remember_me << :register_login_time
32
+ end
24
33
  base.prepend_before_action :validate_session
25
34
  end
26
35
 
27
36
  module InstanceMethods
37
+ def invalidate_active_sessions!
38
+ return unless Config.session_timeout_invalidate_active_sessions_enabled
39
+ return unless current_user.present?
40
+
41
+ current_user.send(:invalidate_sessions_before=, Time.now.in_time_zone)
42
+ current_user.save
43
+ end
44
+
28
45
  protected
29
46
 
30
47
  # Registers last login to be used as the timeout starting point.
31
48
  # Runs as a hook after a successful login.
32
- def register_login_time(_user, _credentials)
49
+ def register_login_time(_user, _credentials = nil)
33
50
  session[:login_time] = session[:last_action_time] = Time.now.in_time_zone
34
51
  end
35
52
 
@@ -37,9 +54,9 @@ module Sorcery
37
54
  # To be used as a before_action, before require_login
38
55
  def validate_session
39
56
  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)
57
+ if (session_to_use && sorcery_session_expired?(session_to_use.to_time)) || sorcery_session_invalidated?
41
58
  reset_sorcery_session
42
- @current_user = nil
59
+ remove_instance_variable :@current_user if defined? @current_user
43
60
  else
44
61
  session[:last_action_time] = Time.now.in_time_zone
45
62
  end
@@ -48,6 +65,15 @@ module Sorcery
48
65
  def sorcery_session_expired?(time)
49
66
  Time.now.in_time_zone - time > Config.session_timeout
50
67
  end
68
+
69
+ # Use login time if present, otherwise use last action time.
70
+ def sorcery_session_invalidated?
71
+ return false unless Config.session_timeout_invalidate_active_sessions_enabled
72
+ return false unless current_user.present? && current_user.try(:invalidate_sessions_before).present?
73
+
74
+ time = session[:login_time] || session[:last_action_time] || Time.now.in_time_zone
75
+ time < current_user.invalidate_sessions_before
76
+ end
51
77
  end
52
78
  end
53
79
  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
- return nil
96
+ nil
91
97
  end
92
98
  end
93
99
  end
@@ -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
- ActionController::Base.send(:include, Sorcery::Controller)
12
- ActionController::Base.helper_method :current_user
13
- ActionController::Base.helper_method :logged_in?
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
- if respond_to?(:"#{config.password_attribute_name}_confirmation=")
196
- send(:"#{config.password_attribute_name}_confirmation=", nil)
197
- end
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
- if defined?(ActionMailer) && config.send(mailer).is_a?(Class) && config.send(mailer) < ActionMailer::Base
206
- mail.send(config.email_delivery_method)
207
- end
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
@@ -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
- end
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
- if send(config.failed_logins_count_attribute_name) >= config.consecutive_login_retries_amount_limit
74
- login_lock!
75
- end
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
- unless config.unlock_token_mailer_disabled || config.unlock_token_mailer.nil?
102
- send_unlock_token_email!
103
- end
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
- user = sorcery_adapter.find_by_id(authentication.send(config.authentications_user_id_attribute_name)) if authentication
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.to_s.downcase.pluralize).build(
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