Permalink
Browse files

Use HMAC on tokens stored in the DB

  • Loading branch information...
josevalim committed Aug 5, 2013
1 parent 3264802 commit 143794d701bcd7b8c900c5bb8a216026c3c68afc
@@ -1,15 +1,18 @@
class Devise::Mailer < Devise.parent_mailer.constantize
include Devise::Mailers::Helpers
- def confirmation_instructions(record, opts={})
+ def confirmation_instructions(record, token, opts={})
+ @token = token
devise_mail(record, :confirmation_instructions, opts)
end
- def reset_password_instructions(record, opts={})
+ def reset_password_instructions(record, token, opts={})
+ @token = token
devise_mail(record, :reset_password_instructions, opts)
end
- def unlock_instructions(record, opts={})
+ def unlock_instructions(record, token, opts={})
+ @token = token
devise_mail(record, :unlock_instructions, opts)
end
end
@@ -2,4 +2,4 @@
<p>You can confirm your account email through the link below:</p>
-<p><%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %></p>
+<p><%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @token) %></p>
@@ -2,7 +2,7 @@
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
-<p><%= link_to 'Change my password', edit_password_url(@resource, :reset_password_token => @resource.reset_password_token) %></p>
+<p><%= link_to 'Change my password', edit_password_url(@resource, :reset_password_token => @token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>
@@ -4,4 +4,4 @@
<p>Click the link below to unlock your account:</p>
-<p><%= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @resource.unlock_token) %></p>
+<p><%= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @token) %></p>
View
@@ -14,6 +14,7 @@ module Devise
autoload :ParameterSanitizer, 'devise/parameter_sanitizer'
autoload :TestHelpers, 'devise/test_helpers'
autoload :TimeInflector, 'devise/time_inflector'
+ autoload :TokenGenerator, 'devise/token_generator'
module Controllers
autoload :Helpers, 'devise/controllers/helpers'
@@ -49,6 +50,10 @@ module Strategies
mattr_accessor :secret_key
@@secret_key = nil
+ # Secret key used by the key generator
+ mattr_accessor :token_generator
+ @@token_generator = nil
+
# Custom domain or key for cookies. Not set by default
mattr_accessor :rememberable_options
@@rememberable_options = {}
View
@@ -83,11 +83,8 @@ def devise(*modules)
devise_modules_hook! do
include Devise::Models::Authenticatable
- selected_modules.each do |m|
- if m == :encryptable && !(defined?(Devise::Models::Encryptable))
- warn "[DEVISE] You're trying to include :encryptable in your model but it is not bundled with the Devise gem anymore. Please add `devise-encryptable` to your Gemfile to proceed.\n"
- end
+ selected_modules.each do |m|
mod = Devise::Models.const_get(m.to_s.classify)
if mod.const_defined?("ClassMethods")
@@ -144,20 +144,20 @@ def devise_mailer
#
# protected
#
- # def send_devise_notification(notification, opts = {})
- # # if the record is new or changed then delay the
+ # def send_devise_notification(notification, *args)
+ # # If the record is new or changed then delay the
# # delivery until the after_commit callback otherwise
# # send now because after_commit will not be called.
# if new_record? || changed?
- # pending_notifications << [notification, opts]
+ # pending_notifications << [notification, args]
# else
- # devise_mailer.send(notification, self, opts).deliver
+ # devise_mailer.send(notification, self, *args).deliver
# end
# end
#
# def send_pending_notifications
- # pending_notifications.each do |n, opts|
- # devise_mailer.send(n, self, opts).deliver
+ # pending_notifications.each do |notification, args|
+ # devise_mailer.send(notification, self, *args).deliver
# end
#
# # Empty the pending notifications array because the
@@ -171,8 +171,8 @@ def devise_mailer
# end
# end
#
- def send_devise_notification(notification, opts={})
- devise_mailer.send(notification, self, opts).deliver
+ def send_devise_notification(notification, *args)
+ devise_mailer.send(notification, self, *args).deliver
end
def downcase_keys
@@ -279,14 +279,6 @@ def find_or_initialize_with_errors(required_attributes, attributes, error=:inval
def devise_parameter_filter
@devise_parameter_filter ||= Devise::ParameterFilter.new(case_insensitive_keys, strip_whitespace_keys)
end
-
- # Generate a token by looping and ensuring does not already exist.
- def generate_token(column)
- loop do
- token = Devise.friendly_token
- break token unless to_adapter.find_first({ column => token })
- end
- end
end
end
end
@@ -40,9 +40,10 @@ module Confirmable
end
def initialize(*args, &block)
- @bypass_postpone = false
+ @bypass_confirmation_postpone = false
@reconfirmation_required = false
@skip_confirmation_notification = false
+ @raw_confirmation_token = nil
super
end
@@ -93,10 +94,12 @@ def pending_reconfirmation?
# Send confirmation instructions by email
def send_confirmation_instructions
- ensure_confirmation_token!
+ unless @raw_confirmation_token
+ generate_confirmation_token!
+ end
opts = pending_reconfirmation? ? { :to => unconfirmed_email } : { }
- send_devise_notification(:confirmation_instructions, opts)
+ send_devise_notification(:confirmation_instructions, @raw_confirmation_token, opts)
end
def send_reconfirmation_instructions
@@ -109,17 +112,11 @@ def send_reconfirmation_instructions
# Resend confirmation token.
# Regenerates the token if the period is expired.
- def resend_confirmation_token
+ def resend_confirmation_instructions
pending_any_confirmation do
- regenerate_confirmation_token! if confirmation_period_expired?
send_confirmation_instructions
end
end
-
- # Generate a confirmation token unless already exists and save the record.
- def ensure_confirmation_token!
- generate_confirmation_token! if should_generate_confirmation_token?
- end
# Overwrites active_for_authentication? for confirmation
# by verifying whether a user is active to sign in or not. If the user
@@ -149,19 +146,16 @@ def skip_confirmation_notification!
# If you don't want reconfirmation to be sent, neither a code
# to be generated, call skip_reconfirmation!
def skip_reconfirmation!
- @bypass_postpone = true
+ @bypass_confirmation_postpone = true
end
protected
- def should_generate_confirmation_token?
- confirmation_token.nil? || confirmation_period_expired?
- end
# A callback method used to deliver confirmation
# instructions on creation. This can be overriden
# in models to map to a nice sign up e-mail.
def send_on_create_confirmation_instructions
- send_devise_notification(:confirmation_instructions)
+ send_confirmation_instructions
end
# Callback to overwrite if confirmation is required or not.
@@ -221,26 +215,19 @@ def pending_any_confirmation
end
end
- # Generates a new random token for confirmation, and stores the time
- # this token is being generated
+ # Generates a new random token for confirmation, and stores
+ # the time this token is being generated
def generate_confirmation_token
- self.confirmation_token = self.class.confirmation_token
+ raw, enc = Devise.token_generator.generate(self.class, :confirmation_token)
+ @raw_confirmation_token = raw
+ self.confirmation_token = enc
self.confirmation_sent_at = Time.now.utc
end
def generate_confirmation_token!
generate_confirmation_token && save(:validate => false)
end
- # Regenerates a new token.
- def regenerate_confirmation_token
- generate_confirmation_token
- end
-
- def regenerate_confirmation_token!
- regenerate_confirmation_token && save(:validate => false)
- end
-
def after_password_reset
super
confirm! unless confirmed?
@@ -250,12 +237,12 @@ def postpone_email_change_until_confirmation_and_regenerate_confirmation_token
@reconfirmation_required = true
self.unconfirmed_email = self.email
self.email = self.email_was
- regenerate_confirmation_token
+ generate_confirmation_token
end
def postpone_email_change?
- postpone = self.class.reconfirmable && email_changed? && !@bypass_postpone && !self.email.blank?
- @bypass_postpone = false
+ postpone = self.class.reconfirmable && email_changed? && !@bypass_confirmation_postpone && !self.email.blank?
+ @bypass_confirmation_postpone = false
postpone
end
@@ -280,7 +267,7 @@ def send_confirmation_instructions(attributes={})
unless confirmable.try(:persisted?)
confirmable = find_or_initialize_with_errors(confirmation_keys, attributes, :not_found)
end
- confirmable.resend_confirmation_token if confirmable.persisted?
+ confirmable.resend_confirmation_instructions if confirmable.persisted?
confirmable
end
@@ -289,16 +276,16 @@ def send_confirmation_instructions(attributes={})
# If the user is already confirmed, create an error for the user
# Options must have the confirmation_token
def confirm_by_token(confirmation_token)
+ original_token = confirmation_token
+ confirmation_token = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
+ unless confirmable.persisted?
+ confirmable = find_or_initialize_with_error_by(:confirmation_token, original_token)
+ end
confirmable.confirm! if confirmable.persisted?
confirmable
end
- # Generate a token checking if one does not already exist in the database.
- def confirmation_token
- generate_token(:confirmation_token)
- end
-
# Find a record for confirmation by unconfirmed email field
def find_by_unconfirmed_email_with_errors(attributes = {})
unconfirmed_required_attributes = confirmation_keys.map { |k| k == :email ? :unconfirmed_email : k }
@@ -38,7 +38,6 @@ def lock_access!
self.locked_at = Time.now.utc
if unlock_strategy_enabled?(:email)
- generate_unlock_token!
send_unlock_instructions
else
save(:validate => false)
@@ -60,11 +59,15 @@ def access_locked?
# Send unlock instructions by email
def send_unlock_instructions
- send_devise_notification(:unlock_instructions)
+ raw, enc = Devise.token_generator.generate(self.class, :unlock_token)
+ self.unlock_token = enc
+ self.save(:validate => false)
+ send_devise_notification(:unlock_instructions, raw, {})
+ raw
end
# Resend the unlock instructions if the user is locked.
- def resend_unlock_token
+ def resend_unlock_instructions
if_access_locked { send_unlock_instructions }
end
@@ -122,15 +125,6 @@ def attempts_exceeded?
self.failed_attempts > self.class.maximum_attempts
end
- # Generates unlock token
- def generate_unlock_token
- self.unlock_token = self.class.unlock_token
- end
-
- def generate_unlock_token!
- generate_unlock_token && save(:validate => false)
- end
-
# Tells if the lock is expired if :time unlock strategy is active
def lock_expired?
if unlock_strategy_enabled?(:time)
@@ -158,7 +152,7 @@ module ClassMethods
# Options must contain the user's unlock keys
def send_unlock_instructions(attributes={})
lockable = find_or_initialize_with_errors(unlock_keys, attributes, :not_found)
- lockable.resend_unlock_token if lockable.persisted?
+ lockable.resend_unlock_instructions if lockable.persisted?
lockable
end
@@ -167,7 +161,14 @@ def send_unlock_instructions(attributes={})
# If the user is not locked, creates an error for the user
# Options must have the unlock_token
def unlock_access_by_token(unlock_token)
+ original_token = unlock_token
+ unlock_token = Devise.token_generator.digest(self, :unlock_token, unlock_token)
+
lockable = find_or_initialize_with_error_by(:unlock_token, unlock_token)
+ unless lockable.persisted?
+ lockable = find_or_initialize_with_error_by(:unlock_token, original_token)
+ end
+
lockable.unlock_access! if lockable.persisted?
lockable
end
@@ -182,10 +183,6 @@ def lock_strategy_enabled?(strategy)
self.lock_strategy == strategy
end
- def unlock_token
- Devise.friendly_token
- end
-
Devise::Models.config(self, :maximum_attempts, :lock_strategy, :unlock_strategy, :unlock_in, :unlock_keys)
end
end
Oops, something went wrong.

0 comments on commit 143794d

Please sign in to comment.