Migrating Away from Devise Part 4: Email Confirmation Setup
Published: January 09, 2025
This is the fourth part of a multi-part series about moving from devise to the Rails generated authentication system
- Part 1 - Setup and Sessions
- Part 2 - User Sign-in
- Part 3 - Password Recovery
- Part 4 - Email Confirmation Setup
The app uses confirmable
with devise to verify the account on creation including the ability to resent confirmation emails. Rails provides all the functionality needed to pull this off in a similar means as password recovery, but it'll have to be implemented manually. In regards to the User
model, we are going to focus on the existing confirmed_at
and confirmation_sent_at
fields that already exist from the devise implementation.
Routes look like this, similar to how devise builds them. The show
action is the landing point the user goes to when they click the link in their email. The new
and create
actions are to resent the email.
resources :confirmations, only: [:new, :create, :show], param: :token
The controller looks like this. It's similar to the password recovery controller built in the last step.
class ConfirmationsController < ApplicationController rate_limit to: 10, within: 3.minutes, only: :create, with: lambda { redirect_to new_confirmation_path, alert: "Try again later." } before_action :set_user_by_token, only: [:show] def show @user.confirmed_at ||= Time.now.utc @user.save! redirect_to new_session_path, notice: "Your account has been successfully confirmed. Please sign in." end def new end def create if (user = User.find_by(email: params.expect(:email))) UserMailer.confirmation_instructions(user).deliver_later end redirect_to new_session_path, notice: "A confirmation link has been sent to your email address to confirm your account." end private def set_user_by_token @user = User.find_by_token_for!(:account_confirmation, params[:token]) rescue ActiveSupport::MessageVerifier::InvalidSignature redirect_to new_confirmation_path, alert: "Account confirmation link is invalid or has expired." end end
The controller follows a similar flow as the PasswordController
. A different email and redirect paths are used as well as what token is pulled from. When we confirm the account, we set confirmed_at
once to prevent a user from being "confirmed multiple times." Also, instead of using the provided token lookup and generation methods that has_secure_password
provides for password reset tokens, we're manually setting up an "account_confirmation" token.
To generate the token for a user, generates_token_for
is called on the User
model. It auto expires after confirmed_at
is set to a different value (ie, from nil
to a date).
generates_token_for :account_confirmation, expires_in: 30.minutes do confirmed_at.to_s end
The UserMailer
method and view looks like this which is based off of the password reset email...
def confirmation_instructions(user) @user = user return if @user.confirmed_at.present? @user.update!(confirmation_sent_at: Time.now.utc) mail subject: "Confirmation instructions", to: user.email end
<p>Welcome <%= @user.username %>!</p> <p>You can confirm your account email through the link below:</p> <p><%= link_to "Confirm my account", confirmation_url(@user.generate_token_for(:account_confirmation)) %></p>
Two small changes compared to password reset. First the mailer method returns early (preventing sending the email) if the user is already confirmed. This allows for a standard no-op response where the callers only need to be concerned about enqueuing the email. Second, we stamp out confirmation_sent_at
. This is more for debugging purposes and not user-visible.