omniauth-identity 3.0.2 → 3.0.7
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 +74 -0
- data/README.md +117 -42
- data/lib/omniauth-identity/version.rb +1 -1
- data/lib/omniauth/identity.rb +2 -0
- data/lib/omniauth/identity/model.rb +93 -30
- 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/nobrainer.rb +31 -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 +103 -32
- data/spec/omniauth/identity/model_spec.rb +19 -99
- data/spec/omniauth/identity/models/active_record_spec.rb +20 -11
- data/spec/omniauth/identity/models/sequel_spec.rb +38 -0
- data/spec/omniauth/strategies/identity_spec.rb +131 -16
- 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 -37
- data/spec/omniauth/identity/models/couch_potato_spec.rb +0 -19
- data/spec/omniauth/identity/models/mongoid_spec.rb +0 -26
@@ -6,8 +6,8 @@ module OmniAuth
|
|
6
6
|
module Identity
|
7
7
|
module Models
|
8
8
|
class ActiveRecord < ::ActiveRecord::Base
|
9
|
-
include OmniAuth::Identity::Model
|
10
|
-
include OmniAuth::Identity::SecurePassword
|
9
|
+
include ::OmniAuth::Identity::Model
|
10
|
+
include ::OmniAuth::Identity::SecurePassword
|
11
11
|
|
12
12
|
self.abstract_class = true
|
13
13
|
has_secure_password
|
@@ -6,6 +6,8 @@ module OmniAuth
|
|
6
6
|
module Identity
|
7
7
|
module Models
|
8
8
|
# can not be named CouchPotato since there is a class with that name
|
9
|
+
# NOTE: CouchPotato is based on ActiveModel.
|
10
|
+
# NOTE: CouchPotato::Persistence must be included before OmniAuth::Identity::Models::CouchPotatoModule
|
9
11
|
module CouchPotatoModule
|
10
12
|
def self.included(base)
|
11
13
|
base.class_eval do
|
@@ -22,6 +24,10 @@ module OmniAuth
|
|
22
24
|
def self.locate(search_hash)
|
23
25
|
where(search_hash).first
|
24
26
|
end
|
27
|
+
|
28
|
+
def save
|
29
|
+
CouchPotato.database.save(self)
|
30
|
+
end
|
25
31
|
end
|
26
32
|
end
|
27
33
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nobrainer'
|
4
|
+
|
5
|
+
module OmniAuth
|
6
|
+
module Identity
|
7
|
+
module Models
|
8
|
+
# http://nobrainer.io/ an ORM for RethinkDB
|
9
|
+
# NOTE: NoBrainer is based on ActiveModel.
|
10
|
+
module NoBrainer
|
11
|
+
def self.included(base)
|
12
|
+
base.class_eval do
|
13
|
+
include ::OmniAuth::Identity::Model
|
14
|
+
include ::OmniAuth::Identity::SecurePassword
|
15
|
+
|
16
|
+
has_secure_password
|
17
|
+
|
18
|
+
def self.auth_key=(key)
|
19
|
+
super
|
20
|
+
validates_uniqueness_of key, case_sensitive: false
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.locate(search_hash)
|
24
|
+
where(search_hash).first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -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,30 +6,31 @@ 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
|
-
|
13
|
-
|
14
|
-
option :
|
15
|
-
option :
|
16
|
-
|
12
|
+
|
13
|
+
# Primary Feature Switches:
|
14
|
+
option :enable_registration, true # See #other_phase and #request_phase
|
15
|
+
option :enable_login, true # See #other_phase
|
16
|
+
|
17
|
+
# Customization Options:
|
18
|
+
option :on_login, nil # See #request_phase
|
19
|
+
option :on_validation, nil # See #registration_phase
|
20
|
+
option :on_registration, nil # See #registration_phase
|
21
|
+
option :on_failed_registration, nil # See #registration_phase
|
17
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
|
18
28
|
|
19
29
|
def request_phase
|
20
30
|
if options[:on_login]
|
21
31
|
options[:on_login].call(env)
|
22
32
|
else
|
23
|
-
|
24
|
-
title: (options[:title] || 'Identity Verification'),
|
25
|
-
url: callback_path
|
26
|
-
) do |f|
|
27
|
-
f.text_field 'Login', 'auth_key'
|
28
|
-
f.password_field 'Password', 'password'
|
29
|
-
if options[:enable_registration]
|
30
|
-
f.html "<p align='center'><a href='#{registration_path}'>Create an Identity</a></p>"
|
31
|
-
end
|
32
|
-
end.to_response
|
33
|
+
build_omniauth_login_form.to_response
|
33
34
|
end
|
34
35
|
end
|
35
36
|
|
@@ -60,36 +61,32 @@ module OmniAuth
|
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
63
|
-
def registration_form
|
64
|
+
def registration_form(validation_message = nil)
|
64
65
|
if options[:on_registration]
|
65
66
|
options[:on_registration].call(env)
|
66
67
|
else
|
67
|
-
|
68
|
-
options[:fields].each do |field|
|
69
|
-
f.text_field field.to_s.capitalize, field.to_s
|
70
|
-
end
|
71
|
-
f.password_field 'Password', 'password'
|
72
|
-
f.password_field 'Confirm Password', 'password_confirmation'
|
73
|
-
end.to_response
|
68
|
+
build_omniauth_registration_form(validation_message).to_response
|
74
69
|
end
|
75
70
|
end
|
76
71
|
|
77
72
|
def registration_phase
|
78
|
-
attributes = (options[:fields] +
|
73
|
+
attributes = (options[:fields] + DEFAULT_REGISTRATION_FIELDS).each_with_object({}) do |k, h|
|
79
74
|
h[k] = request[k.to_s]
|
80
75
|
end
|
81
76
|
if model.respond_to?(:column_names) && model.column_names.include?('provider')
|
82
77
|
attributes.reverse_merge!(provider: 'identity')
|
83
78
|
end
|
84
|
-
@identity = model.
|
85
|
-
if
|
86
|
-
env['PATH_INFO'] = callback_path
|
87
|
-
callback_phase
|
88
|
-
elsif options[:on_failed_registration]
|
79
|
+
@identity = model.new(attributes)
|
80
|
+
if saving_instead_of_creating?
|
89
81
|
env['omniauth.identity'] = @identity
|
90
|
-
|
82
|
+
if !validating? || valid?
|
83
|
+
@identity.save
|
84
|
+
registration_result
|
85
|
+
else
|
86
|
+
registration_failure(options[:validation_failure_message])
|
87
|
+
end
|
91
88
|
else
|
92
|
-
|
89
|
+
deprecated_registration(attributes)
|
93
90
|
end
|
94
91
|
end
|
95
92
|
|
@@ -117,6 +114,80 @@ module OmniAuth
|
|
117
114
|
def model
|
118
115
|
options[:model] || ::Identity
|
119
116
|
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def build_omniauth_login_form
|
121
|
+
OmniAuth::Form.build(
|
122
|
+
title: options[:title],
|
123
|
+
url: callback_path
|
124
|
+
) do |f|
|
125
|
+
f.text_field 'Login', 'auth_key'
|
126
|
+
f.password_field 'Password', 'password'
|
127
|
+
if options[:enable_registration]
|
128
|
+
f.html "<p align='center'><a href='#{registration_path}'>#{options[:create_identity_link_text]}</a></p>"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def build_omniauth_registration_form(validation_message)
|
134
|
+
OmniAuth::Form.build(title: options[:registration_form_title]) do |f|
|
135
|
+
f.html "<p style='color:red'>#{validation_message}</p>" if validation_message
|
136
|
+
options[:fields].each do |field|
|
137
|
+
f.text_field field.to_s.capitalize, field.to_s
|
138
|
+
end
|
139
|
+
f.password_field 'Password', 'password'
|
140
|
+
f.password_field 'Confirm Password', 'password_confirmation'
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def saving_instead_of_creating?
|
145
|
+
@identity.respond_to?(:save) && @identity.respond_to?(:persisted?)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Validates the model before it is persisted
|
149
|
+
#
|
150
|
+
# @return [truthy or falsey] :on_validation option is truthy or falsey
|
151
|
+
def validating?
|
152
|
+
!!options[:on_validation]
|
153
|
+
end
|
154
|
+
|
155
|
+
# Validates the model before it is persisted
|
156
|
+
#
|
157
|
+
# @return [true or false] result of :on_validation call
|
158
|
+
def valid?
|
159
|
+
# on_validation may run a Captcha or other validation mechanism
|
160
|
+
# Must return true when validation passes, false otherwise
|
161
|
+
!!options[:on_validation].call(env: env)
|
162
|
+
end
|
163
|
+
|
164
|
+
def registration_failure(message)
|
165
|
+
if options[:on_failed_registration]
|
166
|
+
options[:on_failed_registration].call(env)
|
167
|
+
else
|
168
|
+
registration_form(message)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def registration_result
|
173
|
+
if @identity.persisted?
|
174
|
+
env['PATH_INFO'] = callback_path
|
175
|
+
callback_phase
|
176
|
+
else
|
177
|
+
registration_failure(options[:registration_failure_message])
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def deprecated_registration(attributes)
|
182
|
+
warn <<~CREATEDEP
|
183
|
+
[DEPRECATION] Please define '#{model.class}#save'.
|
184
|
+
Behavior based on '#{model.class}.create' will be removed in omniauth-identity v4.0.
|
185
|
+
See lib/omniauth/identity/model.rb
|
186
|
+
CREATEDEP
|
187
|
+
@identity = model.create(attributes)
|
188
|
+
env['omniauth.identity'] = @identity
|
189
|
+
registration_result
|
190
|
+
end
|
120
191
|
end
|
121
192
|
end
|
122
193
|
end
|