We often talk about programmers being lazy with regard to how few keystrokes we can type to get something done. Certainly we aren't alone in that if you look at the annual lists of most popular passwords stolen and made public. Year on year the most popular passwords include "123456", "password", "qwerty" and "1qaz2wsx".
We can't protect our users from choosing crappy passwords that leave themselves vulnerable, especially when they use "123456" for both their bank and their favorite Beef Fan message board (which, unfortunately, was just hacked for the fifth time last week).
Even if we can't force the world into using tools like 1Password or LastPass - utilities that let you remember one very strong password to unlock a vault of crazy, impossible to memorize and computationally expensive to guess passwords like ngNbDvnzaQ$Wwwi(,X66gY - we can do our best to protect them in the case of our own database getting hacked by using best practices for storing and protecting their personal information.
According to the hacker who used a SQL injection to gain access to RockYou.com's database in 2009, over 30%(!) of websites store passwords in plain text! The hack of RockYou's database revealed the plain-text passwords of some 32 MILLION users. As Rails developers we know this is maybe the worst idea, especially when password hashing comes pre-installed in every Rails project.
Baked right into Rails' ActiveModel is a SecurePassword class that makes hashed password storage a breeze, so even if your database hits the black market you won't be opening your users to a world of hurt.
SecurePassword uses the password hashing function bcrypt, which Wikipedia tells us was designed in 1999 by Niels Provos and David Mazières. Bcrypt incorporates hash salting to protect against Rainbow Table attacks, and is adaptive in that over time it can be made to operate more slowly, in order to resist brute-force attacks to increasingly powerful computers.
We include has_secure_password` as a macro on the class that will hold the password in it's database table, for example in a User class.
class User < ActiveRecord::Base
By default the class will validate that a password is present, and offers a password-confirmation attribute to check that a user input their password properly at signup. These validations can be overridden by setting
has_secure_password validations: false
It also imposes a limit of 72 characters on passwords. Bcrypt has the ability to hash passwords up to 72 characters long, so SecurePassword performs a check to ensure user passwords are properly hash-able. Any characters beyond 72 are simple ignored by the hashing algorithm.
In hashing the password, Bcrypt makes the password unrecognizable by running it through the hashing algorithm, so the password: "12345" is stored in the database as $2a$04$057HL/XdEJj5RKUVL8J.8.sbzEosI5nFcO4am5V6nZmmKYtAyKI9S
What does it actually mean to "Sign in" a user?
Since HTTP is a stateless protocol, we need to have something to persist a user's state between requests. For example, without finding a way to persist who is logged in when users navigated between pages they would have to log back in on every page, or they would lose the contents of their cart.
To persist this state we use the concept of a session, a class included in Rails ActionDispatch. A new session will be created by Rails automatically with each new user that connects to an application, or will load an existing session if the user has already used the application. This information is stored locally in a cookie, which opens up sessions to some security vulnerabilities through Session Hijacking, which we won't get in to today.
The actually act of authenticating a log-in attempt will also be carried out by SecurePassword by calling its
#authenticate method on a password submitted as a login attempt.
class SessionsController < ApplicationController
@user = User.find_by(username: user_params[:username])
session[:user_id] = @user.id
The act of logging in to a site seems much fancier than it is in actual implementation. The pages a user does not have access to are not behind a moat, or in a vault, or tucked under a mattress somewhere, their routes are right there next to the pages they do have access too! Admin features, for instance, are likely even written on the same ERB template that Rails uses to render your non-admin user's homepage! All we're doing is relatively simple checks on the level of authorization of a user to see what content their account provides access too.
This is the same concept where view content changes after log-in. A navigation element might say "Log In" for a new user, and after logging in a simple
if statement switches the view to display "Hello, Will!".
The authentication process that does this is a two step check against the database where you persist user login information. First, we check if the database has a user with this username. If one doesn't exist we would send back a generic error message to the user attempting to log in, so as to not provide potential hackers the chance to guess at names in our database and be rewarded with confirmation that they exist.
Secondly, if that user exists we check the password they provided against the hashed and salted password. We pass the provided login attempt password to
@user.authenticate as an argument, and if both the user and password match we are returned the user as an object!
Now that we have an authenticated user, we set the
session[:user_id], which we will store locally by the user and will check against to determine the current user when new requests are sent. We'll do this by "poor-man's caching" the session in a
current_user method and checking it's truthiness in another called
logged_in?, and we point to both with
helper_method right in the ApplicationController in order to make them available across the controllers.
helper_method :logged_in?, :current_user
@current_user ||= USer.find(session[:user_id]) if session[:user_id]
We'll finally make use of that
logged_in? method in our views to check which information to show in a view.
<% if logged_in? %>
Welcome, <%= current_user.name %>
<% else %>
<%= link_to "Log In", login_path %>
<% end %>