DEV Community

Ivan Iliukhin
Ivan Iliukhin

Posted on • Originally published at evanilukhin.com

Rails API + React SPA authentication problem — Authentication by cookies

Introduction

In this series of articles, I’ll cover different ways for a user authentification in systems with separated frontend and backend. As an example, I took my lovely programming language Ruby with RoR, with which I’ve been working already four years, for API and React application, based on CRA template, for separated frontend.

Source code for SPA you can find here. For API — here.

Problem

Imagine that some people request to develop a system for storing the most valuable thing for them — their names. Besides, users love to admire their treasure only personally. For it, they wish that the system must show name only after logging in and must not ask it for one week. Moreover, they are planning to develop GUI and API by different teams, so these parts must be independent applications.

Design — API

A core entity of the API has a model User that contains only three fields:

  • login — string which users do not scare to show;
  • password — stored as a password digest;
  • name — sacred for every user information that we only show when they are authorized.

Design — SPA

The page has only one block, that is show login form if user not authorized and not blank field “Name” above in case of successful authentication.

Let’s go further and consider how to authenticate our users by cookies.

Authentication by cookies

The most common and obvious approach is to use HTTP cookies for storing auth information. Ruby on Rails has two similar mechanisms for working with cookies, it’s cookies themselves and sessions. For cookies, we can set an httponly flag, that protects from xss attacks, domain, and expiration date. Sessions are stored in cookies inside an encrypted string, where an httponly flag is set by default. For this example, I took sessions because the SPA does not read from cookies.

How it works:

  • SPA sends a POST request with login and password
  • API write user.id in the session cookie
  • Component try to get the name of the user sending a request with the session
  • API find a user by user id and if all right return name of this user
  • Component is updated

Let’s dive deeper.

Usually, SPA and API are deployed on different hosts, hence there appears the next problem — how to pass and modify cookies. By default browser does not set cookies from another origin by javascript. But we can easily enable it.

SPA side.

For communicating with a server SPA uses the Fetch API that is provided in a global window scope. For enabling a possibility to send and receive cookies from resources with a different origin. We must set the next option:

  • credentials: ‘include’ — it enables sending cookies for cross-origin requests by default it is set for the same origin;
  • mode: ‘cors’ — allows to work with all headers related to CORS. By default, it allows only for same-origin requests.

Examples you will find further.

Server side.

To enable the supporting of cross-origin requests in RoR, you must add gem rack-cors that provides support for CORS for a Rack middleware. When you create rails application from a generator with API you need only uncomment string “gem ‘rack-cors’” in Gemfile and content of the config file config/initializers/cors.rb. For setting cookies you must set parameter credentials as true. Important notice, it works only if the origin is not a wildcard. For security reason and flexibility I usually set it from environment variables like there:

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins ENV['SPA_ORIGIN']

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      credentials: true
  end
end
Enter fullscreen mode Exit fullscreen mode

Sending and handling requests

After the setting our projects for work with cookies let’s look at how are requests handled.

Post request contains data and cors friendly settings, about I mentioned above.


    const authUrl = apiUrl + 'login'
    let payload = {
      'data': {
        'login': this.state.login,
        'password': this.state.password
      }
    }

    let headers = {
      'Content-Type': 'application/json'
    };

    fetch(authUrl, {
      method: 'POST',
      mode: 'cors',
      cache: 'no-cache',
      headers: headers,
      redirect: 'follow',
      referrer: 'no-referrer',
      body: JSON.stringify(payload),
      credentials: 'include'
    });
Enter fullscreen mode Exit fullscreen mode

Request handled by standard Rails controller. API finds a user and if all right writes user’s id in a session.

class AuthController < ApplicationController
  include ::ActionController::Cookies

  def login
    if params['data']['login'] && params['data']['password']
      user = User.find_by(login: params['data']['login'])
      if user && user.authenticate(params['data']['password'])
        session[:user_id] = user.id
      else
        render json: {message: 'Wrong login or password'}, status: 403
      end
    else
      render json: {}, status: 401
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Next requests for getting the name send this session and controller just read it and sends name.

let username_url = apiUrl + "name";

let headers = new Headers({
  'Content-Type': 'application/json'
});

if(this.state.name === null) {
  fetch(username_url, {
    method: 'GET',
    mode: 'cors',
    headers: headers,
    cache: 'no-cache',
    redirect: 'follow',
    referrer: 'no-referrer',
    credentials: 'include'
  })
  .then(function (response) {
    return response.json();
  })
  .then(myJson => {
    this.setState({name: myJson['name']});
  });
};
Enter fullscreen mode Exit fullscreen mode

..and related controller:

class UsersController < ApplicationController
      include ::ActionController::Cookies
      before_action :find_user

      def name
        if @current_user.present? && @current_user.is_a?(User)
          render json: {name: @current_user.name}
        else
          render json: {message: 'Bad user'}, status: 401
        end
      end

      private

      def find_user
        user_id = session[:user_id]
        @current_user = User.find_by(id: user_id)
      end
end
Enter fullscreen mode Exit fullscreen mode

Pretty simple!

Pros

Security — httponly flag prevents cookies from stealing your auth data by XSS attacks. (I hope that you use https by default).

Simplicity — mechanisms for working with cookies and sessions are proven and exist almost in all frameworks.

Cons

Works only inside with web-browsers.

Top comments (0)