sorcery 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4a4f8fffc57148ce88fac95615c62974067f30ab
4
- data.tar.gz: 742f040d7128342812e667e646e80d4f2dcc9119
3
+ metadata.gz: bc71bccb13f8e3fcbbf0b94f660a98bc34cc4826
4
+ data.tar.gz: 9c60fb5db29c7a41378e7c45234b0b0e9df47501
5
5
  SHA512:
6
- metadata.gz: d5bf556539cc55cfa68727b84b2dfd69f22e6fe145d5797cdbd655360ef4cc7bc918dadca217d7d63420972d0e8f4fd86fe469d7edb10a1530eb4b9100a0c694
7
- data.tar.gz: f253c7affbe104709c0f4be6c702ae7f37cecac7ebe7bd1d0609603d8887fe6a4f1c622da5a62b4f85bf542c6e49bdeb322c06ed8d95a34e7f6d880ec4a0c961
6
+ metadata.gz: c557af8ef3828be476750ba465ea4d3c7a34f5ccbf975c5c731d0841891de4f77d56fa867dfd55be9424a2eea0eed768cdfe1a104e6ed70ae643207bb2573eb1
7
+ data.tar.gz: 582ea847785099a70c4866feee1b1f891a411e4f572ff89497ac353914cffe78669102d8a7a6b76c208976894ca932f83e61e590645306e7b5a9c3ebc6868bc2
data/.gitignore CHANGED
@@ -14,6 +14,9 @@ doc
14
14
  # jeweler generated
15
15
  pkg
16
16
 
17
+ # byebug generated
18
+ .byebug_history
19
+
17
20
  # for RVM
18
21
  .rvmrc
19
22
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
+ ## HEAD
3
+
4
+ ## 0.11.0
5
+
6
+ * Refer to User before calling remove_const to avoid NameError [#58](https://github.com/Sorcery/sorcery/pull/58)
7
+ * Resurrect block authentication, showing auth failure reason. [#41](https://github.com/Sorcery/sorcery/pull/41)
8
+ * Add github scope option to initializer.rb [#50](https://github.com/Sorcery/sorcery/pull/50)
9
+
10
+ ## 0.10.3
11
+
12
+ * Revert removal of MongoID Adapter (breaks Sorcery for MongoID users until separate gem is created) [#45](https://github.com/Sorcery/sorcery/pull/45)
13
+
14
+ ## 0.10.2
15
+
16
+ * Added support for Microsoft OAuth (thanks to @athix) [#37](https://github.com/Sorcery/sorcery/pull/37)
17
+
2
18
  ## 0.10.1
19
+
3
20
  * Fixed LinkedIn bug [#36](https://github.com/Sorcery/sorcery/pull/36)
4
21
 
5
22
  ## 0.10.0
@@ -77,7 +94,7 @@
77
94
 
78
95
  ## 0.8.4
79
96
 
80
- * Few security fixes in `external` module
97
+ * Few security fixes in `external` module
81
98
 
82
99
  ## 0.8.3 (yanked because of bad Jeweler release)
83
100
 
@@ -85,38 +102,9 @@
85
102
 
86
103
  * Activity logging feature has a new column called `last_login_from_ip_address` (string type). If you use ActiveRecord, you will have to add this column to DB ([#465](https://github.com/NoamB/sorcery/issues/465))
87
104
 
88
- ## 0.8.1
89
- <!-- TO BE WRITTEN -->
90
-
91
- ## 0.8.0
92
- <!-- TO BE WRITTEN -->
93
-
94
- ## 0.7.13
95
- <!-- TO BE WRITTEN -->
96
-
97
- ## 0.7.12
98
- <!-- TO BE WRITTEN -->
99
-
100
- ## 0.7.11
101
- <!-- TO BE WRITTEN -->
102
-
103
- ## 0.7.10
104
- <!-- TO BE WRITTEN -->
105
-
106
- ## 0.7.9
107
- <!-- TO BE WRITTEN -->
108
-
109
- ## 0.7.8
110
- <!-- TO BE WRITTEN -->
111
-
112
- ## 0.7.7
113
- <!-- TO BE WRITTEN -->
114
-
115
- ## 0.7.6
116
- <!-- TO BE WRITTEN -->
105
+ ## 0.7.5-0.8.1
117
106
 
118
- ## 0.7.5
119
- <!-- TO BE WRITTEN -->
107
+ <!-- HERE BE DRAGONS (Changelogs never written) -->
120
108
 
121
109
  ## 0.7.1-0.7.4
122
110
 
@@ -109,12 +109,14 @@ Rails.application.config.sorcery.configure do |config|
109
109
  # config.facebook.user_info_mapping = {:email => "name"}
110
110
  # config.facebook.access_permissions = ["email", "publish_actions"]
111
111
  # config.facebook.display = "page"
112
- # config.facebook.api_version = "v2.2"
112
+ # config.facebook.api_version = "v2.3"
113
+ # config.facebook.parse = :json
113
114
  #
114
115
  # config.github.key = ""
115
116
  # config.github.secret = ""
116
117
  # config.github.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=github"
117
118
  # config.github.user_info_mapping = {:email => "name"}
119
+ # config.github.scope = ""
118
120
  #
119
121
  # config.paypal.key = ""
120
122
  # config.paypal.secret = ""
@@ -131,6 +133,16 @@ Rails.application.config.sorcery.configure do |config|
131
133
  # config.google.user_info_mapping = {:email => "email", :username => "name"}
132
134
  # config.google.scope = "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
133
135
  #
136
+ # For Microsoft Graph, the key will be your App ID, and the secret will be your app password/public key.
137
+ # The callback URL "can't contain a query string or invalid special characters", see: https://docs.microsoft.com/en-us/azure/active-directory/active-directory-v2-limitations#restrictions-on-redirect-uris
138
+ # More information at https://graph.microsoft.io/en-us/docs
139
+ #
140
+ # config.microsoft.key = ""
141
+ # config.microsoft.secret = ""
142
+ # config.microsoft.callback_url = "http://0.0.0.0:3000/oauth/callback/microsoft"
143
+ # config.microsoft.user_info_mapping = {:email => "userPrincipalName", :username => "displayName"}
144
+ # config.microsoft.scope = "openid email https://graph.microsoft.com/User.Read"
145
+ #
134
146
  # config.vk.key = ""
135
147
  # config.vk.secret = ""
136
148
  # config.vk.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=vk"
@@ -0,0 +1,97 @@
1
+ module Sorcery
2
+ module Adapters
3
+ class MongoidAdapter < BaseAdapter
4
+ def increment(attr)
5
+ mongoid_4? ? @model.inc(attr => 1) : @model.inc(attr, 1)
6
+ end
7
+
8
+ def update_attributes(attrs)
9
+ attrs.each do |name, value|
10
+ attrs[name] = value.utc if value.is_a?(ActiveSupport::TimeWithZone)
11
+ @model.send(:"#{name}=", value)
12
+ end
13
+ @model.class.where(:_id => @model.id).update_all(attrs)
14
+ end
15
+
16
+ def update_attribute(name, value)
17
+ update_attributes(name => value)
18
+ end
19
+
20
+ def save(options = {})
21
+ mthd = options.delete(:raise_on_failure) ? :save! : :save
22
+ @model.send(mthd, options)
23
+ end
24
+
25
+ def mongoid_4?
26
+ Gem::Version.new(::Mongoid::VERSION) >= Gem::Version.new("4.0.0.alpha")
27
+ end
28
+
29
+ class << self
30
+
31
+ def define_field(name, type, options={})
32
+ @klass.field name, options.slice(:default).merge(type: type)
33
+ end
34
+
35
+ def define_callback(time, event, method_name, options={})
36
+ @klass.send "#{time}_#{event}", method_name, options.slice(:if)
37
+ end
38
+
39
+ def credential_regex(credential)
40
+ return { :$regex => /^#{Regexp.escape(credential)}$/i } if (@klass.sorcery_config.downcase_username_before_authenticating)
41
+ credential
42
+ end
43
+
44
+ def find_by_credentials(credentials)
45
+ @klass.sorcery_config.username_attribute_names.each do |attribute|
46
+ @user = @klass.where(attribute => credential_regex(credentials[0])).first
47
+ break if @user
48
+ end
49
+ @user
50
+ end
51
+
52
+ def find_by_oauth_credentials(provider, uid)
53
+ @user_config ||= ::Sorcery::Controller::Config.user_class.to_s.constantize.sorcery_config
54
+ @klass.where(@user_config.provider_attribute_name => provider, @user_config.provider_uid_attribute_name => uid).first
55
+ end
56
+
57
+ def find_by_activation_token(token)
58
+ @klass.where(@klass.sorcery_config.activation_token_attribute_name => token).first
59
+ end
60
+
61
+ def find_by_remember_me_token(token)
62
+ @klass.where(@klass.sorcery_config.remember_me_token_attribute_name => token).first
63
+ end
64
+
65
+ def transaction(&blk)
66
+ tap(&blk)
67
+ end
68
+
69
+ def find_by_id(id)
70
+ @klass.find(id)
71
+ rescue ::Mongoid::Errors::DocumentNotFound
72
+ nil
73
+ end
74
+
75
+ def find_by_username(username)
76
+ query = @klass.sorcery_config.username_attribute_names.map {|name| {name => username}}
77
+ @klass.any_of(*query).first
78
+ end
79
+
80
+ def find_by_token(token_attr_name, token)
81
+ @klass.where(token_attr_name => token).first
82
+ end
83
+
84
+ def find_by_email(email)
85
+ @klass.where(@klass.sorcery_config.email_attribute_name => email).first
86
+ end
87
+
88
+ def get_current_users
89
+ config = @klass.sorcery_config
90
+ @klass.where(config.last_activity_at_attribute_name.ne => nil) \
91
+ .where("this.#{config.last_logout_at_attribute_name} == null || this.#{config.last_activity_at_attribute_name} > this.#{config.last_logout_at_attribute_name}") \
92
+ .where(config.last_activity_at_attribute_name.gt => config.activity_timeout.seconds.ago.utc).order_by([:_id,:asc])
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -22,6 +22,7 @@ module Sorcery
22
22
  require 'sorcery/providers/paypal'
23
23
  require 'sorcery/providers/slack'
24
24
  require 'sorcery/providers/wechat'
25
+ require 'sorcery/providers/microsoft'
25
26
 
26
27
  Config.module_eval do
27
28
  class << self
@@ -30,8 +30,16 @@ module Sorcery
30
30
  # Runs hooks after login or failed login.
31
31
  def login(*credentials)
32
32
  @current_user = nil
33
- user = user_class.authenticate(*credentials)
34
- if user
33
+
34
+ user_class.authenticate(*credentials) do |user, failure_reason|
35
+ if failure_reason
36
+ after_failed_login!(credentials)
37
+
38
+ yield(user, failure_reason) if block_given?
39
+
40
+ return
41
+ end
42
+
35
43
  old_session = session.dup.to_hash
36
44
  reset_sorcery_session
37
45
  old_session.each_pair do |k, v|
@@ -41,10 +49,8 @@ module Sorcery
41
49
 
42
50
  auto_login(user)
43
51
  after_login!(user, credentials)
44
- current_user
45
- else
46
- after_failed_login!(credentials)
47
- nil
52
+
53
+ block_given? ? yield(current_user, nil) : current_user
48
54
  end
49
55
  end
50
56
 
@@ -41,10 +41,15 @@ module Sorcery
41
41
  end
42
42
 
43
43
  module ClassMethods
44
- def load_from_unlock_token(token)
45
- return nil if token.blank?
46
- user = sorcery_adapter.find_by_token(sorcery_config.unlock_token_attribute_name, token)
47
- user
44
+ # This doesn't check to see if the account is still locked
45
+ def load_from_unlock_token(token, &block)
46
+ return if token.blank?
47
+
48
+ load_from_token(
49
+ token,
50
+ sorcery_config.unlock_token_attribute_name,
51
+ &block
52
+ )
48
53
  end
49
54
 
50
55
  protected
@@ -116,7 +121,10 @@ module Sorcery
116
121
  if !login_unlocked? && config.login_lock_time_period != 0
117
122
  login_unlock! if send(config.lock_expires_at_attribute_name) <= Time.now.in_time_zone
118
123
  end
119
- login_unlocked?
124
+
125
+ return false, :locked unless login_unlocked?
126
+
127
+ true
120
128
  end
121
129
  end
122
130
  end
@@ -55,10 +55,13 @@ module Sorcery
55
55
  module ClassMethods
56
56
  # Find user by token, also checks for expiration.
57
57
  # Returns the user if token found and is valid.
58
- def load_from_reset_password_token(token)
59
- token_attr_name = @sorcery_config.reset_password_token_attribute_name
60
- token_expiration_date_attr = @sorcery_config.reset_password_token_expires_at_attribute_name
61
- load_from_token(token, token_attr_name, token_expiration_date_attr)
58
+ def load_from_reset_password_token(token, &block)
59
+ load_from_token(
60
+ token,
61
+ @sorcery_config.reset_password_token_attribute_name,
62
+ @sorcery_config.reset_password_token_expires_at_attribute_name,
63
+ &block
64
+ )
62
65
  end
63
66
 
64
67
  protected
@@ -59,10 +59,13 @@ module Sorcery
59
59
  module ClassMethods
60
60
  # Find user by token, also checks for expiration.
61
61
  # Returns the user if token found and is valid.
62
- def load_from_activation_token(token)
63
- token_attr_name = @sorcery_config.activation_token_attribute_name
64
- token_expiration_date_attr = @sorcery_config.activation_token_expires_at_attribute_name
65
- load_from_token(token, token_attr_name, token_expiration_date_attr)
62
+ def load_from_activation_token(token, &block)
63
+ load_from_token(
64
+ token,
65
+ @sorcery_config.activation_token_attribute_name,
66
+ @sorcery_config.activation_token_expires_at_attribute_name,
67
+ &block
68
+ )
66
69
  end
67
70
 
68
71
  protected
@@ -128,7 +131,14 @@ module Sorcery
128
131
 
129
132
  def prevent_non_active_login
130
133
  config = sorcery_config
131
- config.prevent_non_active_users_to_login ? send(config.activation_state_attribute_name) == 'active' : true
134
+
135
+ if config.prevent_non_active_users_to_login
136
+ unless send(config.activation_state_attribute_name) == 'active'
137
+ return false, :inactive
138
+ end
139
+ end
140
+
141
+ true
132
142
  end
133
143
  end
134
144
  end
@@ -16,13 +16,34 @@ module Sorcery
16
16
  end
17
17
 
18
18
  module ClassMethods
19
- def load_from_token(token, token_attr_name, token_expiration_date_attr)
20
- return nil if token.blank?
19
+ def load_from_token(token, token_attr_name, token_expiration_date_attr = nil, &block)
20
+ return token_response(failure: :invalid_token, &block) if token.blank?
21
+
21
22
  user = sorcery_adapter.find_by_token(token_attr_name, token)
22
- if !user.blank? && !user.send(token_expiration_date_attr).nil?
23
- return Time.now.in_time_zone < user.send(token_expiration_date_attr) ? user : nil
23
+
24
+ return token_response(failure: :user_not_found, &block) unless user
25
+
26
+ unless check_expiration_date(user, token_expiration_date_attr)
27
+ return token_response(user: user, failure: :token_expired, &block)
24
28
  end
25
- user
29
+
30
+ token_response(user: user, return_value: user, &block)
31
+ end
32
+
33
+ protected
34
+
35
+ def check_expiration_date(user, token_expiration_date_attr)
36
+ return true unless token_expiration_date_attr
37
+
38
+ expires_at = user.send(token_expiration_date_attr)
39
+
40
+ !expires_at || (Time.now.in_time_zone < expires_at)
41
+ end
42
+
43
+ def token_response(options = {})
44
+ yield(options[:user], options[:failure]) if block_given?
45
+
46
+ options[:return_value]
26
47
  end
27
48
  end
28
49
  end
data/lib/sorcery/model.rb CHANGED
@@ -80,10 +80,12 @@ module Sorcery
80
80
  # Takes a username and password,
81
81
  # Finds the user by the username and compares the user's password to the one supplied to the method.
82
82
  # returns the user if success, nil otherwise.
83
- def authenticate(*credentials)
83
+ def authenticate(*credentials, &block)
84
84
  raise ArgumentError, 'at least 2 arguments required' if credentials.size < 2
85
85
 
86
- return false if credentials[0].blank?
86
+ if credentials[0].blank?
87
+ return authentication_response(return_value: false, failure: :invalid_login, &block)
88
+ end
87
89
 
88
90
  if @sorcery_config.downcase_username_before_authenticating
89
91
  credentials[0].downcase!
@@ -91,13 +93,29 @@ module Sorcery
91
93
 
92
94
  user = sorcery_adapter.find_by_credentials(credentials)
93
95
 
94
- if user.respond_to?(:active_for_authentication?)
95
- return nil unless user.active_for_authentication?
96
+ unless user
97
+ return authentication_response(failure: :invalid_login, &block)
96
98
  end
97
99
 
98
100
  set_encryption_attributes
99
101
 
100
- user if user && @sorcery_config.before_authenticate.all? { |c| user.send(c) } && user.valid_password?(credentials[1])
102
+ unless user.valid_password?(credentials[1])
103
+ return authentication_response(user: user, failure: :invalid_password, &block)
104
+ end
105
+
106
+ if user.respond_to?(:active_for_authentication?) && !user.active_for_authentication?
107
+ return authentication_response(user: user, failure: :inactive, &block)
108
+ end
109
+
110
+ @sorcery_config.before_authenticate.each do |callback|
111
+ success, reason = user.send(callback)
112
+
113
+ unless success
114
+ return authentication_response(user: user, failure: reason, &block)
115
+ end
116
+ end
117
+
118
+ authentication_response(user: user, return_value: user, &block)
101
119
  end
102
120
 
103
121
  # encrypt tokens using current encryption_provider.
@@ -112,6 +130,12 @@ module Sorcery
112
130
 
113
131
  protected
114
132
 
133
+ def authentication_response(options = {})
134
+ yield(options[:user], options[:failure]) if block_given?
135
+
136
+ options[:return_value]
137
+ end
138
+
115
139
  def set_encryption_attributes
116
140
  @sorcery_config.encryption_provider.stretches = @sorcery_config.stretches if @sorcery_config.encryption_provider.respond_to?(:stretches) && @sorcery_config.stretches
117
141
  @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
@@ -9,9 +9,9 @@ module Sorcery
9
9
  class Facebook < Base
10
10
  include Protocols::Oauth2
11
11
 
12
- attr_reader :mode, :param_name, :parse
12
+ attr_reader :mode, :param_name
13
13
  attr_accessor :access_permissions, :display, :scope, :token_url,
14
- :user_info_path, :auth_path, :api_version
14
+ :user_info_path, :auth_path, :api_version, :parse
15
15
 
16
16
  def initialize
17
17
  super
@@ -24,7 +24,7 @@ module Sorcery
24
24
  @token_url = 'oauth/access_token'
25
25
  @auth_path = 'dialog/oauth'
26
26
  @mode = :query
27
- @parse = :query
27
+ @parse = :json
28
28
  @param_name = 'access_token'
29
29
  end
30
30
 
@@ -0,0 +1,59 @@
1
+ module Sorcery
2
+ module Providers
3
+ # This class adds support for OAuth with Microsoft Graph.
4
+ #
5
+ # config.microsoft.key = <key>
6
+ # config.microsoft.secret = <secret>
7
+ # ...
8
+ #
9
+ class Microsoft < Base
10
+ include Protocols::Oauth2
11
+
12
+ attr_accessor :auth_url, :scope, :token_url, :user_info_url
13
+
14
+ def initialize
15
+ super
16
+
17
+ @site = 'https://login.microsoftonline.com'
18
+ @auth_url = '/common/oauth2/v2.0/authorize'
19
+ @token_url = '/common/oauth2/v2.0/token'
20
+ @user_info_url = 'https://graph.microsoft.com/v1.0/me'
21
+ @scope = 'openid email https://graph.microsoft.com/User.Read'
22
+ @state = SecureRandom.hex(16)
23
+ end
24
+
25
+ def authorize_url(options = {})
26
+ oauth_params = {
27
+ client_id: @key,
28
+ response_type: 'code'
29
+ }
30
+ options.merge!(oauth_params)
31
+ super(options)
32
+ end
33
+
34
+ def get_user_hash(access_token)
35
+ response = access_token.get(user_info_url)
36
+
37
+ auth_hash(access_token).tap do |h|
38
+ h[:user_info] = JSON.parse(response.body)
39
+ h[:uid] = h[:user_info]['id']
40
+ end
41
+ end
42
+
43
+ # calculates and returns the url to which the user should be redirected,
44
+ # to get authenticated at the external provider's site.
45
+ def login_url(_params, _session)
46
+ authorize_url(authorize_url: auth_url)
47
+ end
48
+
49
+ # tries to login the user from access token
50
+ def process_callback(params, _session)
51
+ args = {}.tap do |a|
52
+ a[:code] = params[:code] if params[:code]
53
+ end
54
+
55
+ get_access_token(args, token_url: token_url, token_method: :post)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -67,7 +67,7 @@ module Sorcery
67
67
  # reload user class between specs
68
68
  # so it will be possible to test the different submodules in isolation
69
69
  def reload_user_class
70
- Object.send(:remove_const, 'User')
70
+ User && Object.send(:remove_const, 'User')
71
71
  load 'user.rb'
72
72
  if User.respond_to?(:reset_column_information)
73
73
  User.reset_column_information
@@ -1,3 +1,3 @@
1
1
  module Sorcery
2
- VERSION = '0.10.1'
2
+ VERSION = '0.11.0'
3
3
  end
data/lib/sorcery.rb CHANGED
@@ -65,7 +65,7 @@ module Sorcery
65
65
 
66
66
  require 'sorcery/adapters/base_adapter'
67
67
 
68
- if defined?(ActiveRecord)
68
+ if defined?(ActiveRecord::Base)
69
69
  require 'sorcery/adapters/active_record_adapter'
70
70
  ActiveRecord::Base.extend Sorcery::Model
71
71
 
@@ -78,5 +78,18 @@ module Sorcery
78
78
  end
79
79
  end
80
80
 
81
+ if defined?(Mongoid::Document)
82
+ require 'sorcery/adapters/mongoid_adapter'
83
+ Mongoid::Document::ClassMethods.send :include, Sorcery::Model
84
+
85
+ Mongoid::Document.send :define_method, :sorcery_adapter do
86
+ @sorcery_adapter ||= Sorcery::Adapters::MongoidAdapter.new(self)
87
+ end
88
+
89
+ Mongoid::Document::ClassMethods.send :define_method, :sorcery_adapter do
90
+ Sorcery::Adapters::MongoidAdapter.from(self)
91
+ end
92
+ end
93
+
81
94
  require 'sorcery/engine' if defined?(Rails)
82
95
  end
data/sorcery.gemspec CHANGED
@@ -10,8 +10,8 @@ Gem::Specification.new do |s|
10
10
  s.description = 'Provides common authentication needs such as signing in/out, activating by email and resetting password.'
11
11
  s.summary = 'Magical authentication for Rails applications'
12
12
  s.homepage = 'https://github.com/Sorcery/sorcery'
13
- s.post_install_message = "As of version 1.0 oauth/oauth2 won't be automatically bundled\n"
14
- s.post_install_message += 'you need to add those dependencies to your Gemfile'
13
+ s.post_install_message = "As of version 1.0 oauth/oauth2 won't be automatically bundled so you may need to add those dependencies to your Gemfile.\n"
14
+ s.post_install_message += 'You may need oauth2 if you use external providers such as any of these: https://github.com/Sorcery/sorcery/tree/master/lib/sorcery/providers'
15
15
 
16
16
  s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
17
  s.require_paths = ['lib']
@@ -29,4 +29,5 @@ Gem::Specification.new do |s|
29
29
  s.add_development_dependency 'simplecov', '>= 0.3.8'
30
30
  s.add_development_dependency 'rspec-rails', '~> 3.5.0'
31
31
  s.add_development_dependency 'test-unit', '~> 3.1.0'
32
+ s.add_development_dependency 'byebug', '~> 9.0.0'
32
33
  end
@@ -20,7 +20,7 @@ describe SorceryController, type: :controller do
20
20
  end
21
21
 
22
22
  it 'counts login retries' do
23
- allow(User).to receive(:authenticate)
23
+ allow(User).to receive(:authenticate) { |&block| block.call(nil, :other) }
24
24
  allow(User.sorcery_adapter).to receive(:find_by_credentials).with(['bla@bla.com', 'blabla']).and_return(user)
25
25
 
26
26
  expect(user).to receive(:register_failed_login!).exactly(3).times
@@ -32,7 +32,7 @@ describe SorceryController, type: :controller do
32
32
  # dirty hack for rails 4
33
33
  allow(@controller).to receive(:register_last_activity_time_to_db)
34
34
 
35
- allow(User).to receive(:authenticate).and_return(user)
35
+ allow(User).to receive(:authenticate) { |&block| block.call(user, nil) }
36
36
  expect(user).to receive_message_chain(:sorcery_adapter, :update_attribute).with(:failed_logins_count, 0)
37
37
 
38
38
  get :test_login, params: { email: 'bla@bla.com', password: 'secret' }
@@ -151,7 +151,7 @@ describe SorceryController, active_record: true, type: :controller do
151
151
  expect(flash[:notice]).to eq 'Success!'
152
152
  end
153
153
 
154
- [:github, :google, :liveid, :vk, :salesforce, :paypal, :slack, :wechat].each do |provider|
154
+ [:github, :google, :liveid, :vk, :salesforce, :paypal, :slack, :wechat, :microsoft].each do |provider|
155
155
  describe "with #{provider}" do
156
156
  it 'login_at redirects correctly' do
157
157
  get :"login_at_test_#{provider}"
@@ -201,7 +201,7 @@ describe SorceryController, active_record: true, type: :controller do
201
201
  end
202
202
 
203
203
  sorcery_reload!([:user_activation,:external], :user_activation_mailer => ::SorceryMailer)
204
- sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :salesforce, :paypal, :slack, :wechat])
204
+ sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :salesforce, :paypal, :slack, :wechat, :microsoft])
205
205
 
206
206
  # TODO: refactor
207
207
  sorcery_controller_external_property_set(:facebook, :key, "eYVNBjBDi33aa9GkA3w")
@@ -231,6 +231,9 @@ describe SorceryController, active_record: true, type: :controller do
231
231
  sorcery_controller_external_property_set(:wechat, :key, "eYVNBjBDi33aa9GkA3w")
232
232
  sorcery_controller_external_property_set(:wechat, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8")
233
233
  sorcery_controller_external_property_set(:wechat, :callback_url, "http://blabla.com")
234
+ sorcery_controller_external_property_set(:microsoft, :key, "eYVNBjBDi33aa9GkA3w")
235
+ sorcery_controller_external_property_set(:microsoft, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8")
236
+ sorcery_controller_external_property_set(:microsoft, :callback_url, "http://blabla.com")
234
237
  end
235
238
 
236
239
  after(:all) do
@@ -260,7 +263,7 @@ describe SorceryController, active_record: true, type: :controller do
260
263
  expect(ActionMailer::Base.deliveries.size).to eq old_size
261
264
  end
262
265
 
263
- [:github, :google, :liveid, :vk, :salesforce, :paypal, :wechat].each do |provider|
266
+ [:github, :google, :liveid, :vk, :salesforce, :paypal, :wechat, :microsoft].each do |provider|
264
267
  it "does not send activation email to external users (#{provider})" do
265
268
  old_size = ActionMailer::Base.deliveries.size
266
269
  create_new_external_user provider
@@ -409,7 +412,7 @@ describe SorceryController, active_record: true, type: :controller do
409
412
  end
410
413
 
411
414
  def set_external_property
412
- sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :salesforce, :paypal, :slack, :wechat])
415
+ sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :salesforce, :paypal, :slack, :wechat, :microsoft])
413
416
  sorcery_controller_external_property_set(:facebook, :key, "eYVNBjBDi33aa9GkA3w")
414
417
  sorcery_controller_external_property_set(:facebook, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8")
415
418
  sorcery_controller_external_property_set(:facebook, :callback_url, "http://blabla.com")
@@ -437,6 +440,9 @@ describe SorceryController, active_record: true, type: :controller do
437
440
  sorcery_controller_external_property_set(:wechat, :key, "eYVNBjBDi33aa9GkA3w")
438
441
  sorcery_controller_external_property_set(:wechat, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8")
439
442
  sorcery_controller_external_property_set(:wechat, :callback_url, "http://blabla.com")
443
+ sorcery_controller_external_property_set(:microsoft, :key, "eYVNBjBDi33aa9GkA3w")
444
+ sorcery_controller_external_property_set(:microsoft, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8")
445
+ sorcery_controller_external_property_set(:microsoft, :callback_url, "http://blabla.com")
440
446
  end
441
447
 
442
448
  def provider_url(provider)
@@ -448,7 +454,8 @@ describe SorceryController, active_record: true, type: :controller do
448
454
  vk: "https://oauth.vk.com/authorize?client_id=#{::Sorcery::Controller::Config.vk.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=#{::Sorcery::Controller::Config.vk.scope}&state",
449
455
  salesforce: "https://login.salesforce.com/services/oauth2/authorize?client_id=#{::Sorcery::Controller::Config.salesforce.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope#{'=' + ::Sorcery::Controller::Config.salesforce.scope unless ::Sorcery::Controller::Config.salesforce.scope.nil?}&state",
450
456
  slack: "https://slack.com/oauth/authorize?client_id=#{::Sorcery::Controller::Config.slack.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=identity.basic%2C+identity.email&state",
451
- wechat: "https://open.weixin.qq.com/connect/qrconnect?appid=#{::Sorcery::Controller::Config.wechat.key}&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=snsapi_login&state=#wechat_redirect"
457
+ wechat: "https://open.weixin.qq.com/connect/qrconnect?appid=#{::Sorcery::Controller::Config.wechat.key}&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=snsapi_login&state=#wechat_redirect",
458
+ microsoft: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=#{::Sorcery::Controller::Config.microsoft.key}&display&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=openid+email+https%3A%2F%2Fgraph.microsoft.com%2FUser.Read&state"
452
459
  }[provider]
453
460
  end
454
461
  end
@@ -22,7 +22,7 @@ describe SorceryController, type: :controller do
22
22
  end
23
23
 
24
24
  it 'sets cookie on remember_me!' do
25
- expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user)
25
+ expect(User).to receive(:authenticate).with('bla@bla.com', 'secret') { |&block| block.call(user, nil) }
26
26
  expect(user).to receive(:remember_me!)
27
27
 
28
28
  post :test_login_with_remember, params: { email: 'bla@bla.com', password: 'secret' }
@@ -45,7 +45,7 @@ describe SorceryController, type: :controller do
45
45
  end
46
46
 
47
47
  it 'login(email,password,remember_me) logs user in and remembers' do
48
- expect(User).to receive(:authenticate).with('bla@bla.com', 'secret', '1').and_return(user)
48
+ expect(User).to receive(:authenticate).with('bla@bla.com', 'secret', '1') { |&block| block.call(user, nil) }
49
49
  expect(user).to receive(:remember_me!)
50
50
  expect(user).to receive(:remember_me_token).and_return('abracadabra').twice
51
51
 
@@ -39,7 +39,7 @@ describe SorceryController, type: :controller do
39
39
  it 'works if the session is stored as a string or a Time' do
40
40
  session[:login_time] = Time.now.to_s
41
41
  # TODO: ???
42
- expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user)
42
+ expect(User).to receive(:authenticate).with('bla@bla.com', 'secret') { |&block| block.call(user, nil) }
43
43
 
44
44
  get :test_login, params: { email: 'bla@bla.com', password: 'secret' }
45
45
 
@@ -50,7 +50,7 @@ describe SorceryController, type: :controller do
50
50
  context "with 'session_timeout_from_last_action'" do
51
51
  it 'does not logout if there was activity' do
52
52
  sorcery_controller_property_set(:session_timeout_from_last_action, true)
53
- expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user)
53
+ expect(User).to receive(:authenticate).with('bla@bla.com', 'secret') { |&block| block.call(user, nil) }
54
54
 
55
55
  get :test_login, params: { email: 'bla@bla.com', password: 'secret' }
56
56
  Timecop.travel(Time.now.in_time_zone + 0.3)
@@ -52,7 +52,7 @@ describe SorceryController, type: :controller do
52
52
  describe '#login' do
53
53
  context 'when succeeds' do
54
54
  before do
55
- expect(User).to receive(:authenticate).with('bla@bla.com', 'secret').and_return(user)
55
+ expect(User).to receive(:authenticate).with('bla@bla.com', 'secret') { |&block| block.call(user, nil) }
56
56
  get :test_login, params: { email: 'bla@bla.com', password: 'secret' }
57
57
  end
58
58
 
@@ -104,6 +104,10 @@ class SorceryController < ActionController::Base
104
104
  login_at(:wechat)
105
105
  end
106
106
 
107
+ def login_at_test_microsoft
108
+ login_at(:microsoft)
109
+ end
110
+
107
111
  def login_at_test_google
108
112
  login_at(:google)
109
113
  end
@@ -174,6 +178,14 @@ class SorceryController < ActionController::Base
174
178
  end
175
179
  end
176
180
 
181
+ def test_login_from_microsoft
182
+ if @user = login_from(:microsoft)
183
+ redirect_to 'bla', notice: 'Success!'
184
+ else
185
+ redirect_to 'blu', alert: 'Failed!'
186
+ end
187
+ end
188
+
177
189
  def test_login_from_google
178
190
  if @user = login_from(:google)
179
191
  redirect_to 'bla', notice: 'Success!'
@@ -272,6 +284,14 @@ class SorceryController < ActionController::Base
272
284
  end
273
285
  end
274
286
 
287
+ def test_return_to_with_external_microsoft
288
+ if @user = login_from(:microsoft)
289
+ redirect_back_or_to 'bla', notice: 'Success!'
290
+ else
291
+ redirect_to 'blu', alert: 'Failed!'
292
+ end
293
+ end
294
+
275
295
  def test_return_to_with_external_google
276
296
  if @user = login_from(:google)
277
297
  redirect_back_or_to 'bla', notice: 'Success!'
@@ -22,6 +22,7 @@ AppRoot::Application.routes.draw do
22
22
  get :test_login_from_github
23
23
  get :test_login_from_paypal
24
24
  get :test_login_from_wechat
25
+ get :test_login_from_microsoft
25
26
  get :test_login_from_google
26
27
  get :test_login_from_liveid
27
28
  get :test_login_from_vk
@@ -34,6 +35,7 @@ AppRoot::Application.routes.draw do
34
35
  get :login_at_test_github
35
36
  get :login_at_test_paypal
36
37
  get :login_at_test_wechat
38
+ get :login_at_test_microsoft
37
39
  get :login_at_test_google
38
40
  get :login_at_test_liveid
39
41
  get :login_at_test_vk
@@ -46,6 +48,7 @@ AppRoot::Application.routes.draw do
46
48
  get :test_return_to_with_external_github
47
49
  get :test_return_to_with_external_paypal
48
50
  get :test_return_to_with_external_wechat
51
+ get :test_return_to_with_external_microsoft
49
52
  get :test_return_to_with_external_google
50
53
  get :test_return_to_with_external_liveid
51
54
  get :test_return_to_with_external_vk
@@ -235,6 +235,27 @@ shared_examples_for 'rails_3_activation_model' do
235
235
 
236
236
  expect(User.authenticate(user.email, 'secret')).to be_truthy
237
237
  end
238
+
239
+ context 'in block mode' do
240
+ it 'does not allow a non-active user to authenticate' do
241
+ sorcery_model_property_set(:prevent_non_active_users_to_login, true)
242
+
243
+ User.authenticate(user.email, 'secret') do |user2, failure|
244
+ expect(user2).to eq user
245
+ expect(user2.activation_state).to eq 'pending'
246
+ expect(failure).to eq :inactive
247
+ end
248
+ end
249
+
250
+ it 'allows a non-active user to authenticate if configured so' do
251
+ sorcery_model_property_set(:prevent_non_active_users_to_login, false)
252
+
253
+ User.authenticate(user.email, 'secret') do |user2, failure|
254
+ expect(user2).to eq user
255
+ expect(failure).to be_nil
256
+ end
257
+ end
258
+ end
238
259
  end
239
260
 
240
261
  describe 'load_from_activation_token' do
@@ -279,5 +300,62 @@ shared_examples_for 'rails_3_activation_model' do
279
300
 
280
301
  expect(User.load_from_activation_token(user.activation_token)).to eq user
281
302
  end
303
+
304
+ describe '#load_from_activation_token' do
305
+ context 'in block mode' do
306
+ it 'yields user when token is found' do
307
+ User.load_from_activation_token(user.activation_token) do |user2, failure|
308
+ expect(user2).to eq user
309
+ expect(failure).to be_nil
310
+ end
311
+ end
312
+
313
+ it 'does NOT yield user when token is NOT found' do
314
+ User.load_from_activation_token('a') do |user2, failure|
315
+ expect(user2).to be_nil
316
+ expect(failure).to eq :user_not_found
317
+ end
318
+ end
319
+
320
+ it 'yields user when token is found and not expired' do
321
+ sorcery_model_property_set(:activation_token_expiration_period, 500)
322
+
323
+ User.load_from_activation_token(user.activation_token) do |user2, failure|
324
+ expect(user2).to eq user
325
+ expect(failure).to be_nil
326
+ end
327
+ end
328
+
329
+ it 'yields the user and failure reason when token is found and expired' do
330
+ sorcery_model_property_set(:activation_token_expiration_period, 0.1)
331
+ user
332
+
333
+ Timecop.travel(Time.now.in_time_zone + 0.5)
334
+
335
+ User.load_from_activation_token(user.activation_token) do |user2, failure|
336
+ expect(user2).to eq user
337
+ expect(failure).to eq :token_expired
338
+ end
339
+ end
340
+
341
+ it 'yields a failure reason if token is blank' do
342
+ [nil, ''].each do |token|
343
+ User.load_from_activation_token(token) do |user2, failure|
344
+ expect(user2).to be_nil
345
+ expect(failure).to eq :invalid_token
346
+ end
347
+ end
348
+ end
349
+
350
+ it 'is always valid if expiration period is nil' do
351
+ sorcery_model_property_set(:activation_token_expiration_period, nil)
352
+
353
+ User.load_from_activation_token(user.activation_token) do |user2, failure|
354
+ expect(user2).to eq user
355
+ expect(failure).to be_nil
356
+ end
357
+ end
358
+ end
359
+ end
282
360
  end
283
361
  end
@@ -128,6 +128,71 @@ shared_examples_for 'rails_3_reset_password_model' do
128
128
  expect(User.load_from_reset_password_token('')).to be_nil
129
129
  end
130
130
 
131
+ describe '#load_from_reset_password_token' do
132
+ context 'in block mode' do
133
+ it 'yields user when token is found' do
134
+ user.generate_reset_password_token!
135
+ updated_user = User.sorcery_adapter.find(user.id)
136
+
137
+ User.load_from_reset_password_token(user.reset_password_token) do |user2, failure|
138
+ expect(user2).to eq updated_user
139
+ expect(failure).to be_nil
140
+ end
141
+ end
142
+
143
+ it 'does NOT yield user when token is NOT found' do
144
+ user.generate_reset_password_token!
145
+
146
+ User.load_from_reset_password_token('a') do |user2, failure|
147
+ expect(user2).to be_nil
148
+ expect(failure).to eq :user_not_found
149
+ end
150
+ end
151
+
152
+ it 'yields user when token is found and not expired' do
153
+ sorcery_model_property_set(:reset_password_expiration_period, 500)
154
+ user.generate_reset_password_token!
155
+ updated_user = User.sorcery_adapter.find(user.id)
156
+
157
+ User.load_from_reset_password_token(user.reset_password_token) do |user2, failure|
158
+ expect(user2).to eq updated_user
159
+ expect(failure).to be_nil
160
+ end
161
+ end
162
+
163
+ it 'yields user and failure reason when token is found and expired' do
164
+ sorcery_model_property_set(:reset_password_expiration_period, 0.1)
165
+ user.generate_reset_password_token!
166
+ Timecop.travel(Time.now.in_time_zone + 0.5)
167
+
168
+ User.load_from_reset_password_token(user.reset_password_token) do |user2, failure|
169
+ expect(user2).to eq user
170
+ expect(failure).to eq :token_expired
171
+ end
172
+ end
173
+
174
+ it 'is always valid if expiration period is nil' do
175
+ sorcery_model_property_set(:reset_password_expiration_period, nil)
176
+ user.generate_reset_password_token!
177
+ updated_user = User.sorcery_adapter.find(user.id)
178
+
179
+ User.load_from_reset_password_token(user.reset_password_token) do |user2, failure|
180
+ expect(user2).to eq updated_user
181
+ expect(failure).to be_nil
182
+ end
183
+ end
184
+
185
+ it 'returns nil if token is blank' do
186
+ [nil, ''].each do |token|
187
+ User.load_from_reset_password_token(token) do |user2, failure|
188
+ expect(user2).to be_nil
189
+ expect(failure).to eq :invalid_token
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+
131
196
  it "'deliver_reset_password_instructions!' generates a reset_password_token" do
132
197
  expect(user.reset_password_token).to be_nil
133
198
 
@@ -146,6 +146,31 @@ shared_examples_for 'rails_3_core_model' do
146
146
  expect(User.authenticate(user.email, 'secret')).to be_nil
147
147
  end
148
148
  end
149
+
150
+ context 'in block mode' do
151
+ it 'yields the user if credentials are good' do
152
+ User.authenticate(user.email, 'secret') do |user2, failure|
153
+ expect(user2).to eq user
154
+ expect(failure).to be_nil
155
+ end
156
+ end
157
+
158
+ it 'yields the user and proper error if credentials are bad' do
159
+ User.authenticate(user.email, 'wrong!') do |user2, failure|
160
+ expect(user2).to eq user
161
+ expect(failure).to eq :invalid_password
162
+ end
163
+ end
164
+
165
+ it 'yields the proper error if no user exists' do
166
+ [nil, '', 'not@a.user'].each do |email|
167
+ User.authenticate(email, 'wrong!') do |user2, failure|
168
+ expect(user2).to be_nil
169
+ expect(failure).to eq :invalid_login
170
+ end
171
+ end
172
+ end
173
+ end
149
174
  end
150
175
 
151
176
  specify { expect(User).to respond_to(:encrypt) }
data/spec/spec_helper.rb CHANGED
@@ -11,6 +11,7 @@ SORCERY_ORM = :active_record
11
11
  require 'rails/all'
12
12
  require 'rspec/rails'
13
13
  require 'timecop'
14
+ require 'byebug'
14
15
 
15
16
  def setup_orm; end
16
17
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sorcery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noam Ben Ari
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2017-01-03 00:00:00.000000000 Z
14
+ date: 2017-05-16 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: oauth
@@ -137,6 +137,20 @@ dependencies:
137
137
  - - "~>"
138
138
  - !ruby/object:Gem::Version
139
139
  version: 3.1.0
140
+ - !ruby/object:Gem::Dependency
141
+ name: byebug
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: 9.0.0
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: 9.0.0
140
154
  description: Provides common authentication needs such as signing in/out, activating
141
155
  by email and resetting password.
142
156
  email: chase.gilliam@gmail.com
@@ -170,6 +184,7 @@ files:
170
184
  - lib/sorcery.rb
171
185
  - lib/sorcery/adapters/active_record_adapter.rb
172
186
  - lib/sorcery/adapters/base_adapter.rb
187
+ - lib/sorcery/adapters/mongoid_adapter.rb
173
188
  - lib/sorcery/controller.rb
174
189
  - lib/sorcery/controller/config.rb
175
190
  - lib/sorcery/controller/submodules/activity_logging.rb
@@ -206,6 +221,7 @@ files:
206
221
  - lib/sorcery/providers/jira.rb
207
222
  - lib/sorcery/providers/linkedin.rb
208
223
  - lib/sorcery/providers/liveid.rb
224
+ - lib/sorcery/providers/microsoft.rb
209
225
  - lib/sorcery/providers/paypal.rb
210
226
  - lib/sorcery/providers/salesforce.rb
211
227
  - lib/sorcery/providers/slack.rb
@@ -289,8 +305,8 @@ licenses:
289
305
  - MIT
290
306
  metadata: {}
291
307
  post_install_message: |-
292
- As of version 1.0 oauth/oauth2 won't be automatically bundled
293
- you need to add those dependencies to your Gemfile
308
+ As of version 1.0 oauth/oauth2 won't be automatically bundled so you may need to add those dependencies to your Gemfile.
309
+ You may need oauth2 if you use external providers such as any of these: https://github.com/Sorcery/sorcery/tree/master/lib/sorcery/providers
294
310
  rdoc_options: []
295
311
  require_paths:
296
312
  - lib
@@ -311,3 +327,4 @@ signing_key:
311
327
  specification_version: 4
312
328
  summary: Magical authentication for Rails applications
313
329
  test_files: []
330
+ has_rdoc: