Skip to content

Commit

Permalink
Merge pull request #1975 from promisedlandt/email-token-expiration
Browse files Browse the repository at this point in the history
Email token expiration
  • Loading branch information
rodrigoflores committed Jul 22, 2012
2 parents 01d3ed7 + dcada8f commit 6a37945
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 11 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ group :test do
platforms :mri_18 do
gem "ruby-debug", ">= 0.10.3"
end
platforms :mri_19 do
gem 'debugger'
end
end

platforms :jruby do
Expand Down
8 changes: 8 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ GEM
bson_ext (1.3.1)
builder (3.0.0)
columnize (0.3.5)
debugger (1.1.4)
columnize (>= 0.3.1)
debugger-linecache (~> 1.1.1)
debugger-ruby_core_source (~> 1.1.3)
debugger-linecache (1.1.2)
debugger-ruby_core_source (>= 1.1.1)
debugger-ruby_core_source (1.1.3)
erubis (2.7.0)
faraday (0.7.5)
addressable (~> 2.2.6)
Expand Down Expand Up @@ -149,6 +156,7 @@ DEPENDENCIES
activerecord-jdbc-adapter
activerecord-jdbcsqlite3-adapter
bson_ext (~> 1.3.0)
debugger
devise!
jruby-openssl
mocha
Expand Down
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ en:
not_saved:
one: "1 error prohibited this %{resource} from being saved:"
other: "%{count} errors prohibited this %{resource} from being saved:"
confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"

devise:
failure:
Expand Down
6 changes: 5 additions & 1 deletion lib/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ module Strategies
mattr_accessor :allow_unconfirmed_access_for
@@allow_unconfirmed_access_for = 0.days

# Time interval the confirmation token is valid. nil = unlimited
mattr_accessor :confirm_within
@@confirm_within = nil

# Defines which key will be used when confirming an account.
mattr_accessor :confirmation_keys
@@confirmation_keys = [ :email ]
Expand Down Expand Up @@ -199,7 +203,7 @@ module Strategies
# to provide custom routes.
mattr_accessor :router_name
@@router_name = nil

# Set the omniauth path prefix so it can be overriden when
# Devise is used in a mountable engine
mattr_accessor :omniauth_path_prefix
Expand Down
38 changes: 34 additions & 4 deletions lib/devise/models/confirmable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ module Models
# db field to be setup (t.reconfirmable in migrations). Until confirmed new email is
# stored in unconfirmed email column, and copied to email column on successful
# confirmation.
# * +confirm_within+: the time before a sent confirmation token becomes invalid.
# You can use this to force the user to confirm within a set period of time.
#
# == Examples
#
Expand All @@ -28,6 +30,7 @@ module Models
#
module Confirmable
extend ActiveSupport::Concern
include ActionView::Helpers::DateHelper

included do
before_create :generate_confirmation_token, :if => :confirmation_required?
Expand Down Expand Up @@ -118,7 +121,6 @@ def headers_for(action)
end
headers
end

protected

# A callback method used to deliver confirmation
Expand Down Expand Up @@ -156,12 +158,40 @@ def confirmation_period_valid?
confirmation_sent_at && confirmation_sent_at.utc >= self.class.allow_unconfirmed_access_for.ago
end


# Checks if the user confirmation happens before the token becomes invalid
# Examples:
#
# # confirm_within = 3.days and confirmation_sent_at = 2.days.ago
# confirmation_period_expired? # returns false
#
# # confirm_within = 3.days and confirmation_sent_at = 4.days.ago
# confirmation_period_expired? # returns true
#
# # confirm_within = nil
# confirmation_period_expired? # will always return false
#
def confirmation_period_expired?
if @confirmation_period_expired.nil?
@confirmation_period_expired = self.class.confirm_within && (Time.now > self.confirmation_sent_at + self.class.confirm_within )
@confirmation_period_expired
else
@confirmation_period_expired
end
end

# Checks whether the record requires any confirmation.
def pending_any_confirmation
if !confirmed? || pending_reconfirmation?
@confirmation_period_expired = confirmation_period_expired?

if (!confirmed? || pending_reconfirmation?) && !@confirmation_period_expired
yield
else
self.errors.add(:email, :already_confirmed)
if @confirmation_period_expired
self.errors.add(:email, :confirmation_period_expired, :period => time_ago_in_words(self.class.confirm_within.ago))
else
self.errors.add(:email, :already_confirmed)
end
false
end
end
Expand Down Expand Up @@ -235,7 +265,7 @@ def find_by_unconfirmed_email_with_errors(attributes = {})
find_or_initialize_with_errors(unconfirmed_required_attributes, unconfirmed_attributes, :not_found)
end

Devise::Models.config(self, :allow_unconfirmed_access_for, :confirmation_keys, :reconfirmable)
Devise::Models.config(self, :allow_unconfirmed_access_for, :confirmation_keys, :reconfirmable, :confirm_within)
end
end
end
Expand Down
10 changes: 9 additions & 1 deletion lib/generators/templates/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@
# the user cannot access the website without confirming his account.
# config.allow_unconfirmed_access_for = 2.days

# A period that the user is allowed to confirm their account before their token
# becomes invalid. For example, if set to 3.days, the user can confirm their account
# within 3 days after the mail was sent, but on the fourth day their account can't be
# confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take before
# confirming their account.
# config.confirm_within = 3.days

# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email
# db field (see migrations). Until confirmed new email is stored in
Expand Down Expand Up @@ -125,7 +133,7 @@
# The time you want to timeout the user session without activity. After this
# time the user will be asked for credentials again. Default is 30 minutes.
# config.timeout_in = 30.minutes

# If true, expires auth token on session timeout.
# config.expire_auth_token_on_timeout = false

Expand Down
32 changes: 28 additions & 4 deletions test/integration/confirmable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def resend_confirmation
fill_in 'email', :with => user.email
click_button 'Resend confirmation instructions'
end

test 'user should be able to request a new confirmation' do
resend_confirmation

Expand Down Expand Up @@ -50,6 +50,30 @@ def resend_confirmation
assert user.reload.confirmed?
end

test 'user with valid confirmation token should not be able to confirm an account after the token has expired' do
swap Devise, :confirm_within => 3.days do
user = create_user(:confirm => false, :confirmation_sent_at => 4.days.ago)
assert_not user.confirmed?
visit_user_confirmation_with_token(user.confirmation_token)

assert_have_selector '#error_explanation'
assert_contain /needs to be confirmed within 3 days/
assert_not user.reload.confirmed?
end
end

test 'user with valid confirmation token should be able to confirm an account before the token has expired' do
swap Devise, :confirm_within => 3.days do
user = create_user(:confirm => false, :confirmation_sent_at => 2.days.ago)
assert_not user.confirmed?
visit_user_confirmation_with_token(user.confirmation_token)

assert_contain 'Your account was successfully confirmed.'
assert_current_url '/'
assert user.reload.confirmed?
end
end

test 'user should be redirected to a custom path after confirmation' do
Devise::ConfirmationsController.any_instance.stubs(:after_confirmation_path_for).returns("/?custom=1")

Expand Down Expand Up @@ -239,19 +263,19 @@ def visit_admin_confirmation_with_token(confirmation_token)
assert admin.reload.confirmed?
assert_not admin.reload.pending_reconfirmation?
end

test 'admin with previously valid confirmation token should not be able to confirm email after email changed again' do
admin = create_admin
admin.update_attributes(:email => 'first_test@example.com')
assert_equal 'first_test@example.com', admin.unconfirmed_email
confirmation_token = admin.confirmation_token
admin.update_attributes(:email => 'second_test@example.com')
assert_equal 'second_test@example.com', admin.unconfirmed_email

visit_admin_confirmation_with_token(confirmation_token)
assert_have_selector '#error_explanation'
assert_contain /Confirmation token(.*)invalid/

visit_admin_confirmation_with_token(admin.confirmation_token)
assert_contain 'Your account was successfully confirmed.'
assert_current_url '/admin_area/home'
Expand Down
24 changes: 24 additions & 0 deletions test/models/confirmable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,30 @@ def setup
assert_equal "can't be blank", confirm_user.errors[:username].join
end
end

def confirm_user_by_token_with_confirmation_sent_at(confirmation_sent_at)
user = create_user
user.update_attribute(:confirmation_sent_at, confirmation_sent_at)
confirmed_user = User.confirm_by_token(user.confirmation_token)
assert_equal confirmed_user, user
user.reload.confirmed?
end

test 'should accept confirmation email token even after 5 years when no expiration is set' do
assert confirm_user_by_token_with_confirmation_sent_at(5.years.ago)
end

test 'should accept confirmation email token after 2 days when expiration is set to 3 days' do
swap Devise, :confirm_within => 3.days do
assert confirm_user_by_token_with_confirmation_sent_at(2.days.ago)
end
end

test 'should not accept confirmation email token after 4 days when expiration is set to 3 days' do
swap Devise, :confirm_within => 3.days do
assert_not confirm_user_by_token_with_confirmation_sent_at(4.days.ago)
end
end
end

class ReconfirmableTest < ActiveSupport::TestCase
Expand Down
2 changes: 1 addition & 1 deletion test/rails_app/lib/shared_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module SharedUser
:trackable, :validatable, :omniauthable

attr_accessor :other_key
attr_accessible :username, :email, :password, :password_confirmation, :remember_me
attr_accessible :username, :email, :password, :password_confirmation, :remember_me, :confirmation_sent_at

# They need to be included after Devise is called.
extend ExtendMethods
Expand Down
1 change: 1 addition & 0 deletions test/support/integration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def create_user(options={})
:password_confirmation => options[:password] || '12345678',
:created_at => Time.now.utc
)
user.update_attribute(:confirmation_sent_at, options[:confirmation_sent_at]) if options[:confirmation_sent_at]
user.confirm! unless options[:confirm] == false
user.lock_access! if options[:locked] == true
user
Expand Down

0 comments on commit 6a37945

Please sign in to comment.