Reconfirmable: integrating Devise into a non-standard registration system
Tags: rails, ruby, devise, security
Published: 2019-07-16 19:42:30

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](https://github.com/plataformatec/devise) 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](https://github.com/plataformatec/devise/wiki/How-To:-Add-:confirmable-to-Users). 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](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](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](https://github.com/verenion) for development of the server-side code and architecture of the application.