Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disallow confirmation token reuse #98

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/concerns/authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ def user_signed_in?
end

def store_location
session[:user_return_to] = request.original_url if request.get? && request.local?
session[:user_return_to] = request.original_url if request.get?
end
end
11 changes: 10 additions & 1 deletion app/controllers/confirmations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ def create
end

def edit
@user = User.find_signed(params[:confirmation_token], purpose: :confirm_email)
expected_email = params[:email].strip.downcase

# It's possible that the email address that we're confirming is the
# primary "email" field or the secondary "unconfirmed_email" field.
# We need to check both fields to find the user, but will only check
# the primary field if the secondary field is null.
@user = (User.where(unconfirmed_email: expected_email).
or(User.where(email: expected_email, unconfirmed_email: nil))).
find_signed(params[:confirmation_token],
purpose: User.email_confirmation_purpose_for(expected_email))
if @user.present? && @user.unconfirmed_or_reconfirming?
if @user.confirm!
login @user
Expand Down
3 changes: 2 additions & 1 deletion app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ class UserMailer < ApplicationMailer
#
# en.user_mailer.confirmation.subject
#
def confirmation(user, confirmation_token)
def confirmation(user, confirmation_token, confirmable_email)
@user = user
@confirmation_token = confirmation_token
@confirmable_email = confirmable_email

mail to: @user.confirmable_email, subject: "Confirmation Instructions"
end
Expand Down
13 changes: 11 additions & 2 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,17 @@ def confirmable_email
end
end

def self.email_confirmation_purpose_for(email)
"confirm_email: #{email}"
end

def email_confirmation_purpose
self.class.email_confirmation_purpose_for(confirmable_email)
end

def generate_confirmation_token
signed_id expires_in: CONFIRMATION_TOKEN_EXPIRATION, purpose: :confirm_email
signed_id expires_in: CONFIRMATION_TOKEN_EXPIRATION,
purpose: email_confirmation_purpose
end

def generate_password_reset_token
Expand All @@ -63,7 +72,7 @@ def generate_password_reset_token

def send_confirmation_email!
confirmation_token = generate_confirmation_token
UserMailer.confirmation(self, confirmation_token).deliver_now
UserMailer.confirmation(self, confirmation_token, confirmable_email).deliver_now
end

def send_password_reset_email!
Expand Down
2 changes: 1 addition & 1 deletion app/views/user_mailer/confirmation.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<h1>Confirmation Instructions</h1>

<%= link_to "Click here to confirm your email.", edit_confirmation_url(@confirmation_token) %>
<%= link_to "Click here to confirm your email.", edit_confirmation_url(@confirmation_token, email: @confirmable_email) %>
2 changes: 1 addition & 1 deletion app/views/user_mailer/confirmation.text.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Confirmation Instructions

<%= edit_confirmation_url(@confirmation_token) %>
<%= edit_confirmation_url(@confirmation_token, email: @confirmable_email) %>
35 changes: 28 additions & 7 deletions test/controllers/confirmations_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class ConfirmationsControllerTest < ActionDispatch::IntegrationTest
freeze_time do
confirmation_token = @unconfirmed_user.generate_confirmation_token

get edit_confirmation_path(confirmation_token)
get edit_confirmation_path(confirmation_token, email: @unconfirmed_user.confirmable_email)

assert @unconfirmed_user.reload.confirmed?
assert_equal Time.now, @unconfirmed_user.confirmed_at
Expand All @@ -26,7 +26,7 @@ class ConfirmationsControllerTest < ActionDispatch::IntegrationTest
freeze_time do
confirmation_token = @reconfirmed_user.generate_confirmation_token

get edit_confirmation_path(confirmation_token)
get edit_confirmation_path(confirmation_token, email: unconfirmed_email)

assert @reconfirmed_user.reload.confirmed?
assert_equal Time.current, @reconfirmed_user.reload.confirmed_at
Expand All @@ -44,7 +44,7 @@ class ConfirmationsControllerTest < ActionDispatch::IntegrationTest
freeze_time do
confirmation_token = @reconfirmed_user.generate_confirmation_token

get edit_confirmation_path(confirmation_token)
get edit_confirmation_path(confirmation_token, email: @confirmed_user.confirmable_email)

assert_equal original_email, @reconfirmed_user.reload.email
assert_redirected_to new_confirmation_path
Expand All @@ -56,7 +56,7 @@ class ConfirmationsControllerTest < ActionDispatch::IntegrationTest
confirmation_token = @unconfirmed_user.generate_confirmation_token

travel_to 601.seconds.from_now do
get edit_confirmation_path(confirmation_token)
get edit_confirmation_path(confirmation_token, email: @unconfirmed_user.confirmable_email)

assert_nil @unconfirmed_user.reload.confirmed_at
assert_not @unconfirmed_user.reload.confirmed?
Expand All @@ -66,7 +66,15 @@ class ConfirmationsControllerTest < ActionDispatch::IntegrationTest
end

test "should redirect if confirmation link is incorrect" do
get edit_confirmation_path("not_a_real_token")
get edit_confirmation_path("not_a_real_token", email: @unconfirmed_user.confirmable_email)
assert_redirected_to new_confirmation_path
assert_not_nil flash[:alert]
end

test "should redirect if confirmation email is incorrect" do
confirmation_token = @unconfirmed_user.generate_confirmation_token

get edit_confirmation_path(confirmation_token, email: "[email protected]")
assert_redirected_to new_confirmation_path
assert_not_nil flash[:alert]
end
Expand Down Expand Up @@ -99,7 +107,7 @@ class ConfirmationsControllerTest < ActionDispatch::IntegrationTest

login @confirmed_user

get edit_confirmation_path(confirmation_token)
get edit_confirmation_path(confirmation_token, email: @confirmed_user.confirmable_email)

assert_not_equal Time.current, @confirmed_user.reload.confirmed_at
assert_redirected_to new_confirmation_path
Expand All @@ -115,7 +123,7 @@ class ConfirmationsControllerTest < ActionDispatch::IntegrationTest

confirmation_token = @reconfirmed_user.generate_confirmation_token

get edit_confirmation_path(confirmation_token)
get edit_confirmation_path(confirmation_token, email: unconfirmed_email)

assert_equal Time.current, @reconfirmed_user.reload.confirmed_at
assert @reconfirmed_user.reload.confirmed?
Expand All @@ -126,6 +134,19 @@ class ConfirmationsControllerTest < ActionDispatch::IntegrationTest
end
end

test "should prevent reuse of confirmation token" do
login @unconfirmed_user

confirmation_token = @unconfirmed_user.generate_confirmation_token

get edit_confirmation_path(confirmation_token, email: @unconfirmed_user.confirmable_email)
assert @unconfirmed_user.reload.confirmed?
@unconfirmed_user.update(unconfirmed_email: "[email protected]")

get edit_confirmation_path(confirmation_token, email: @unconfirmed_user.unconfirmed_email)
assert_redirected_to new_confirmation_path
end

test "should prevent authenticated user from submitting the confirmation form" do
login @confirmed_user

Expand Down
6 changes: 6 additions & 0 deletions test/controllers/sessions_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest

assert_not_nil current_user
assert_not_nil cookies[:remember_token]

remember_me_cookie = cookies.get_cookie("remember_token")

assert remember_me_cookie.http_only?
assert remember_me_cookie.secure?
assert_equal "Strict", remember_me_cookie.to_h["SameSite"]
end

test "should forget user when logging out" do
Expand Down
2 changes: 1 addition & 1 deletion test/mailers/user_mailer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class UserMailerTest < ActionMailer::TestCase

test "confirmation" do
confirmation_token = @user.generate_confirmation_token
mail = UserMailer.confirmation(@user, confirmation_token)
mail = UserMailer.confirmation(@user, confirmation_token, @user.unconfirmed_email)
assert_equal "Confirmation Instructions", mail.subject
assert_equal [@user.email], mail.to
assert_equal [User::MAILER_FROM_EMAIL], mail.from
Expand Down
Loading