Giter VIP home page Giter VIP logo

phase-4-password-protection-readme's Introduction

Password Protection

Learning Goals

  • Explain why it's a bad idea to store passwords in plaintext
  • Write code to store and verify hashed, salted passwords
  • Use Rails' has_secure_password to store and authenticate user login credentials securely

Introduction

It's quite difficult to manage passwords securely. About once a month, there is another big hack in the news, and all the passwords and credit cards from some poor site show up on the dark web.

Rails provides us with tools to store passwords securely so that even if our database is compromised, no one can gain access to users' actual passwords.

The problem with passwords

Let's imagine a SessionsController#create method that does very simple authentication. It goes like this:

def create
  user = User.find_by(username: params[:username])
  if params[:password] == user.password
    session[:user_id] = user.id
    render json: user, status: :created
  else
    render json: { error: "Invalid username or password" }, status: :unauthorized
  end
end

We find the user in the database by their username, check to see if the provided password is equal to the password stored in the database, and, if it is, set user_id in the session.

This is tremendously insecure because you then have to store all your users' passwords in the database, unencrypted.

Never do this.

Even if you don't care about the security of your site, people have a strong tendency to reuse passwords. That means that the inevitable security breach of your site will leak passwords which some users also use for Gmail. Your users table probably has an email column. This means that, if I'm a hacker, getting access to your database has given me the Internet equivalent of the house keys and home address for some (probably surprisingly large) percentage of your users.

Hashing Passwords

So how do we store passwords if we can't store passwords?

Instead of storing users' passwords in plain text, we store a hashed version of them. A hash is a fixed-length output computed by feeding a string to a hash function. Hash functions have the property that they will always produce the same output given the same input.

A helpful analogy for a hash function is making a smoothie. If I put the exact same ingredients into the blender, I'll get the exact same smoothie every time. But there's no way to reverse the operation, and get back the original ingredients from the smoothie.

Hash functions work in a similar way: given the same input, they'll always produce the same output; and there's no way to reverse the output and recreate the original input.

You could even write a hash function yourself. Here's a very simple one:

# dumb_hash(input: string) -> number
def dumb_hash(input)
  input.bytes.reduce(:+)
end

This dumb_hash function just finds the sum of the bytes that comprise the string. It satisfies the criterion that the same string always produces the same result. (It doesn't quite meet the "fixed-length output" requirement for hashes, but for demo purposes, it'll do.)

We could imagine using this function to avoid storing passwords in the database. Our User model and SessionsController might look like this:

# app/models/user.rb
class User < ApplicationRecord

  # takes a plaintext password and stores a hashed version as a password_digest
  def password=(new_password)
    self.password_digest = dumb_hash(new_password)
  end

  # checks if the hashed plaintext password matches the password_digest
  def authenticate(password)
    return nil unless dumb_hash(password) == password_digest
    self
  end

  private

  # the hashing method
  def dumb_hash(input)
    input.bytes.reduce(:+)
  end
end

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    user = User.find_by(username: params[:username])
    if user&.authenticate(params[:password])
      session[:user_id] = user.id
      render json: user, status: :created
    else
      render json: { error: "Invalid username or password" }, status: :unauthorized
    end
  end
end

Note: &. is known in Ruby as the "safe navigation operator". If user is nil, it will return nil; if not, it will call the .authenticate method on user. It would be similar to writing user && user.authenticate(params[:password]).

In this world, we have saved the password hashes in a password_digest column in the database. We are not storing the passwords themselves.

With the code above, a user's password is set by calling user.password = *new_password*. Presumably, our UsersController would do this, but we're not worrying about that for the moment.

dumb_hash is, as its name suggests, a pretty dumb hash function to use for this purpose. It's a poor choice because similar strings hash to similar values. If my password was Joshua, you could log in as me by entering the password Jnshub. Since 'n' is one less than 'o' and 'b' is one more than 'a', the output of dumb_hash would be the same. This is known as a collision. With dumb_hash as our hashing function, there would be many, similar, variants of our Joshua password (many collisions) that could be used successfully to access the account, making our authentication process much less secure.

Unfortunately, collisions are inevitable when you're writing a hash function, since hash functions usually produce either a 32-bit or 64-bit number, and the space of all possible strings is much larger than either 2**32 or 2**64. Fortunately, however, smart people who have thought about this a lot have written a lot of different hash functions that are well-suited to different purposes. And nearly all hash functions are designed with the quality that strings that are similar, but not the same, will hash to significantly different values.

Ruby internally uses MurmurHash, which produces better results for this:

'Joshua'.hash
# => 2365460548529352617

'Jnshub'.hash
# => -2645381839118209905

But Murmur still isn't ideal, because while it does not produce collisions so readily, it is still not difficult to produce them if that's what you're trying to do.

Instead, Rails uses a library called BCrypt. BCrypt is designed with these properties in mind:

  1. BCrypt hashes similar strings to very different values.
  2. It is a cryptographic hash. That means that, if you have an output in mind, finding a string which produces that output is designed to be "very difficult." "Very difficult" means "even if Google put all their computers on it, they couldn't do it."
  3. BCrypt is designed to be slow. It is intentionally computationally expensive.

The last two features make BCrypt a particularly good choice for passwords. (2) means that, even if an attacker gets your database of hashed passwords, it is not easy for them to turn a hash back into its original string. (3) means that, even if an attacker has a dictionary of common passwords to check against, it will still take them a considerable amount of time to check for your password against that list.

The BCrypt gem is open source, and their documentation has some excellent examples that demonstrate this functionality. If you're interested in exploring more, their docs and source code are a great resource.

Salt

But what if our attackers have done their homework?

Say I'm a hacker. I know I'm going to break into a bunch of sites and get their password databases. I want to make that worth my while.

Before I do all this breaking and entering, I'm going to find the ten million most common passwords and hash them with BCrypt. I can do around 1,000 hashes per second, so that's about three hours. Maybe I'll do the top five hundred million just to be sure.

It doesn't really matter that this is going to take long time to run — I'm only doing it once. Let's call this mapping of strings to hash outputs a "rainbow table".

Now, when I get your database, I just look and see if any of the passwords in it are in my rainbow table. If they are, then I know the password.

Going back to our smoothie analogy, this would be the equivalent of someone taking all the possible combinations of smoothie ingredients and running them through the blender to create a giant collection of smoothies. By tasting all the smoothies, they could figure out which original ingredients were used to make the smoothie they're trying to identify.

The solution to the rainbow table problem is salting our passwords. A salt is a random string prepended to the password before hashing it. It's stored in plain text next to the password, so it's not a secret. But the fact that it's there makes an attacker's life much more difficult: it's very unlikely that I constructed my rainbow table with your particular salt in mind, so I'm back to running the hash algorithm over and over as I guess passwords. And, remember, BCrypt is designed to be expensive to run.

Let's update our User model to use BCrypt:

# Gemfile:
gem 'bcrypt'

# app/models/user.rb
class User < ActiveRecord::Base

  # generate a salted + hashed password and save it to password_digest
  def password=(new_password)
    salt = BCrypt::Engine::generate_salt
    # => $2a$12$UW5etUc/o1YL4sSdeTBPku
    self.password_digest = BCrypt::Engine::hash_secret(new_password, salt)
    # => $2a$12$UW5etUc/o1YL4sSdeTBPkueUWwNIPNdQNAwzuSGkS3L5coBKMMZHm"
  end

  # check the plaintext password against the salted + hashed password
  def authenticate(password)
    # Salts generated by generate_salt are always 29 chars long.
    salt = password_digest[0..28]
    # compare the saved password_digest against the plaintext password by running the plaintext password through the same hashing function
    return nil unless BCrypt::Engine::hash_secret(password, salt) == self.password_digest
    self
  end
end

Our users.password_digest column really stores two values: the salt and the actual return value of BCrypt. We just concatenate them together in the column and use our knowledge of the length of salts — generate_salt always produces 29-character strings — to separate them.

After we've loaded the User, we find the salt which we previously stored in their password_digest column. We run the password we were given in params through BCrypt along with the salt we read from the database. If the results match, you're in. If they don't, no dice.

Rails Makes It Easier

You don't have to deal with all this yourself. Rails provides a method called has_secure_password that you can use on your Active Record models to handle all of this. It looks like this:

class User < ApplicationRecord
  has_secure_password
end

To use the has_secure_password macro, you'll need to add gem 'bcrypt' to your Gemfile if it isn't there already.

When using has_secure_password, Rails will use the bcrypt gem to hash and salt all passwords on the User model.

The has_secure_password method also provides two new instance methods on your User model: password and password_confirmation. These methods don't correspond to database columns! Instead, to make these methods work, your users table must have a password_digest column:

create_table :users do |t|
  t.string :username
  t.string :password_digest

  t.timestamps
end

These two instance methods enable you to easily include password and password confirmation fields in a signup (or password reset) form. has_secure_password handles these fields by adding a before_save hook to your model that compares password and password_confirmation. If they match (or if password_confirmation is nil), the user is saved and the hashed version of the password is stored in the password_digest column of the database, pretty much exactly like our example code before did.

Under the hood, has_secure_password calls upon an Active Record helper method, validates_confirmation_of. As such, as with other Active Record validator methods, when the fields don't match and the validation fails, an ActiveRecord::RecordInvalid exception will be raised. You can handle this exception by using rescue or rescue_from.

All together, the code implementing the signup functionality of our very secure app might look like this:

function SignUp({ onLogin }) {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [passwordConfirmation, setPasswordConfirmation] = useState("");

  function handleSubmit(e) {
    e.preventDefault();
    fetch("/signup", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        username,
        password,
        password_confirmation: passwordConfirmation,
      }),
    })
      .then((r) => r.json())
      .then(onLogin);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="username">Username:</label>
      <input
        type="text"
        id="username"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <label htmlFor="password">Password:</label>
      <input
        type="password"
        id="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <label htmlFor="password_confirmation">Confirm Password:</label>
      <input
        type="password"
        id="password_confirmation"
        value={passwordConfirmation}
        onChange={(e) => setPasswordConfirmation(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  );
}
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def create
    user = User.create(user_params)
    if user.valid?
      render json: user, status: :created
    else
      render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
    end
  end

  private

  def user_params
    params.permit(:username, :password, :password_confirmation)
  end
end
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    user = User.find_by(username: params[:username])
    if user&.authenticate(params[:password])
      session[:user_id] = user.id
      render json: user, status: :created
    else
      render json: { error: "Invalid username or password" }, status: :unauthorized
    end
  end
end
# app/models/user.rb
class User < ActiveRecord::Base
  has_secure_password
end

Conclusion

When dealing with users' passwords, it's important for security that we never store passwords in our database directly in plain text. Instead, we can use a trusted library like BCrypt to help keep our users' passwords safe.

Check For Understanding

Before you move on, make sure you can answer the following questions:

  1. What setup steps do you need to complete to use BCrypt in your Rails app?
  2. What two things does BCrypt do to secure passwords?

Resources

phase-4-password-protection-readme's People

Contributors

ahimmelstoss avatar annjohn avatar blake41 avatar brunoboehm avatar drakeltheryuujin avatar franknowinski avatar genericlady avatar gj avatar gormanjp avatar heavenlyboheme avatar ihollander avatar jlboba avatar lawrend avatar lizbur10 avatar maxwellbenton avatar mendelb avatar pletcher avatar queerviolet avatar sgharms avatar sophiedebenedetto avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

phase-4-password-protection-readme's Issues

Easy to misread this phrase: "but not the same hash"

Canvas Link

https://learning.flatironschool.com/courses/5188/pages/password-protection?module_item_id=399410

Concern

This phrase is logically correct, but easy to misread. It took me some mental processing to get the idea:
strings that are similar but not the same hash to significantly different values.

That is, I read this as "the same hash", treating "hash" as a noun rather than a verb.

Additional Context

No response

Suggested Changes

Maybe change this:
strings that are similar but not the same hash to significantly different values.

to this?:
strings that are similar but not the same will hash to significantly different values.

yes, it's more wordy (ugh), but "hash" reads clearly as a verb now.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.