omniauth-identity 3.0.2 → 3.0.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|