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?
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.
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.
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
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
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.reconfirmable = false
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.
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.
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.
- How to add :confirmable to users - Devise Github Wiki (https://github.com/plataformatec/devise/wiki/How-To:-Add-:confirmable-to-Users);
- Devise::Models::Confirmable - Devise Rubydocs (https://www.rubydoc.info/github/plataformatec/devise/master/Devise/Models/Confirmable);
- Getting started with Rails and Devise - Devise Github repository (https://github.com/plataformatec/devise);
- Credit to verenion for development of the server-side code and architecture of the application.