Cookies

If you haven't already, first read the previous post about Encryption & Data Security.

You may already be aware of the notion of browser cookies - clearing a website's cookies is often a recommendation when a website isn't working properly.  But what does that really mean?

Cookies are comprised of key-value pairs, or, in ruby parlance, a Hash.  This hash of data is stored in the browser's memory.  Since it lives in the user's browser, it's also specific to the user and website.  Cookies are a useful tool that websites can tap into to create a sense of state.

The flash message that we implemented above is using this tool.  When we assign a value to the flash hash (e.g. "notice"), Rails is setting that key and value in the cookies hash as a single-use cookie - it expires after it's read in the very next request.  This is how we see that message after being redirected and then it does not appear again in future requests.

When it comes to users, we want to "remember" who the logged-in user is.  We'll refer to this user as the "current user".  Cookies are the perfect tool for this - when a user successfully authenticates, we can store identifying information for this user as a browser cookie and then check for that cookie on each new request.  If the cookie exists, we know the user is logged-in.  If it doesn't exist, the user is not logged-in.  Let's see how that works in code.

Back in the create action of the sessions controller, let's set a cookie.

def create
  @user = User.find_by({ "email" => params["email"] })
  if @user
    if BCrypt::Password.new(@user["password"]) == params["password"]
      # assign a cookie
      cookies["monster"] = "Me like cookies"

      # login the user
      flash["notice"] = "You've logged in."
      redirect_to "/companies"
    else
      flash["notice"] = "Unsuccessful login."
      redirect_to "/sessions/new"
    end
  else
    flash["notice"] = "Unsuccessful login."
    redirect_to "/sessions/new"
  end
end

Now submit the login form.  After you do, let's view this cookie in the browser.

In chrome, go to the "View" menu, then "Developer", then "Developer Tools".  Chrome has many tools that are incredibly useful for web development.  The panel that opens up (either on the side or the bottom of your chrome window) has several tabs.  Navigate to the "Application" tab.  On the left side, you'll see a "Navigation" list.  Find the "Cookies" submenu and open it up.  You'll see another nested list that includes a url (matching the url in your address bar).  Click on that and you'll see the cookies assigned to this browser and this website.  There you'll find our new "monster" cookie.

Refresh the browser, visit the companies index, click on an individual company, click on an individual contact.  Along with every request you make to this app's domain, the cookie is there.  In other words, this user's experience now has a continuous "monster" state.

This silly cookie isn't particularly useful.  In order to be aware of who the current user is, we need some identifying information about the user - how about the user row's id.

Back in the sessions controller, let's replace the cookie code with this:

cookies["user_id"] = @user["id"]

Go back to the login form and submit it again.  Afterwards, check the list of cookies and you'll see the user_id cookie.  Depending on which user you logged in with, you might see 1 as the value or 2 or whatever the integer id value is for that row in the users table.  Now, on every subsequent request, we know what user (based on their id) is the current user!

Before we go any further, there's a security vulnerability here.  Right click on that cookie in the Chrome developer tools and click "Edit Value".  You can now change the user_id to any value you want.  If you were "logged in" as user 1, maybe you want to be "logged in" as user 2.  By changing that value, you're changing what user our application will think is logged in.  Imagine being logged-in to Amazon and seeing all of your orders.  By changing this value, you're now considered logged-in as someone else entirely and will see their orders.  Not good.

Knowing this, there's a more secure cookie that we can use - an encrypted cookie that is impossible to maliciously manipulate.  Change the cookie code one more time to this:

session["user_id"] = @user["id"]

The session hash is like cookies, but more secure.  Delete the user_id and monster cookies by right clicking on them and click "Delete".  Then submit the login form again.  Check the cookies and you'll notice you can't even identify the user_id cookie.  It's there, but you can't recognize it from the browser.  The next step is to use this cookie value to find the current user.

Current User

Let's start with the action where we're redirected after a successful login - the companies index.  In the companies controller index action, let's find the current user by this cookie.  In /app/controllers/companies_controller.rb, let's write a query:

def index
  @companies = Company.all
  @current_user = User.find_by({ "id" => session["user_id"] })
end

Now we can use the @current_user variable in our view.  We could put this in the companies index.html.erb file, but it might be nice to recognize the current user in the same area where we have the Signup link.  That code lives in the shared layout view (/app/views/layouts/application.html.erb).

<p>
  <a href="/users/new">Sign Up</a>
</p>

While we're here, let's add a link to login.

<p>
  <a href="/sessions/new">Login</a> |
  <a href="/users/new">Sign Up</a>
</p>

And let's acknowledge the current user.

<p>
  <%= @current_user["first_name"] %> |
  <a href="/sessions/new">Login</a> |
  <a href="/users/new">Sign Up</a>
</p>

Refresh the companies index page and take a look at the navbar - there you'll see the current user's first name.  Refresh it again, new request, and the current user's first name still appears.  We can now, at long last, consider the current user as being "logged in"!

Visit a different path, like an individual company's page.  You'll probably get an error.

"undefined method '[]' for nil:NilClass"

This is saying that @current_user is nil and therefore we can't try to read it's first_name column.  Why?  Because we only assigned @current_user in the companies index action.  We want this variable to exist in every action for every resource in our app.  There's a global controller where we can add this type of code that we want for every request.  It's the "application controller" (/app/controllers/application_controller.rb).  Open up that file and let's define a new method:

class ApplicationController < ActionController::Base

  def current_user
  end

end

We want this code to run on every request, so rails has a built-in method for this called before_action.  Add the following code:

class ApplicationController < ActionController::Base
  before_action :current_user

  def current_user
  end

end

The current_user method will now run before (or at the beginning) of every action.  To see this working, let's output some text in that method to the server log:

class ApplicationController < ActionController::Base
  before_action :current_user

  def current_user
    puts "------- running for every request"
  end
end

Refresh the browser and then check the server log and you'll see this output.  So now let's move the @current_user query from the companies index into this method:

class ApplicationController < ActionController::Base
  before_action :current_user

  def current_user
    @current_user = User.find_by({ "id" => session["user_id"] })
  end
end

Now when you refresh the browser and visit any path, you'll see the current user's name.  It works everywhere!

Now that we have access to a current user, we can decide what functionality should be impacted by having authenticated this current user - i.e. authorization (next post).