Skip to content

User Authentication And Persistence

Mike Bradford edited this page May 25, 2012 · 1 revision

We'll use the customized approach that Rails gives us out of the box.

ActiveModel::SecurePassword.has_secure_password

Adds methods to set and authenticate against a BCrypt password. Requires you to have a password_digest attribute in your model. We already set up our Cook model with a password_digest attribute and included the bcrypt-ruby gem to our Gemfile, so we're good to go. Strictly speaking, we don't need to add bcrypt-ruby to our Gemfile because its loaded with the class when has_secure_password is called.

You can take a look at the source by opening up ActiveModel gem with bundler and checking out secure_password.rb:

bundle open activemodel

This assumes you have set a $EDITOR or $BUNDLE_EDITOR environmental variable.

The methods has_secure_password gives us a password virtual attribute whose encrypted value is persisted on the database as password_digest_, a password_confirmation validation, a presence validation on password_digest, and an authenticate instance method.

Here's what our Cook model looks like so far, including associations and additional validations:

class Cook < ActiveRecord::Base
  EMAIL_PATTERN = /^([a-z0-9]+[-\._]?)+[a-z0-9]+@([a-z0-9]+[-\._]?)+[a-z0-9]+\.[a-z]{2,8}$/ui
  USERNAME_PATTERN = /^[a-z0-9]+$/i

  has_secure_password
  
  has_many :cookbooks
  has_many :recipes, through: :cookbooks
  
  attr_accessible :username, :email
  
  validates_presence_of   :email, :username
  validates_uniqueness_of :email, :username
  validates_format_of     :email, with: EMAIL_PATTERN, message: "Invalid email address."

  validates_format_of     :username,  with: /^[a-z0-9]+$/i, message: "Username can only contain alphanumeric characters."
  validates_length_of     :username,  minimum: 7, maximum: 32
end

Authentication Key

For persistence, we'll use a unique 40-character digest sent with each request that requires authentication. We'll make this an instance attribute on the Cook model called auth_key. We'll need to add functionality to generate this key and validate its presence before the model is persisted.

So with our new functionality, our Cook model looks like this:

class Cook < ActiveRecord::Base
  EMAIL_PATTERN = /^([a-z0-9]+[-\._]?)+[a-z0-9]+@([a-z0-9]+[-\._]?)+[a-z0-9]+\.[a-z]{2,8}$/ui
	USERNAME_PATTERN = /^[a-z0-9]+$/i
  AUTH_KEY_PATTERN = /^[a-z0-9\$!\*_\-=\^]{40}$/i
  
  has_secure_password
  
  has_many :cookbooks
  has_many :recipes, through: :cookbooks
  
  attr_accessible :username, :email
  
  validates_presence_of   :email, :username
  validates_uniqueness_of :email, :username
  validates_format_of     :email, with: EMAIL_PATTERN, message: "Invalid email address."

  before_create :set_auth_key

  validates_format_of     :username,  with: USERNAME_PATTERN, message: "Username can only contain alphanumeric characters."
  validates_length_of     :username,  minimum: 7, maximum: 32
  
  validates_presence_of   :auth_key
  validates_format_of     :auth_key, with: AUTH_KEY_PATTERN
	
  class <<self
    def generate_auth_key(length=40)
      chars = ("a".."z").to_a + ("0".."9").to_a + %w($ ! * _ - = ^)
      Array.new(length).map { chars[rand(chars.length)].send([:upcase,:downcase][rand(2)]) }.join
    end
  end

  private

  def set_auth_key
    self.auth_key = generate_auth_key
  end
end

We can also use Cook.generate_auth_key as a part of a forgotten password request.

If we were creating a public API to be used by mobile application developers, we would want to use OAuth 2 so that users can authorize other apps to use your API on their behalf without having to pass sensitive login info.

Clone this wiki locally