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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -0
- data/README.md +94 -12
- data/lib/omniauth-identity/version.rb +1 -1
- data/lib/omniauth/identity.rb +2 -1
- data/lib/omniauth/identity/model.rb +92 -29
- data/lib/omniauth/identity/models/active_record.rb +2 -2
- data/lib/omniauth/identity/models/couch_potato.rb +6 -0
- data/lib/omniauth/identity/models/mongoid.rb +1 -0
- data/lib/omniauth/identity/models/{no_brainer.rb → nobrainer.rb} +1 -0
- data/lib/omniauth/identity/models/sequel.rb +48 -0
- data/lib/omniauth/identity/secure_password.rb +98 -37
- data/lib/omniauth/strategies/identity.rb +47 -23
- data/spec/omniauth/identity/model_spec.rb +19 -99
- data/spec/omniauth/identity/models/active_record_spec.rb +19 -10
- data/spec/omniauth/identity/models/sequel_spec.rb +38 -0
- data/spec/omniauth/strategies/identity_spec.rb +123 -5
- data/spec/spec_helper.rb +16 -4
- data/spec/support/shared_contexts/instance_with_instance_methods.rb +89 -0
- data/spec/support/shared_contexts/model_with_class_methods.rb +29 -0
- data/spec/support/shared_contexts/persistable_model.rb +24 -0
- metadata +29 -54
- data/spec/omniauth/identity/models/couch_potato_spec.rb +0 -21
- data/spec/omniauth/identity/models/mongoid_spec.rb +0 -28
- data/spec/omniauth/identity/models/no_brainer_spec.rb +0 -17
@@ -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
|
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
|
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
|
-
#
|
22
|
-
#
|
23
|
-
#
|
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(:
|
33
|
-
# user.save
|
34
|
-
# user.password =
|
35
|
-
# user.save
|
36
|
-
# user.password_confirmation =
|
37
|
-
# user.save
|
38
|
-
# user.
|
39
|
-
# user.
|
40
|
-
#
|
41
|
-
#
|
42
|
-
|
43
|
-
|
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
|
-
|
46
|
-
validates_presence_of :password_digest
|
82
|
+
include InstanceMethodsOnActivation.new(attribute)
|
47
83
|
|
48
|
-
|
84
|
+
if validations
|
85
|
+
include ActiveModel::Validations
|
49
86
|
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
69
|
-
|
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] +
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
-
|
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:
|
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}'
|
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:
|
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
|
-
|
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
|
-
|
151
|
-
|
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
|
-
|
9
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
expect
|
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
|
-
|
37
|
-
subject {
|
23
|
+
describe 'Instance Methods' do
|
24
|
+
subject(:instance) { IdentityTestClass.new }
|
38
25
|
|
39
|
-
|
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 '#
|
44
|
-
it '
|
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 {
|
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 '
|
82
|
-
|
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
|