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.
@@ -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