loading...

Reconfirmable: integrating Devise into a non-standard registration system

themagickoala1 profile image themagickoala ・5 min read

Devise makes it easy to implement a full user authentication system in Ruby on Rails while writing as little code as necessary. The modules it provides are easy to integrate and follow best practices for securing your application. But sometimes your application requires a little out of the box thinking. So what do you do when your user journeys aren't what Devise is expecting?

What problem are you even solving here?

Consider an application where you want to confirm a user's email address before letting them even get into your application. The standard flow for the devise confirmable module is to do it the other way round. So you're going to have to hand-crank this functionality. But you also really want to use confirmable's reconfirmable feature when users change their email address, which means enabling confirmable as well. Now you've got two competing confirmation systems and you've got your knickers in a twist! This post will lay out how to bypass confirmable for registration while still using reconfirmable for changing emails.

Getting started

Setting up a Ruby on Rails project is a solved problem, as is integrating Devise, so we'll try not to spend too much time on it. The Devise Github repository has a good tutorial for this, and the rest of the post will assume a working implementation of Devise without confirmable enabled.

Configuring confirmable

This section is going to take heavy inspiration from the Devise wiki on Github here. Assuming your user model is at models/user.rb, all you need to do is add :confirmable:

devise :registerable, :confirmable

Enabling confirmable will require adding four columns to your users table, so create a migration script:

rails g migration add_confirmable_to_devise

In the resulting migration file db/migrate/YYYYMMDDxxx_add_confirmable_to_devise.rb, add the following code:

class AddConfirmableToDevise < ActiveRecord::Migration
  # Note: You can't use change, as User.update_all will fail in the down migration
  def up
    add_column :users, :confirmation_token, :string
    add_column :users, :confirmed_at, :datetime
    add_column :users, :confirmation_sent_at, :datetime
    add_column :users, :unconfirmed_email, :string
    add_index :users, :confirmation_token, unique: true

    # User.reset_column_information # Need for some types of updates, but not for update_all.
    # To avoid a short time window between running the migration and updating all existing
    # users as confirmed, do the following
    User.update_all confirmed_at: DateTime.now
    # All existing user accounts should be able to log in after this.
  end

  def down
    remove_columns :users, :confirmation_token, :confirmed_at, :confirmation_sent_at, :unconfirmed_email
  end
end

The first step here is to add the necessary columns:

add_column :users, :confirmation_token, :string
add_column :users, :confirmed_at, :datetime
add_column :users, :confirmation_sent_at, :datetime
add_column :users, :unconfirmed_email, :string

The confirmation_token column is your confirmation key - this will need to be sent to the user when setting a new email address to verify that they have access to the inbox. The confirmation_sent_at column gets populated on generation of the token, and the confirmed_at column gets set when confirmation is completed successfully. Having only skimmed the surface of the sort of magic Devise is capable of, I can only assume these two columns can be compared in order to prevent multiple confirmations being attempted.

The fourth column, unconfirmed_email, is added for use with the reconfirmable flow. The temporary email address is stored here, and persisted to the email column when confirmation is complete. Sounds simple enough right?

Well we're not quite done - confirmable introduces additional functionality to the user model, and users have to confirm their accounts to use the application. But what about existing users? They aren't confirmed, and they don't have a token with which to confirm. To get around this, we can just run User.update_all confirmed_at: DateTime.now and just like that, all our existing users are confirmed.

To finish off, run rake db:migrate and then make sure reconfirmable is enabled in the Devise config by adding/setting the following line in config/initializers/devise.rb:

config.reconfirmable = false

Tweaking reconfirmable

By default, the confirmation_token generated uses SecureRandom.urlsafe_base64(15) to create the random token. This can be changed in the User model by overriding generate_confirmation_token. Bear in mind that this method also sets the confirmation_sent_at property, so I recommend calling super and then setting the confirmation_token property to your new preferred value. We used this to set a 6-digit passcode to be manually entered into a continuous client-side flow, though you can just as easily use the more standard email-with-link-to-unique-page using the token as a url parameter to identify the user.

Bypassing confirmable for registration

Here we get to the problem we're trying to solve. We're already confirming a user's email address as part of the registration journey, so we don't need to confirm it post-registration. But by default, confirmable is going to send its own confirmation email to the user. So how do we get around it?

Fortunately, we can override a method provided by Devise to allow you to customise the mail mapping of the registration email. This is send_on_create_confirmation_instructions. By default, all it does is call send_confirmation_instructions, allowing the user to perform additional configuration before or after sending the email. So the first step is to do nothing at all:

def send_on_create_confirmation_instructions
end

This will prevent the confirmation email from being sent but the user still won't be able to log in as they aren't confirmed. So let's expand a little:

def send_on_create_confirmation_instructions
  self.confirm
end

That should do it. We know the user is confirmed from our own custom flow, so we can just tell Devise as much, and voila! The user can log in just fine, and didn't receive any superfluous confirmation emails.

Summary

Devise provides a lot of additional functionality to the user model, allowing us to focus on small customisations to support our application's individual requirements.

This post is a work in progress, and I hope to be able to improve it as I continue to integrate our application with the server-side code being written by our stellar back-end team, and with feedback from you. Please leave a comment if you can help to improve this guide, whether with constructive criticism or additional sources.

Sources

  1. How to add :confirmable to users - Devise Github Wiki (https://github.com/plataformatec/devise/wiki/How-To:-Add-:confirmable-to-Users);
  2. Devise::Models::Confirmable - Devise Rubydocs (https://www.rubydoc.info/github/plataformatec/devise/master/Devise/Models/Confirmable);
  3. Getting started with Rails and Devise - Devise Github repository (https://github.com/plataformatec/devise);
  4. Credit to verenion for development of the server-side code and architecture of the application.

Posted on by:

Discussion

markdown guide