Sessions

Now that we can create new accounts (signing up), we should enable "signing in". How does signing in work?

Every time the client sends a request, it comes with a token. If the token matches with the one we have in our database, that means the user is authorized.

In fact, whenever the browser sends a request, it will also send along the cookies. A cookie is a small piece of text stored on a user's computer by their browser. Each time the web browser interacts with a web server, it will pass the cookie information to the web server.

Cookies belong to its own domains only. So, cookies of example_1.com will not be sent to example_2.com.

We can set cookies to store information for use in later pages. So, what we usually do is:

  1. create a token (a code like i75spJzfBMgyHovue4ZKnw)
  2. store this token in our database (server side)
  3. set this token inside a cookie in client's browser (client side)
  4. every time, the client makes a request to the server, it will automatically check if the token matches

So, how do we store tokens in the database. Well, we usually call them sessions. When you log in, the user has started a new session. When the user logs out, the session ends.

So, let's create a Session table to track when user logs in and out.

$ rails g model session

In db/migrate/xxxx_create_sessions.rb,

class CreateSessions < ActiveRecord::Migration[5.0]
  def change
    create_table :sessions do |t|
      t.string :token
      t.belongs_to :user, index: true, foreign_key: true

      t.timestamps
    end
  end
end

We have the attribute token, so we can compare this to the browser cookie. What's t.belongs_to :user? Effectively, this Rails method will help you generate the user_id attribute in the database, so you can associate each session with its user. index: true is to index this attribute in this database. foreign_key: true is to ensure referential integrity in the database level.

Why is Indexing important? https://stackoverflow.com/questions/1108/how-does-database-indexing-work What's Referential Integrity? https://en.wikipedia.org/wiki/Referential_integrity ActiveRecord and Referential Integrity http://guides.rubyonrails.org/active_record_migrations.html#active-record-and-referential-integrity

Every time a new session is created, we want it to automatically generate its token.

In app/models/session.rb,

class Session < ApplicationRecord
  belongs_to :user

  before_validation :generate_session_token

  private

    def generate_session_token
      self.token = SecureRandom.urlsafe_base64
    end
end

SecureRandom.urlsafe_base64 generates a random URL-safe base64 string, meaning that it's a random string that's ok to be passed via the URL.

Some characters are not "safe" to passed via URL due to encoding issues: https://perishablepress.com/stop-using-unsafe-characters-in-urls/

We also need to make sure the two models are associated correctly:

In app/models/user.rb,

class User < ApplicationRecord
  has_many :sessions
end

Now that we have both the sessions and users model, let's create an API endpoint to log in a user (effectively, this is the same as "Log In" on the website).

In config/routes.rb,

Rails.application.routes.draw do
  # ...

  # USERS
  post '/users' => 'users#create'

  # SESSIONS
  post '/sessions' => 'sessions#create'

  # ...
end

In app/controllers/sessions_controller.rb,

class SessionsController < ApplicationController
  def create
    @user = User.find_by(username: params[:user][:username])

    if @user and @user.password == params[:user][:password]
      session = @user.sessions.create
      cookies.permanent.signed[:todolist_session_token] = {
        value: session.token,
        httponly: true
      }

      render json: {
        success: true
      }
    else
      render json: {
        success: false
      }
    end
  end
end

Let's go over this line by line:

  1. The user sends a request with the username and password to log in
  2. With the username, we can find if the user exists with username being the identifier, hence @user = User.find_by(username: params[:user][:username])
  3. if @user checks if the user exists, if not, it will render success: false
  4. Otherwise, it will check if the login password matches with the one we have in our database, hence @user.password == params[:user][:password]
  5. If the password matches, we will create a new session session = @user.sessions.create which will also automatically generate a new token stored inside.
  6. We permanently set the cookie's key todolist_session_token to the value of the session token session.token.
  7. render success: true

This will generate a token. Store that token in the database (server side). Store that token in the client's browser (client side). And, we say that the user has logged in.

The next time user makes another request, we can check the cookie and compare with our database. How about we make an endpoint that specifically check if the user is logged in or not?

We will use the endpoint /authenticated.

In config/routes.rb,

Rails.application.routes.draw do
  # ...

  # USERS
  post '/users' => 'users#create'

  # SESSIONS
  # ...
  get '/authenticated' => 'sessions#authenticated'

  # ...
end

In app/controllers/sessions_controller.rb,

class SessionsController < ApplicationController
  def create
    # ...
  end

  def authenticated
    token = cookies.signed[:todolist_session_token]
    session = Session.find_by(token: token)

    if session
      user = session.user

      render json: {
        authenticated: true,
        username: user.username
      }
    else
      render json: {
        authenticated: false
      }
    end
  end
end

cookies.signed[:todolist_session_token] retrieves the cookie and the value of the key todolist_session_token from the request. Once we get the value of the token, we try to find it in our database Session.find_by(token: token). If it's found, meaning that session exists, then the user is authenticated, and vice versa.

Awesome! So, we just store the same code in two different places. We compare them every time the user makes a request. If they matches, then we know the user is indeed THE USER.

We can log in and check if we are logged in. How about logging out? Similarly, we can create another API endpoint to do that. In this case, the session in our database will be destroyed, so the tokens will no longer match. In fact, you can't find that session in our database anymore.

In config/routes.rb,

Rails.application.routes.draw do
  # ...

  # USERS
  post '/users' => 'users#create'

  # SESSIONS
  # ...
  delete '/sessions' => 'sessions#destroy'

  # ...
end

In app/controllers/sessions_controller.rb,

class SessionsController < ApplicationController
  def create
    # ...
  end

  def authenticated
    # ...
  end

  def destroy
    token = cookies.signed[:todolist_session_token]
    session = Session.find_by(token: token)

    if session and session.destroy
      render json: {
        success: true
      }
    end
  end
end

Similarly, we retrieve the cookie, get the token value, find the session that matches the token, and destroy that session. The user has now logged out, and the session has ended.

References:

results matching ""

    No results matching ""