Repository navigation
User Authentication And Persistence
We'll use the customized approach that Rails gives us out of the box.
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 activemodelThis 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
endFor 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
endWe 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.