sorcery 0.11.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE.md +20 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +5 -0
  4. data/.github/workflows/ruby.yml +23 -0
  5. data/.rubocop.yml +55 -0
  6. data/.rubocop_todo.yml +155 -0
  7. data/.travis.yml +11 -51
  8. data/CHANGELOG.md +75 -0
  9. data/CODE_OF_CONDUCT.md +14 -0
  10. data/Gemfile +2 -2
  11. data/{LICENSE.txt → LICENSE.md} +1 -1
  12. data/README.md +34 -7
  13. data/SECURITY.md +18 -0
  14. data/gemfiles/rails_52.gemfile +7 -0
  15. data/gemfiles/rails_60.gemfile +7 -0
  16. data/lib/generators/sorcery/USAGE +1 -1
  17. data/lib/generators/sorcery/helpers.rb +4 -0
  18. data/lib/generators/sorcery/install_generator.rb +21 -21
  19. data/lib/generators/sorcery/templates/initializer.rb +176 -69
  20. data/lib/generators/sorcery/templates/migration/activity_logging.rb +5 -5
  21. data/lib/generators/sorcery/templates/migration/brute_force_protection.rb +4 -4
  22. data/lib/generators/sorcery/templates/migration/core.rb +4 -4
  23. data/lib/generators/sorcery/templates/migration/external.rb +3 -3
  24. data/lib/generators/sorcery/templates/migration/magic_login.rb +9 -0
  25. data/lib/generators/sorcery/templates/migration/remember_me.rb +3 -3
  26. data/lib/generators/sorcery/templates/migration/reset_password.rb +5 -4
  27. data/lib/generators/sorcery/templates/migration/user_activation.rb +4 -4
  28. data/lib/sorcery.rb +2 -0
  29. data/lib/sorcery/adapters/active_record_adapter.rb +4 -3
  30. data/lib/sorcery/adapters/mongoid_adapter.rb +23 -11
  31. data/lib/sorcery/controller.rb +26 -15
  32. data/lib/sorcery/controller/config.rb +7 -5
  33. data/lib/sorcery/controller/submodules/activity_logging.rb +9 -3
  34. data/lib/sorcery/controller/submodules/external.rb +52 -33
  35. data/lib/sorcery/controller/submodules/http_basic_auth.rb +2 -0
  36. data/lib/sorcery/controller/submodules/remember_me.rb +3 -8
  37. data/lib/sorcery/controller/submodules/session_timeout.rb +28 -5
  38. data/lib/sorcery/crypto_providers/aes256.rb +2 -1
  39. data/lib/sorcery/crypto_providers/bcrypt.rb +8 -2
  40. data/lib/sorcery/engine.rb +16 -3
  41. data/lib/sorcery/model.rb +14 -10
  42. data/lib/sorcery/model/config.rb +12 -4
  43. data/lib/sorcery/model/submodules/brute_force_protection.rb +6 -7
  44. data/lib/sorcery/model/submodules/external.rb +19 -3
  45. data/lib/sorcery/model/submodules/magic_login.rb +130 -0
  46. data/lib/sorcery/model/submodules/reset_password.rb +25 -2
  47. data/lib/sorcery/model/submodules/user_activation.rb +1 -1
  48. data/lib/sorcery/model/temporary_token.rb +3 -1
  49. data/lib/sorcery/protocols/oauth.rb +1 -0
  50. data/lib/sorcery/providers/auth0.rb +46 -0
  51. data/lib/sorcery/providers/battlenet.rb +51 -0
  52. data/lib/sorcery/providers/discord.rb +52 -0
  53. data/lib/sorcery/providers/heroku.rb +1 -0
  54. data/lib/sorcery/providers/instagram.rb +73 -0
  55. data/lib/sorcery/providers/line.rb +63 -0
  56. data/lib/sorcery/providers/linkedin.rb +45 -36
  57. data/lib/sorcery/providers/vk.rb +5 -4
  58. data/lib/sorcery/providers/wechat.rb +8 -6
  59. data/lib/sorcery/test_helpers/internal.rb +5 -4
  60. data/lib/sorcery/test_helpers/internal/rails.rb +11 -11
  61. data/lib/sorcery/test_helpers/rails/request.rb +20 -0
  62. data/lib/sorcery/version.rb +1 -1
  63. data/sorcery.gemspec +26 -10
  64. data/spec/active_record/user_activation_spec.rb +2 -2
  65. data/spec/active_record/user_activity_logging_spec.rb +2 -2
  66. data/spec/active_record/user_brute_force_protection_spec.rb +2 -2
  67. data/spec/active_record/user_magic_login_spec.rb +15 -0
  68. data/spec/active_record/user_oauth_spec.rb +2 -2
  69. data/spec/active_record/user_remember_me_spec.rb +2 -2
  70. data/spec/active_record/user_reset_password_spec.rb +2 -2
  71. data/spec/active_record/user_spec.rb +0 -10
  72. data/spec/controllers/controller_http_basic_auth_spec.rb +1 -1
  73. data/spec/controllers/controller_oauth2_spec.rb +230 -123
  74. data/spec/controllers/controller_oauth_spec.rb +13 -7
  75. data/spec/controllers/controller_remember_me_spec.rb +16 -8
  76. data/spec/controllers/controller_session_timeout_spec.rb +90 -3
  77. data/spec/controllers/controller_spec.rb +13 -3
  78. data/spec/orm/active_record.rb +2 -2
  79. data/spec/providers/example_provider_spec.rb +17 -0
  80. data/spec/providers/example_spec.rb +17 -0
  81. data/spec/providers/vk_spec.rb +42 -0
  82. data/spec/rails_app/app/assets/config/manifest.js +1 -0
  83. data/spec/rails_app/app/controllers/application_controller.rb +2 -0
  84. data/spec/rails_app/app/controllers/sorcery_controller.rb +152 -33
  85. data/spec/rails_app/app/mailers/sorcery_mailer.rb +7 -0
  86. data/spec/rails_app/app/views/sorcery_mailer/magic_login_email.html.erb +13 -0
  87. data/spec/rails_app/app/views/sorcery_mailer/magic_login_email.text.erb +6 -0
  88. data/spec/rails_app/config/application.rb +8 -3
  89. data/spec/rails_app/config/boot.rb +1 -1
  90. data/spec/rails_app/config/environment.rb +1 -1
  91. data/spec/rails_app/config/routes.rb +17 -0
  92. data/spec/rails_app/config/secrets.yml +4 -0
  93. data/spec/rails_app/db/migrate/activity_logging/20101224223624_add_activity_logging_to_users.rb +2 -2
  94. data/spec/rails_app/db/migrate/invalidate_active_sessions/20180221093235_add_invalidate_active_sessions_before_to_users.rb +9 -0
  95. data/spec/rails_app/db/migrate/magic_login/20170924151831_add_magic_login_to_users.rb +17 -0
  96. data/spec/rails_app/db/migrate/reset_password/20101224223622_add_reset_password_to_users.rb +2 -0
  97. data/spec/rails_app/db/schema.rb +7 -9
  98. data/spec/shared_examples/user_magic_login_shared_examples.rb +150 -0
  99. data/spec/shared_examples/user_oauth_shared_examples.rb +1 -1
  100. data/spec/shared_examples/user_remember_me_shared_examples.rb +1 -1
  101. data/spec/shared_examples/user_reset_password_shared_examples.rb +37 -5
  102. data/spec/shared_examples/user_shared_examples.rb +104 -43
  103. data/spec/sorcery_crypto_providers_spec.rb +61 -1
  104. data/spec/sorcery_temporary_token_spec.rb +27 -0
  105. data/spec/spec.opts +1 -1
  106. data/spec/spec_helper.rb +2 -2
  107. data/spec/support/migration_helper.rb +29 -0
  108. data/spec/support/providers/example.rb +11 -0
  109. data/spec/support/providers/example_provider.rb +11 -0
  110. metadata +92 -29
  111. data/gemfiles/active_record-rails40.gemfile +0 -7
  112. data/gemfiles/active_record-rails41.gemfile +0 -7
  113. data/gemfiles/active_record-rails42.gemfile +0 -7
  114. 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 => 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
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
- @current_user = nil
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
- 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