omniauth-identity 3.0.3 → 3.0.8

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.
@@ -5,6 +5,7 @@ require 'mongoid'
5
5
  module OmniAuth
6
6
  module Identity
7
7
  module Models
8
+ # NOTE: Mongoid is based on ActiveModel.
8
9
  module Mongoid
9
10
  def self.included(base)
10
11
  base.class_eval do
@@ -6,6 +6,7 @@ module OmniAuth
6
6
  module Identity
7
7
  module Models
8
8
  # http://nobrainer.io/ an ORM for RethinkDB
9
+ # NOTE: NoBrainer is based on ActiveModel.
9
10
  module NoBrainer
10
11
  def self.included(base)
11
12
  base.class_eval do
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nobrainer'
4
+
5
+ module OmniAuth
6
+ module Identity
7
+ module Models
8
+ # http://sequel.jeremyevans.net/ an SQL ORM
9
+ # NOTE: Sequel is *not* based on ActiveModel, but supports the API we need, except for `persisted?`:
10
+ # * create
11
+ # * save
12
+ module Sequel
13
+ def self.included(base)
14
+ base.class_eval do
15
+ # NOTE: Using the deprecated :validations_class_methods because it defines
16
+ # validates_confirmation_of, while current :validation_helpers does not.
17
+ # plugin :validation_helpers
18
+ plugin :validation_class_methods
19
+
20
+ include ::OmniAuth::Identity::Model
21
+ include ::OmniAuth::Identity::SecurePassword
22
+
23
+ has_secure_password
24
+
25
+ alias_method :persisted?, :valid?
26
+
27
+ def self.auth_key=(key)
28
+ super
29
+ validates_uniqueness_of :key, case_sensitive: false
30
+ end
31
+
32
+ def self.locate(search_hash)
33
+ where(search_hash).first
34
+ end
35
+
36
+ def persisted?
37
+ exists?
38
+ end
39
+
40
+ def save
41
+ super
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -4,7 +4,7 @@ require 'bcrypt'
4
4
 
5
5
  module OmniAuth
6
6
  module Identity
7
- # This is taken directly from Rails 3.1 code and is used if
7
+ # This is lightly edited from Rails 6.1 code and is used if
8
8
  # the version of ActiveModel that's being used does not
9
9
  # include SecurePassword. The only difference is that instead of
10
10
  # using ActiveSupport::Concern, it checks to see if there is already
@@ -14,63 +14,124 @@ module OmniAuth
14
14
  base.extend ClassMethods unless base.respond_to?(:has_secure_password)
15
15
  end
16
16
 
17
+ # BCrypt hash function can handle maximum 72 bytes, and if we pass
18
+ # password of length more than 72 bytes it ignores extra characters.
19
+ # Hence need to put a restriction on password length.
20
+ MAX_PASSWORD_LENGTH_ALLOWED = 72
21
+
22
+ class << self
23
+ attr_accessor :min_cost # :nodoc:
24
+ end
25
+ self.min_cost = false
26
+
17
27
  module ClassMethods
18
28
  # Adds methods to set and authenticate against a BCrypt password.
19
- # This mechanism requires you to have a password_digest attribute.
29
+ # This mechanism requires you to have a +XXX_digest+ attribute.
30
+ # Where +XXX+ is the attribute name of your desired password.
31
+ #
32
+ # The following validations are added automatically:
33
+ # * Password must be present on creation
34
+ # * Password length should be less than or equal to 72 bytes
35
+ # * Confirmation of password (using a +XXX_confirmation+ attribute)
20
36
  #
21
- # Validations for presence of password, confirmation of password (using
22
- # a "password_confirmation" attribute) are automatically added.
23
- # You can add more validations by hand if need be.
37
+ # If confirmation validation is not needed, simply leave out the
38
+ # value for +XXX_confirmation+ (i.e. don't provide a form field for
39
+ # it). When this attribute has a +nil+ value, the validation will not be
40
+ # triggered.
41
+ #
42
+ # For further customizability, it is possible to suppress the default
43
+ # validations by passing <tt>validations: false</tt> as an argument.
44
+ #
45
+ # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
46
+ #
47
+ # gem 'bcrypt', '~> 3.1.7'
24
48
  #
25
49
  # Example using Active Record (which automatically includes ActiveModel::SecurePassword):
26
50
  #
27
- # # Schema: User(name:string, password_digest:string)
51
+ # # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
28
52
  # class User < ActiveRecord::Base
29
53
  # has_secure_password
54
+ # has_secure_password :recovery_password, validations: false
30
55
  # end
31
56
  #
32
- # user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch")
33
- # user.save # => false, password required
34
- # user.password = "mUc3m00RsqyRe"
35
- # user.save # => false, confirmation doesn't match
36
- # user.password_confirmation = "mUc3m00RsqyRe"
37
- # user.save # => true
38
- # user.authenticate("notright") # => false
39
- # user.authenticate("mUc3m00RsqyRe") # => user
40
- # User.find_by_name("david").try(:authenticate, "notright") # => nil
41
- # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user
42
- def has_secure_password
43
- attr_reader :password
57
+ # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
58
+ # user.save # => false, password required
59
+ # user.password = 'mUc3m00RsqyRe'
60
+ # user.save # => false, confirmation doesn't match
61
+ # user.password_confirmation = 'mUc3m00RsqyRe'
62
+ # user.save # => true
63
+ # user.recovery_password = "42password"
64
+ # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
65
+ # user.save # => true
66
+ # user.authenticate('notright') # => false
67
+ # user.authenticate('mUc3m00RsqyRe') # => user
68
+ # user.authenticate_recovery_password('42password') # => user
69
+ # User.find_by(name: 'david')&.authenticate('notright') # => false
70
+ # User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
71
+ def has_secure_password(attribute = :password, validations: true)
72
+ # Load bcrypt gem only when has_secure_password is used.
73
+ # This is to avoid ActiveModel (and by extension the entire framework)
74
+ # being dependent on a binary library.
75
+ begin
76
+ require 'bcrypt'
77
+ rescue LoadError
78
+ warn "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
79
+ raise
80
+ end
44
81
 
45
- validates_confirmation_of :password
46
- validates_presence_of :password_digest
82
+ include InstanceMethodsOnActivation.new(attribute)
47
83
 
48
- include InstanceMethodsOnActivation
84
+ if validations
85
+ include ActiveModel::Validations
49
86
 
50
- if respond_to?(:attributes_protected_by_default)
51
- def self.attributes_protected_by_default
52
- super + ['password_digest']
87
+ # This ensures the model has a password by checking whether the password_digest
88
+ # is present, so that this works with both new and existing records. However,
89
+ # when there is an error, the message is added to the password attribute instead
90
+ # so that the error message will make sense to the end-user.
91
+ validate do |record|
92
+ record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
53
93
  end
94
+
95
+ validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
96
+ validates_confirmation_of attribute, allow_blank: true
54
97
  end
55
98
  end
56
99
  end
57
100
 
58
- module InstanceMethodsOnActivation
59
- # Returns self if the password is correct, otherwise false.
60
- def authenticate(unencrypted_password)
61
- if BCrypt::Password.new(password_digest) == unencrypted_password
62
- self
63
- else
64
- false
101
+ class InstanceMethodsOnActivation < Module
102
+ def initialize(attribute)
103
+ attr_reader attribute
104
+
105
+ define_method("#{attribute}=") do |unencrypted_password|
106
+ if unencrypted_password.nil?
107
+ public_send("#{attribute}_digest=", nil)
108
+ elsif !unencrypted_password.empty?
109
+ instance_variable_set("@#{attribute}", unencrypted_password)
110
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
111
+ public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
112
+ end
65
113
  end
66
- end
67
114
 
68
- # Encrypts the password into the password_digest attribute.
69
- def password=(unencrypted_password)
70
- @password = unencrypted_password
71
- if unencrypted_password && !unencrypted_password.empty?
72
- self.password_digest = BCrypt::Password.create(unencrypted_password)
115
+ define_method("#{attribute}_confirmation=") do |unencrypted_password|
116
+ instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
73
117
  end
118
+
119
+ # Returns +self+ if the password is correct, otherwise +false+.
120
+ #
121
+ # class User < ActiveRecord::Base
122
+ # has_secure_password validations: false
123
+ # end
124
+ #
125
+ # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
126
+ # user.save
127
+ # user.authenticate_password('notright') # => false
128
+ # user.authenticate_password('mUc3m00RsqyRe') # => user
129
+ define_method("authenticate_#{attribute}") do |unencrypted_password|
130
+ attribute_digest = public_send("#{attribute}_digest")
131
+ BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
132
+ end
133
+
134
+ alias_method :authenticate, :authenticate_password if attribute == :password
74
135
  end
75
136
  end
76
137
  end
@@ -6,8 +6,8 @@ module OmniAuth
6
6
  # user authentication using the same process flow that you
7
7
  # use for external OmniAuth providers.
8
8
  class Identity
9
+ DEFAULT_REGISTRATION_FIELDS = %i[password password_confirmation].freeze
9
10
  include OmniAuth::Strategy
10
-
11
11
  option :fields, %i[name email]
12
12
 
13
13
  # Primary Feature Switches:
@@ -20,6 +20,11 @@ module OmniAuth
20
20
  option :on_registration, nil # See #registration_phase
21
21
  option :on_failed_registration, nil # See #registration_phase
22
22
  option :locate_conditions, ->(req) { { model.auth_key => req['auth_key'] } }
23
+ option :create_identity_link_text, 'Create an Identity'
24
+ option :registration_failure_message, 'One or more fields were invalid'
25
+ option :validation_failure_message, 'Validation failed'
26
+ option :title, 'Identity Verification' # Title for Login Form
27
+ option :registration_form_title, 'Register Identity' # Title for Registration Form
23
28
 
24
29
  def request_phase
25
30
  if options[:on_login]
@@ -65,29 +70,25 @@ module OmniAuth
65
70
  end
66
71
 
67
72
  def registration_phase
68
- attributes = (options[:fields] + %i[password password_confirmation]).each_with_object({}) do |k, h|
73
+ attributes = (options[:fields] + DEFAULT_REGISTRATION_FIELDS).each_with_object({}) do |k, h|
69
74
  h[k] = request[k.to_s]
70
75
  end
71
76
  if model.respond_to?(:column_names) && model.column_names.include?('provider')
72
77
  attributes.reverse_merge!(provider: 'identity')
73
78
  end
74
- @identity = model.new(attributes)
75
-
76
- # on_validation may run a Captcha or other validation mechanism
77
- # Must return true when validation passes, false otherwise
78
- if options[:on_validation] && !options[:on_validation].call(env: env)
79
- if options[:on_failed_registration]
80
- env['omniauth.identity'] = @identity
81
- options[:on_failed_registration].call(env)
79
+ if validating?
80
+ @identity = model.new(attributes)
81
+ env['omniauth.identity'] = @identity
82
+ if valid?
83
+ @identity.save
84
+ registration_result
82
85
  else
83
- validation_message = 'Validation failed'
84
- registration_form(validation_message)
86
+ registration_failure(options[:validation_failure_message])
85
87
  end
86
- elsif @identity.save && @identity.persisted?
87
- env['PATH_INFO'] = callback_path
88
- callback_phase
89
88
  else
90
- show_custom_options_or_default
89
+ @identity = model.create(attributes)
90
+ env['omniauth.identity'] = @identity
91
+ registration_result
91
92
  end
92
93
  end
93
94
 
@@ -120,19 +121,19 @@ module OmniAuth
120
121
 
121
122
  def build_omniauth_login_form
122
123
  OmniAuth::Form.build(
123
- title: (options[:title] || 'Identity Verification'),
124
+ title: options[:title],
124
125
  url: callback_path
125
126
  ) do |f|
126
127
  f.text_field 'Login', 'auth_key'
127
128
  f.password_field 'Password', 'password'
128
129
  if options[:enable_registration]
129
- f.html "<p align='center'><a href='#{registration_path}'>Create an Identity</a></p>"
130
+ f.html "<p align='center'><a href='#{registration_path}'>#{options[:create_identity_link_text]}</a></p>"
130
131
  end
131
132
  end
132
133
  end
133
134
 
134
135
  def build_omniauth_registration_form(validation_message)
135
- OmniAuth::Form.build(title: 'Register Identity') do |f|
136
+ OmniAuth::Form.build(title: options[:registration_form_title]) do |f|
136
137
  f.html "<p style='color:red'>#{validation_message}</p>" if validation_message
137
138
  options[:fields].each do |field|
138
139
  f.text_field field.to_s.capitalize, field.to_s
@@ -142,13 +143,36 @@ module OmniAuth
142
143
  end
143
144
  end
144
145
 
145
- def show_custom_options_or_default
146
+ # Validates the model before it is persisted
147
+ #
148
+ # @return [truthy or falsey] :on_validation option is truthy or falsey
149
+ def validating?
150
+ !!options[:on_validation]
151
+ end
152
+
153
+ # Validates the model before it is persisted
154
+ #
155
+ # @return [true or false] result of :on_validation call
156
+ def valid?
157
+ # on_validation may run a Captcha or other validation mechanism
158
+ # Must return true when validation passes, false otherwise
159
+ !!options[:on_validation].call(env: env)
160
+ end
161
+
162
+ def registration_failure(message)
146
163
  if options[:on_failed_registration]
147
- env['omniauth.identity'] = @identity
148
164
  options[:on_failed_registration].call(env)
149
165
  else
150
- validation_message = 'One or more fields were invalid'
151
- registration_form(validation_message)
166
+ registration_form(message)
167
+ end
168
+ end
169
+
170
+ def registration_result
171
+ if @identity.persisted?
172
+ env['PATH_INFO'] = callback_path
173
+ callback_phase
174
+ else
175
+ registration_failure(options[:registration_failure_message])
152
176
  end
153
177
  end
154
178
  end
@@ -1,123 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ExampleModel
4
- include OmniAuth::Identity::Model
5
- end
6
-
7
3
  RSpec.describe OmniAuth::Identity::Model do
8
- context 'Class Methods' do
9
- subject { ExampleModel }
10
-
11
- describe '.locate' do
12
- it('is abstract') { expect { subject.locate('abc') }.to raise_error(NotImplementedError) }
4
+ before do
5
+ identity_test_klass = Class.new do
6
+ include OmniAuth::Identity::Model
13
7
  end
8
+ stub_const('IdentityTestClass', identity_test_klass)
9
+ end
14
10
 
15
- describe '.authenticate' do
16
- it 'calls locate and then authenticate' do
17
- mocked_instance = double('ExampleModel', authenticate: 'abbadoo')
18
- allow(subject).to receive(:locate).with('email' => 'example').and_return(mocked_instance)
19
- expect(subject.authenticate({ 'email' => 'example' }, 'pass')).to eq('abbadoo')
20
- end
11
+ describe 'Class Methods' do
12
+ subject(:model_klass) { IdentityTestClass }
21
13
 
22
- it 'calls locate with additional scopes when provided' do
23
- mocked_instance = double('ExampleModel', authenticate: 'abbadoo')
24
- allow(subject).to receive(:locate).with('email' => 'example',
25
- 'user_type' => 'admin').and_return(mocked_instance)
26
- expect(subject.authenticate({ 'email' => 'example', 'user_type' => 'admin' }, 'pass')).to eq('abbadoo')
27
- end
14
+ include_context 'model with class methods'
28
15
 
29
- it 'recovers gracefully if locate is nil' do
30
- allow(subject).to receive(:locate).and_return(nil)
31
- expect(subject.authenticate('blah', 'foo')).to be false
16
+ describe '::locate' do
17
+ it('is abstract') do
18
+ expect { model_klass.locate('email' => 'example') }.to raise_error(NotImplementedError)
32
19
  end
33
20
  end
34
21
  end
35
22
 
36
- context 'Instance Methods' do
37
- subject { ExampleModel.new }
23
+ describe 'Instance Methods' do
24
+ subject(:instance) { IdentityTestClass.new }
38
25
 
39
- describe '#authenticate' do
40
- it('is abstract') { expect { subject.authenticate('abc') }.to raise_error(NotImplementedError) }
41
- end
26
+ include_context 'instance with instance methods'
42
27
 
43
- describe '#uid' do
44
- it 'defaults to #id' do
45
- allow(subject).to receive(:respond_to?).with(:id).and_return(true)
46
- allow(subject).to receive(:id).and_return 'wakka-do'
47
- expect(subject.uid).to eq('wakka-do')
48
- end
49
-
50
- it 'stringifies it' do
51
- allow(subject).to receive(:id).and_return 123
52
- expect(subject.uid).to eq('123')
53
- end
54
-
55
- it 'raises NotImplementedError if #id is not defined' do
56
- allow(subject).to receive(:respond_to?).with(:id).and_return(false)
57
- expect { subject.uid }.to raise_error(NotImplementedError)
58
- end
28
+ describe '#authenticate' do
29
+ it('is abstract') { expect { instance.authenticate('my-password') }.to raise_error(NotImplementedError) }
59
30
  end
60
31
 
61
32
  describe '#auth_key' do
62
- it 'defaults to #email' do
63
- allow(subject).to receive(:respond_to?).with(:email).and_return(true)
64
- allow(subject).to receive(:email).and_return('bob@bob.com')
65
- expect(subject.auth_key).to eq('bob@bob.com')
66
- end
67
-
68
- it 'uses the class .auth_key' do
69
- subject.class.auth_key 'login'
70
- allow(subject).to receive(:login).and_return 'bob'
71
- expect(subject.auth_key).to eq('bob')
72
- subject.class.auth_key nil
73
- end
74
-
75
33
  it 'raises a NotImplementedError if the auth_key method is not defined' do
76
- expect { subject.auth_key }.to raise_error(NotImplementedError)
34
+ expect { instance.auth_key }.to raise_error(NotImplementedError)
77
35
  end
78
36
  end
79
37
 
80
38
  describe '#auth_key=' do
81
- it 'defaults to setting email' do
82
- allow(subject).to receive(:respond_to?).with(:email=).and_return(true)
83
- expect(subject).to receive(:email=).with 'abc'
84
-
85
- subject.auth_key = 'abc'
86
- end
87
-
88
- it 'uses a custom .auth_key if one is provided' do
89
- subject.class.auth_key 'login'
90
- allow(subject).to receive(:respond_to?).with(:login=).and_return(true)
91
- expect(subject).to receive(:login=).with('abc')
92
-
93
- subject.auth_key = 'abc'
94
- end
95
-
96
- it 'raises a NotImplementedError if the autH_key method is not defined' do
97
- expect { subject.auth_key = 'broken' }.to raise_error(NotImplementedError)
98
- end
99
- end
100
-
101
- describe '#info' do
102
- it 'includes attributes that are set' do
103
- allow(subject).to receive(:name).and_return('Bob Bobson')
104
- allow(subject).to receive(:nickname).and_return('bob')
105
-
106
- expect(subject.info).to eq({
107
- 'name' => 'Bob Bobson',
108
- 'nickname' => 'bob'
109
- })
110
- end
111
-
112
- it 'automaticallies set name off of nickname' do
113
- allow(subject).to receive(:nickname).and_return('bob')
114
- subject.info['name'] == 'bob'
115
- end
116
-
117
- it 'does not overwrite a provided name' do
118
- allow(subject).to receive(:name).and_return('Awesome Dude')
119
- allow(subject).to receive(:first_name).and_return('Frank')
120
- expect(subject.info['name']).to eq('Awesome Dude')
39
+ it 'raises a NotImplementedError if the auth_key method is not defined' do
40
+ expect { instance.auth_key = 'broken' }.to raise_error(NotImplementedError)
121
41
  end
122
42
  end
123
43
  end