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.
@@ -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
@@ -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
@@ -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 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,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
- option :enable_login, true # See #other_phase documentation
13
- option :on_login, nil
14
- option :on_registration, nil
15
- option :on_failed_registration, nil
16
- option :enable_registration, true
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
- OmniAuth::Form.build(
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
- OmniAuth::Form.build(title: 'Register Identity') do |f|
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] + %i[password password_confirmation]).each_with_object({}) do |k, h|
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.create(attributes)
85
- if @identity.persisted?
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
- options[:on_failed_registration].call(env)
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
- registration_form
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