Secret URLs in Rails

Posted 19 May 2009

Like many Rails websites, my first production Rails site needed user sign-ups. I wanted to have this work in a way that allowed a user to register only after confirming their email address.

The way to do this is with secret URLs.These are URLs that contain an encrypted string and are effectively impossible to guess. By sending a secret URL to an email address, if the URL is subsequently accessed, that’s pretty much a guarantee that the email was received, since there’s no other way that URL could have been obtained. (Check out the Rails Recipes book, which has a good chapter explaining secret URLs.)

Introducing the ActiveUrl Gem

As a first attempt at contributing to the Rails community, I’ve extracted my site’s secret URL functionality into a gem. Since it’s used in a similar fashion to ActiveRecord, I’ve called it ActiveUrl.

How is my implementation distinctive? Basically, it’s database-free. You don’t need any new database tables or fields to use it, since all the relevant information is persisted in the URL itself. All you need to do to hide a page behind a secret URL is to nest its route beneath an ActiveUrl object that the library provides. Neat!

Installation & Usage

First, install the gem:

gem sources -a http://gems.github.com
sudo gem install mholling-active_url

In your Rails app, make sure to specify the gem dependency in environment.rb:

config.gem "mholling-active_url", :lib => "active_url", :source => "http://gems.github.com"

[ UPDATE: The gem is now hosted on Gemcutter, with slightly different installation instructions – see here. ]

Specify a secret passphrase for the library to perform its encryption. You can set this by adding an initializer (say active_url.rb) in your config/initializers directory. This will just set the secret passphrase for your app (you might not want to check this into your source control):

ActiveUrl::Config.secret = "my-app-encryption-secret"

To generate secret URLs in your Rails application, simply inherit a model from ActiveUrl::Base, in the same way you would normally inherit from ActiveRecord::Base. These objects won’t be stored in your database; instead they will be persisted as an encrypted ID and placed in an URL given only to that user (typically by email).

class Secret < ActiveUrl::Base
  ...
end

The following class methods are available for your model:

  • attribute(*attribute_names) [sets attributes on your model];
  • belongs_to(model_name) [sets a “foreign key” attribute and an association method];
  • attr_accessible(*attribute_names) [allows mass-assignment of attributes]
  • validations: most of the ActiveRecord validations are available on the attributes you set;
  • after_save(callback_name) [sets a callback to be run after the object is persisted];
  • find(id) [finds an object from the specified ID, which will be extracted from an URL].

Save your object by using the ActiveUrl::Base#save method—this will run any validations and generate the encrypted ID if the validations pass. (You will usually use this method in your model’s controller.)

In your controllers which deal with ActiveUrl models, you’ll want to deal with the case of an invalid URL; usually just to render a 404. This is easily done using rescue_from in your application controller:

rescue_from ActiveUrl::RecordNotFound do
  render :file => "#{Rails.root}/public/404.html", :status => 404
end

Example: Confirming an Email Address

The typical use case for this example is the verification of an email address provided by a someone signing up to your website. You want to check that the address is valid by sending an email to that address; the user must follow a secret URL in the email to confirm they received the email.

Registration Model

We don’t want to create a User model until the email is confirmed, so instead we’ll use a ActiveUrl::Base model. This is what will be created when a user registers:

class Registration < ActiveUrl::Base  
  attribute :email, :accessible => true
  validates_format_of :email, :with => /^[\w\.=-]+@[\w\.-]+\.[a-zA-Z]{2,4}$/ix
  validate :email_not_taken
  
  after_save :send_registration_email
  
  protected
  
  def email_not_taken
    if User.find_by_email(email)
      errors.add(:email, "is already in use")
    end
  end
  
  def send_registration_email
    Mailer.deliver_registration(self)
  end
end

Going through this step-by-step:

  1. First, we set our email attribute using attribute :email, which generates setter and getter methods for the attribute.
  2. Next, validate the email address so it at least looks right (validates_format_of :email).
  3. We also want to check that a user has not already signed up with that email address, so we add a custom validation (email_not_taken) which adds an error if a User with that email address is found.
  4. Finally, we set an after_save callback to actually send the registration email when the model is saved. In the mailer method, we pass in the object so that we know what email address to send to and what secret URL to use.

Routes

Next, let’s set up our routes to allow user creation only via an email confirmation. In routes.rb the relevant routes would be:

map.resources :registrations, :only => [ :new, :create ] do |registration|
  registration.resources :users, :only => [ :new, :create ]
end

Registrations Controller

To allow a user to register, create a registrations controller with just two REST actions, new and create. The controller is entirely generic, as it should be:

class RegistrationsController < ApplicationController
  def new
    @registration = Registration.new
  end

  def create
    @registration = Registration.new(params[:registration])
    if @registration.save
      flash[:notice] = "Please check your email to complete the registration."
      redirect_to root_path # or wherever...
    else
      flash.now[:error] = "There were problems with that email address."
      render :action => "new"
    end
  end
end

When the create action succeeds, the registration object is saved and the registration email sent automatically by its after_save callback.

Registration View

In the new.html.erb view, the registration form would look something like:

<% form_for @registration do |form| %>
  <div>
    <%= form.label :email %>
    <%= form.text_field :email %>
  </div>
  <div>
    <%= form.submit "Register" %>
  </div>
<% end %>

Mailer

Finally, we set the mailer to deliver a registration email to the supplied email address:

class Mailer < ActionMailer::Base
  def registration(registration)
    subject    "Registration successful"
    recipients registration.email
    from       "admin@website.com"
    
    body       :registration => registration
  end
end

The registration object is passed through to the email template, where we use it to get the email address and also to generate the new user URL. Since the URL is secret, if it is subsequently accessed then we know that whoever is accessing it was able to read that email. Thus we have confirmed the email address as a real one, which is what we wanted.

The email template might look something like:

Hi <%= @registration.email %>,

Thanks for registering! Please follow this link to complete your
registration process:

<%= new_registration_user_url(@registration, :host => "website.com") %>

Thanks!
website.com

The secret URL generated in the email would look something like:

http://website.com/registrations/yAfxbJIeUFKX9YiY6Pqv0UAwufcacnYabEYS7TxTgZY/users/new

User Model

In our User model, we want to make sure the email address cannot be mass-assigned, so be sure to use attr_protected (or even better, attr_accessible) to prevent this:

class User < ActiveRecord::Base
  ...
  attr_protected :email
  ...
end

Users Controller

Now let’s turn our attention to the users controller. We access the new and create actions only via the nested routes, so that we can load our Registration object from the controller parameters. We’ll use the ActiveUrl::Base.find method to retrieve the registration object, and then set the user’s email address from it:

class UsersController < ApplicationController
  def new
    @registration = Registration.find(params[:registration_id])
    @user = User.new
    @user.email = @registration.email
  end

  def create
    @registration = Registration.find(params[:registration_id])
    @user = User.new(params[:user])
    @user.email = @registration.email
    if @user.save
      flash[:notice] = "Thanks for registering!"
      redirect_to @user # or wherever...
    else
      flash.now[:error] = "There were problems with your information."
      render :action => "new"
    end
  end
end

New User View

The exact contents of the user creation form will depend on our User model, among other things. Notably however,it will not include a field for the email address, since we’ve already obtained the email address from the registration object and we don’t want the user to be able to subsequently change it. (It’s probably advisable to include the email address in the form’s text though, for the sake of clarity.)

The new user form might look something like this:

<% form_for [ @registration, @user ] do |form| %>
  <div>
    Please enter new user details for <%= @user.email %>.
  </div>
  <div>
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>
  <!-- ... other user fields here ... -->
  <div>
    <%= form.submit "OK" %>
  </div>
<% end %>

Benefits of ActiveUrl

In other email confirmation schemes, whenever a registration process is initiated, a new user object is created, even before the email address is confirmed. This causes a couple of problems:

  • The user model will need some form of state (to distinguish between confirmed and unconfirmed users).
  • If a registration is initiated but not completed, the unconfirmed record will remain in the database, and will need to be manually removed at a later date.

The ActiveUrl gem overcomes both these problems by persisting all the relevant data to the URL itself, in encrypted form. No database table is needed.

One potential problem with this approach? The URL may become quite long if you store much data in the model. Keep the number of attributes and the length of their names to a minimum to avoid this. Typically, a single attribute or a belongs_to reference is all that’s needed, and produces URLs of modest length.

Other Uses

Another use for secret URLs is to enable your website users to access user-specific parts of the site without being logged in.

A classic example of this is a forgotten-password page, where the user cannot log in because of a forgotten password. By sending a secret URL to the user’s email address, the user can access a password-editing page without being logged in (the knowledge of the secret URL being a alternate form of authentication).

Another example is a user-specific RSS or Atom feed. Since most newsreaders cannot access authenticated feeds, it may not be desirable to put the feeds behind an authenticated controller action. Secret URLs offer an alternative authentication method in this case.

[ UPDATE: A companion article describing another example is here. ]

Over to You

Rails developers: let me know what you think in the comments! Go easy, I’ve never published a gem before. Please feel free to fork and improve as you see fit; the code is on GitHub here. (I may even pull back the changes—if I can figure out how…)

The gem includes a set of RSpec tests, though they’re a bit rough. rake spec will run the tests.

Feed-icon-14x14 Comments

  • sir ross
    (28 May 2009 at 02:38 AM) writes:

    validate :email_not_taken? Are you kidding me? Go and read about validates_uniqueness_of

    Cheers.

  • Carl
    (28 May 2009 at 06:09 AM) writes:

    If I'm not mistaken, he couldn't use validates_uniqueness_of since it is actually a different model he needs to validate against (User rather than Registration.)

  • Matthew Hollingworth
    (28 May 2009 at 11:09 AM) writes:

    @sir ross: Carl is right, you can't use validates_uniqueness_of as you need the check that the email doesn't already exist in the User model, not the Registration model. It's one of the few validations that won't work, since it requires an ActiveRecord model. (Not to mention that it doesn't really make any sense in the context!)

  • Bruce Hauman
    (29 May 2009 at 12:39 AM) writes:

    I have to ask: Why not persist the collected info to the DB? Yes, it requires clean up later. But one of the major problems with verification tokens is their length right? Depending on their mail client people tend to munge them up. And that is a very dissatisfying experience.

    While the purest in me loves the idea of not having to clean up token records at a later date. It's not exactly the worst thing either.

    I do love the fact that the token is not at the end of the generated URL. I think that alone will stop a lot of the mungin going on out there.

    All in all though, I think it's pretty cool. Thanks for throwing this in the ring.

  • nice64
    (29 May 2009 at 04:25 AM) writes:

    Nice, very nice in did. The "There were problems with your information." flash is what the looser see when racing for a email, or?

  • Matthew Hollingworth
    (29 May 2009 at 07:52 AM) writes:

    @Bruce: It's a design choice, is all. I agree though, the length of the token could be a downside for people with mail clients that break the URL into multiple lines.

    The gem has other uses as well as email confirmation. Check out the next article for an example of implementing a "forgot my password" feature. You could also use it to generate a secret URL for hiding a user-specific, non-authenticated RSS feed.

    @nice64: That flash message would show if other validations on your User model (.g. validates_presence_of :name, or whatever) failed.

  • Bob Sturim
    (20 June 2009 at 01:57 PM) writes:

    I love the plugin and have started using it. But I'm having trouble enabling translations of the attributes. It doesn't seem to follow the activerecord.attributes.[model].[attribute] convention. Is it possible to allow the activeurl model attributes to be translated?

    Thanks!

  • Matthew Hollingworth
    (20 June 2009 at 10:28 PM) writes:

    @Bob: glad you're finding it useful. I'm assuming you're talking about I18n, for labels in your forms or something? Unfortunately that's not really on my radar as I've never needed any of the I18n stuff myself and don't know how it works. You'd have to fork the code and add it yourself; I can't imagine it'd be too hard though!

  • -jul-
    (21 June 2009 at 01:55 AM) writes:

    I also started using ActiveUrl, and it seems I have a problem with your example: the email is sent every time the Registration class is instantiated with valid data (eg. Registration.find(params[:id])). I thought it's because ActiveUrl.find calls create, and in libs/active_url/callbacks.rb, active_method_chain hooks after_save to create method, instead of save. I replaced 'create' with 'save' but with no success. Any advise?

  • Bob Sturim
    (21 June 2009 at 06:20 AM) writes:

    Sorry if I wasn't clear. If I specify validation rules (such as validates_presence_of), with active record models rails will search my locales/en.yml file for the name of the attribute (assuming I've specified it using the message hierarchy defined in my earlier post. So, if the message "XXX cannot be empty?" is shown, active record finds the value of XXX using my locale file. I tried doing the same thing with activeurl model and it doesn't appear to work. Should it?

    Thanks again.

  • Bob Sturim
    (21 June 2009 at 06:23 AM) writes:

    @Jul: I had the same problem. I came up with a kludgy work around, which is to set a attr_accessor flag to true when the registration is created, and leave it false otherwise. And then my callback method will only send the email if the flag has been set. It's not particularly clean, but it does work. I'd love to see a better solution.

  • Matthew Hollingworth
    (21 June 2009 at 10:37 AM) writes:

    @Jul: Yikes! Thanks for picking that up, what a glaring error. I've added a new spec for the correct behaviour and changed the code - now the callback runs on #save rather than #create. I bumped the gem version to 0.1.4; reinstall it and see how you go.

    commit (just the top bit): http://github.com/mholling/active_url/commit/ef4ab862dda3a3b6cfa0f2d7b182b7c0001d8b4f)
    spec: http://github.com/mholling/active_url/tree/ef4ab862dda3a3b6cfa0f2d7b182b7c0001d8b4f/spec/callbacks_spec.rb

    @Bob: OK, validation messages, of course. Nope I have not added anything specifically to support the I18n translations and the locales file, so I wouldn't expect it to work off the bat. I expect it'd be a fairly straightforward addition for someone familiar with how the translations work though. (Might see about adding it at a later date myself, but no guarantees. :)

  • Mike
    (22 August 2009 at 12:41 AM) writes:

    Hi,

    I've tried using our library, but when I try to create url I receive url like this (not secret):

    http://localhost:3000/booking/new.%23%3Cprebooking:0xb6ef13c8%3E

    I set the encryption passphrase. What will be the name of this function to creating url? If I try

    <%= new_booking_url(@booking, :host => "localhost:3000") %>

    then I get link as above.

    Thx in advance for response

  • Matthew Hollingworth
    (22 August 2009 at 09:46 AM) writes:

    @Mike: You don't say what your ActiveUrl model is, but I'll assume it's your @booking object. Your new_booking_url should not take any objects as parameters since it's a route for a "new" action - you haven't created the object yet. If you want to follow the style I've used in the article, your actual secret URL should be nested under your bookings resource in your routes (as a "confirmation" resource, let's say), and the named route call for the secret URL would be new_booking_confirmation_url(@booking).

    Have another read through the article. If you need more help, feel free to send me an email. (But make sure to include info on your routes and models and so on!)