How to Add an Invitation Code to Devise and Rails

  • rails, gems, tutorials
  • 3 Comments

Ever wonder how to restrict account sign up to invited guests using Devise and Rails? This easy-to-implement technique adds an invitation code field to the Devise gem's sign-up form, ensuring only visitors who were sent an invitation code can create an account.

Customize the Devise Sign-up View

The first step is to create an :invite field in the Devise sign-up view. To do that I need to generate the Devise views by hitting rails generate devise:views and then add the :invite field to app/views/devise/registrations/new.html.erb:

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>
    <div class="form-group">
      <%= f.label :email %>
      <%= f.email_field :email, autofocus: true %>
    </div>
    <div class="form-group">
      <%= f.label :invite, "Invitation Code" %>
      <%= f.text_field :invite, placeholder: "Input invitation code" %>
    </div>
    <div class="form-group">
      <%= f.label :password %>
      <% if @minimum_password_length %>
      <em class="devise-label">(<%= @minimum_password_length %> characters minimum)</em>
      <% end %><br />
      <%= f.password_field :password, autocomplete: "off" %>
    </div>
    <div class="form-group">
      <%= f.label :password_confirmation %>
      <%= f.password_field :password_confirmation, autocomplete: "off" %>
    </div>
  <div class="actions form-group">
    <%= f.submit "Sign up" %>
  </div>
<% end %>
<%= render "devise/shared/links" %>

Usually I'd create a migration and add the :invite attribute to the Users table, but I don't need to go through all of that trouble here. I just need to validate what the visitor enters in the :invite field is indeed the invitation code I sent to them. And to do that, all I need is a virtual attribute.

Virtual Attribute Validation

In app/models/user.rb I add the virtual attribute :invite to the User model via attr_accessor:

class User < ActiveRecord::Base
  attr_accessor :invite
end

Now, I need a method I can call to validate the :invite field and I only want to trigger this validation on the create action:

class User < ActiveRecord::Base
  validate :validate_invite, :on => :create
  attr_accessor :invite

  def validate_invite
    if self.invite != “my_invite_code"
      self.errors[:base] << "The Invitation Code is Incorrect"
    end
  end
end

If the value of self.invite is not equal to “my_invite_code", an exception is raised, the visitor sees the error message “The Invitation Code is Incorrect", and a new User will not be created. If the two values are the same, the new User will be created.

Of course, I can't hardcode the invitation code into the conditional and push it to GitHub. For situations like this, I like to use the Figaro gem to keep my passwords or API keys out of version control. Figaro creates an application.yml file were I can store the invitation code as a key-value pair: invitation_code: "my_invitation_code". Figaro automatically adds application.yml to .gitignore, ensuring my sensitive data is not pushed to GitHub. The ready-for-production method now looks like this:

def validate_invite
  if self.invite != Figaro.env.invitation_code
    self.errors[:base] << "The Invitation Code is Incorrect"
  end
end

Configure Strong Params

Last and certainly not least, I need to update the strong parameters for the Devise sign-up form so the :invite field will be permitted. In app/controllers/application_controller.rb I'll use the before_action to configure Devise's standard parameters:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:email, :password, :password_confirmation, :invite) }
  end
end

Admittedly, this technique might not be suited for all apps since every new user will be sent the same invitation code. Some apps may require dynamic invitation codes that are different for every user (hey, another blog post!) but this technique is more than capable of handling simple invitation-only app sign ups.