The Pragmatic Studio

User Accounts: Model

Exercises

Objective

Over the next few exercises we'll incrementally create a basic user account and authentication system from scratch. For the purposes of this exercise, we'll focus on getting the User model in good shape. Doing that involves the following tasks:

  • Generate a User resource that securely stores user passwords in a password_digest column
  • Install the bcrypt gem
  • Declare reasonable validations in the User model
  • Create a few example users in the database using the Rails console

We hope we don't need to belabor the point that passwords should never, ever be stored in the database as plain text. But just for good measure, go ahead and raise your right hand now and repeat aloud after us:

"I, (state your name), being a professional in the craft of software development, do hereby promise to take responsibility for securing the passwords entrusted to me by users of my application. I will never, under any circumstance including project deadline pressures, store the plain-text version of a user's password in my database or other storage medium. Furthermore, I will not hold Mike and Nicole liable for any wrongdoing on my part. And I won't use totally obvious passwords such as '123456' when creating my own account."

OK, then. We're ready to make good on that promise!

If you're feeling confident, give this exercise a try on your own first based on the objectives above. And as always, feel free to follow the steps below if you need help.

Specs

In the previous course you learned how to write automated tests (specs) upfront using RSpec and Capybara. We used a combination of feature and model specs to specify how particular application features should work. Given a failing spec, we knew a feature was complete only when its spec passed. This test-first approach offered several benefits:

  • Writing an executable spec first made us think about the feature from the user's perspective before going heads-down into the actual code.

  • Starting with a failing spec focused our attention on a measurable goal while we were coding.

  • Once we had a passing spec, we could easily run it (and all the other specs) every time we made application changes to make sure we didn't accidentally break anything.

We simply can't imagine working on a real-world Rails app without having specs. At the same time, we realize that not everyone may yet be comfortable with test-first development. So we have some options for you:

  1. If you'd like to practice test-first development with RSpec and Capybara, then by all means write some specs up front! You can then compare your specs against ours by clicking the link below. You may choose to use the specs to work through the exercise using a test-first approach if you like, or to check your work at the end of the exercise.

  2. If you're still getting in the groove of test-first development, you might start by trying to identify what you would test at a high level. For example, in this exercise, what should we test in the User model? Well, for starters a user must have a name and email. What else can you think of? Compare the list you come up with against our list by clicking the link below.

  3. Finally, if you'd prefer to focus on the core concepts in the course your first time through and revisit testing later, then feel free to simply read through the spec file.

So, here's your first spec. Click the link below to reveal a model spec that specifies how the User model should behave at the end of this exercise. Read through the spec as if you're reading a list of requirements, and then paste the code into a new user_spec.rb file in your spec/models directory.

These specs rely on a user_attributes utility method that returns a hash of example user attributes. If you want to take a peek at that method, you'll find it in the spec/support/attributes.rb file.

To run this model spec file, use the rspec command and give it the spec file name, like so:

rspec spec/models/user_spec.rb

We haven't written any code for this part of the exercise yet, so the spec should fail. But here's the cool part: When the spec passes, you know you've successfully completed the first part of this exercise!

1. Generate the User Resource

First we need to create a new (third) resource to represent a user with a name, e-mail address, and secure password. We'll need a users database table, a User model, and ultimately a UsersController and conventional routes for interacting with users via the web. As in the previous course, we'll use the resource generator to make quick work of this.

  1. Start by generating a resource named user (singular) with name and email string fields, and the password field as the digest type.

  2. We'll look at everything that got generated as we go along. For now, open the generated migration file and you should see the following:

    class CreateUsers < ActiveRecord::Migration[5.0]
      def change
        create_table :users do |t|
          t.string :name
          t.string :email
          t.string :password_digest
    
          t.timestamps
        end
      end
    end
    

    Notice that the generator translated the declaration password:digest into a password_digest column of type string in the migration file. That's pretty smart!

  3. Remember, a migration file is simply a set of instructions for modifying your database schema. To actually create the users database table, you need to run the migration.

  4. The generator also gave us a User model in the app/models/user.rb file, so crack that file open and you should see the following:

    class User < ApplicationRecord
      has_secure_password
    end
    

    Rails 4: Your class will inherit from ActiveRecord::Base:

    class User < ActiveRecord::Base
      has_secure_password
    end
    

    Again, the generator is pretty clever! Notice that it added the has_secure_password line. It knew we wanted to store passwords securely because we used password:digest when running the generator.

    As you'll recall from the video, the built-in has_secure_password method in Rails helps us do the right thing when it comes to securely storing passwords. And when you read it out loud it makes sense: A user has a secure password. In the previous course we used similar declarations inside of models such as belongs_to and has_many to get functionality for managing model relationships. In this case, calling the built-in has_secure_password method adds functionality for securely storing passwords.

  5. The has_secure_password method depends on the bcrypt gem which implements a state-of-the-art secure hash algorithm used to encrypt passwords. Not all Rails applications need this gem, so it's commented out in the default Gemfile.

    So you need to uncomment the following line in your Gemfile:

    gem 'bcrypt', '~> 3.1.7'
    

    Rails 4: The bcrypt gem was previously named bcrypt-ruby. Depending on which version of Rails 4 you're using, you may see it listed as:

    gem 'bcrypt-ruby', '~> 3.1.2'
    

    Don't worry about the differences or the exact version number. Both gems do exactly the same thing, and for the purposes of this course the exact version number makes no difference.

  6. Then install the gem using

    bundle install

And that's all there is to it! With these changes in place, Rails has everything it needs to securely store passwords in the password_digest column.

2. Declare User Validations

Next we need to add some reasonable validations to the generated User model to ensure that invalid user records can't be stored in the database. The has_secure_password line automatically adds password-related validations, but we also need validations for the user's name and email. Use built-in model validations to enforce the following validation rules:

  1. A name must be present.

  2. An email must be present and formatted so that it has one or more non-whitespace characters on the left and right side of the @ sign.

  3. We don't want two users in the database to have the same e-mail address. So make sure emails are unique regardless of whether they use upper or lower case characters.

  4. At this point the model spec should now pass, so run it as a quick sanity check:

    rspec spec/models/user_spec.rb

3. Create Users in the Console

Now that we have a users database table and a User model with validations, let's try creating some users in the database using the Rails console and see what has_secure_password gives us:

  1. Fire up a Rails console session.

    rails c
  2. Then instantiate a new User object without a name, email, or password.

  3. Now try to save the invalid User object to the database.

    The result should be false. Remember, when you try to save (or create) a model object, its validations are automatically run. If a validation fails, a corresponding message is added to the model's errors collection. And if the errors collection contains any messages, then the save is abandoned and false is returned. In short, the failed validations prevent the user from being saved to the database, which is exactly what we want!

  4. To see which validations are failing, inspect the validation error messages by accessing the errors collection. To dig down into the actual error messages, tack on a call to full_messages to get an array of error messages.

    You should get the following:

    => ["Password can't be blank", "Name can't be blank", "Email can't be blank", "Email is invalid"]
    

    Notice that has_secure_password added a validation to ensure a password is present when creating a new user, in addition to the name and email validations we declared. Nice!

  5. Go ahead and assign just a name and email for the user.

    These attributes map directly to the name and email database columns.

  6. Next, set a password for the user by assigning a value to the virtual password attribute.

    Remember, the password attribute is a virtual attribute that was dynamically defined by the has_secure_password method. Unlike a typical attribute, when we assign a value to the password attribute it doesn't try to store the value in a corresponding password database column. That's good because we don't have a password column and we don't want plain-text passwords stored in our database!

    Instead, assigning a value to the password attribute causes the plain-text version of the password to be encrypted and the encrypted version is then stored in the password_digest column. It appears to be a clever sleight of hand, but now you know it's not actually magic. It works because assigning a value to the password attribute calls the special password= method defined by has_secure_password. And that method turns around and does the encryption for us.

  7. Now for the big reveal: Print the value of the password_digest attribute.

    >> user.password_digest
    

    You should get a string of what looks like gibberish, such as "$2a$10$wTBwLqnYrXffr.ainX60qOVB6hWeF4T1rU3RMHTL2olZ.erAmJS7O". That string, typically referred to as an irreversible digest, is the result of running the plain-text password through the one-way hash algorithm in the bcrypt gem.

  8. Now set a password confirmation that doesn't match the password and try to save the user record again.

    Think of a typical sign-up form that prompts for a password and makes you re-enter it to confirm that the passwords match. That's pretty common, and has_secure_password has you covered. It added a password_confirmation attribute and a validation that requires a password confirmation to be present, as well.

  9. Check the validation error messages and you should get the following:

    >> user.errors.full_messages
    => ["Password confirmation doesn't match Password"]
    

    Note that the password_confirmation attribute is also a virtual attribute. It doesn't map to a database column. Instead, when you assign a value to password_confirmation, the value is simply stored temporarily in an instance variable. Behind the scenes, has_secure_password runs the validations against that instance variable.

  10. Then assign a matching password confirmation so that you can finally (successfully) save the user to the database.

  11. Next, create a second user by calling the new method and passing it a hash of attribute names and values: name, email, password, and password_confirmation. Save it and make sure it successfully saves without any validation errors.

  12. To demonstrate that the plain-text password is only temporarily stored in memory, find the user you just created by their email address like so:

    >> user = User.find_by(email: "curly@example.com")
    

    Then print the value of the password_digest attribute and you should get the encrypted-version of the password (gibberish):

    >> user.password_digest
    => "$2a$10$oiB9u4fT.SUOKK90iMKFCOPi4DwKfQU00YStJKQL5zRHFKar/1ipO"
    

    We assigned a plain-text password when we initially created the user, but is it still there? To check, print the value of the password attribute:

    >> user.password
    => nil
    

    Ah, it has a value of nil! Remember, we fetched the user from the database. Doing that populated all the real (non-virtual) attributes with the values corresponding to each column. But there is no password column, so the password virtual attribute has a value of nil. And that's exactly what we want: there's no trace of the original plain-text password!

  13. Finally, since we required that a user's email be unique, try creating a third user with the same email address as a previously-created user.

    It should return false and the database transaction should get rolled back. Notice that the uniqueness validation ran a SQL SELECT query to check if a user with the same email address already existed in the database.

    Print the validation error messages.

    Indeed, a user already exists with the same email address so the new (duplicate) user wasn't created.

  14. Remember how to check that you now have two unique users in the database?

Hey, we got a lot for free! By following the tried-and-true convention, has_secure_password takes care of validating that a user has a (confirmed) password and securely stores it for us.

Solution

All the exercise solutions for the app you're writing, as well as the example code for the app we're writing in the videos, are in the code bundle file you downloaded earlier. In the example-app and exercise-solutions directories you'll find code organized into subdirectories matching the names of the course modules.

The full solution for this exercise is in the user-accounts-model directory of the code bundle.

Bonus Round

Want to stretch your skills? At the end of an exercise we'll often include bonus features you might consider adding to the app. You don't have to complete the bonus rounds in order to move on to the next section. And in fact you might want to revisit these bonuses after you've completed the course.

Add More Password Validations

We've seen that has_secure_password automatically gives us validations for the presence and confirmation of a user password. That's a really good start, but of course you can add more validations as necessary.

For example, suppose you wanted to require passwords to be at least 10 characters in length. To do that, add the following to your User model:

validates :password, length: { minimum: 10, allow_blank: true }

By setting the allow_blank option to true, the length validation won't run if the password field is blank. That's important because a password isn't required when a user updates his name and/or email. So if the password field is left blank when editing the user account, the length validation is skipped.

Disable Test File Generation

When running a generator, you can use the --no-test-framework option to skip the generation of test stub files. You can also set that as a default option so you don't have to type it every time you run a generator. We've done that in the baseline app by adding the following bit of configuration in the config/application.rb file:

config.generators do |g|
  g.test_framework false
end

What About a Gem?

You might be wondering why we just don't use an off-the-shelf authentication gem such as Devise. It's not because we have anything against gems. But gems tend to come and go. So rather than investing a lot of time in learning a particular gem, we believe that learning how things work at a fundamental level is a better long-term investment.

Every Rails developer should know at a basic level how an authentication system works. And the best way to gain that understanding is to build one yourself. That way, should you decide down the road to slip a third-party gem in your app, you'll be in a much better position to understand, customize, and troubleshoot the code.

Wrap Up

Excellent! Our User model looks to be in good shape, so we're ready for the next exercise where we'll tackle the web interface for user accounts. Abracadabra...

Dive Deeper

  • If you're curious how has_secure_password works under the hood, and don't mind stretching your Ruby skills, spend a few minutes reviewing the has_secure_password source. It's well-documented and a lot less code than you might expect. And it's always good to at least have a high-level understanding of how something works, especially if you're entrusting it to do something as important as securing user passwords.

  • Care to guess the 50 most common passwords? Check out this list and an important lesson in why it's vital that you securely store user passwords. (Yes, the bcrypt gem is more robust because it uses one-way cryptographic hashing algorithm.)

  • Not to scare you too much, but here are 6 million more reasons to be super-careful with user passwords. (Yup, the algorithm behind the bcrypt gem also uses a salt to avoid these types of vulnerabilities.)

  • If you want to be more strict about e-mail address validation, you might consider using the email_validator gem.

All course material, including videos and source code, is copyrighted and licensed for individual use only. You may make copies for your own personal use (e.g. on your laptop, on your iPad, on your backup drive). However, you may not transfer ownership or share the material with other people. We make no guarantees that the source code is fit for any purpose. Course material may not be used to create training material, courses, books, and the like. Please support us by encouraging others to purchase their own copies. Thank you!

Copyright © 2005–2024, The Pragmatic Studio. All Rights Reserved.