DEV Community

Ivan Iliukhin
Ivan Iliukhin

Posted on • Originally published at evanilukhin.com

Rails API + SPA authorization — Authorization by JWT

One of the ways to provide authorization is to use JSON Web Tokens or JWT. It is a popular way to implement authorization between different services by logging in only once. In this blog post, I’ll briefly explain what is it, tell about some pitfalls and show how to add in an application.

What is it, and why is it so popular?

Briefly, it’s a signed string that contains some information in a not ciphered form and a verification signature, that proves that information was not changed. JWTs doesn’t protect data from stealing. They are only protected from changing. The closest analogy from the real world is an RFID ID card with the printed on it information. Everyone can see what is written on a card, but only who know secret can change it. Furthermore, someone can steal your card and freely use one. I will not explain in detail about the structure of JWTs, it very clear and short described on the official site.

And why is this approach so popular today?

Most of the modern applications consist of a bunch of microservices developing by different teams. First returns frontend, second provides an API, third gathers statistics of usage, etc., and they communicate with each other. We want to know some information about a request (who made it, user type and etc.) and that this information has not tampered.

There are listed the most popular cases of usage JWTs:

  • authorization between independent SPA and API;
  • providing authorization for different web-applications using only one for an authentification;
  • secured information exchange between services, tokens guarantees that information was not changed during transmission.

JWT is a good solution for authorization but not without some controversies.

Let’s go to nuances of usage and caveats.

Confidential information exchange

This mentioned in almost all posts about JWTs, and I will remind too.

DO NOT USE JSON WEB TOKENS FOR STORING AND PASSING CONFIDENTIAL DATA!
Like passwords, information from official documents and etc. Everyone who has a token sees the content.

Stateless requests

In many articles are spoked of the next advantage of JWTs— stateless. It means that we store all necessary information inside the token, though we can reduce the amount of connection to a central database. It makes sense in the environment with dozens of microservices. But, what if a token was compromised. Yes usually it has an expiration date, but what if we must cancel it right now.

There is a simple solution, unfortunately, it violates the mentioned above advantage. We can store blacklisted tokens in a database (Redis for example) to deny access by using an invalid token. Another good idea is to save user sessions(user id + valid token + token expiration date). It will be a little bit slow, but we can always get the information about how much users are logged in. Furthermore, the application must go to a database every time processing a request, as a consequence recommended to use for a blacklist/session storage an in-memory database with O(1) access time.

In many cases, it is not necessary, however, keep it in mind when you decide to use JWTs for users authentication/authorization.

Time to practice

For this service, I used the most popular authentification solution for the Ruby on Rails — Knock. This gem leverages efforts for creating and decoding tokens. Let’s look at how it easy.

  • add gem to the project link;
  • add finding user by the login to the User model(by default Knock uses email field);
def self.from_token_request(request)
  # Returns a valid user, `nil` or raise `Knock.not_found_exception_class_name`
  login = request.params['auth'] && request.params['auth']['login']
  user = find_by(login: login)
  if user.present?
    return user
  else
    raise Knock.not_found_exception_class_name
  end
end
Enter fullscreen mode Exit fullscreen mode

Create UserTokenController to generate token and UsersController, that returns user’s name, when token is correct and user still exists

module Api
  module Jwt
    class UsersController < ApplicationController
      include Knock::Authenticable

      before_action :authenticate_user

      def name
        render json: {name: current_user.name}
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

and controller, that generates tokens for the User.

module Api
  module Jwt
    class UserTokenController < Knock::AuthTokenController
      skip_before_action :verify_authenticity_token
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Pretty simple!

On this scheme is depicted how it works.

We make POST request with the form-data(login and password in our case) and get token in response. This token can be attached as the bearer authentification token in headers, or just transferred in a request body.

Yes, it looks like a simple and working solution, but what can happen if you decide to use it in a web application(SPA, for example) that works from a browser. A gotten token can be stored by javascript to next usage in headers to local storage or normal cookies(not HTTP only), it means that anyone(third-party javascript libraries, browser extensions, etc.) has access to this token. What is wrong with it? Correct! The token is vulnerable to XSS attacks.

But there is a way to avoid it — cookies with HttpOnly flag. In Rails you can use or sessions (special encrypted cookies, httponly by default) or just cookie with this flag. The main difference between them is next. A session cookie is encrypted by default, a usual cookie you must encrypt manually. I’ll show how to realize it on sessions.

JWT inside session

By default sessions are destroyed right after closing a browser, though if you want to increase their expiration date add this parameter to the session initializer config/initializers/session_store.rb:

Rails.application.config.session_store(:cookie_store, expire_after: 7.days)
Enter fullscreen mode Exit fullscreen mode

or to the middleware in config/application.rb when you use api-only application:

config.middleware.use ActionDispatch::Session::CookieStore, expire_after: 7.days
Enter fullscreen mode Exit fullscreen mode

In case, when you want to store and configure it separately from session use encrypted, httponly cookie.

Next, you should redefine current knock behaviour for creating token:

class UserTokenController < Knock::AuthTokenController
  include ::ActionController::Cookies
  skip_before_action :verify_authenticity_token

  def create
    session[:jwt] = auth_token.to_json
    render body: nil, status: :created
  end
end
Enter fullscreen mode Exit fullscreen mode

As you can see, I skipped CSRF verification, because this API works with SPA with another origin and Knock::AuthToken depends from AuthController::Base, where it enabled by default. In the redefined method create I add token to the session and render nothing with the status code 201.

UsersController will be a little bit more complicated:

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

  def name
    if @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
    jwt = session[:jwt]
    if jwt.present?
      token = JSON.parse(jwt)['jwt']
      user_id = Knock::AuthToken.new(token: token).payload['sub']
      @current_user = User.find_by(id: user_id)
    else
      render json: { message: 'Token not found' }, status: 401
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

In find_user I try to find the token in the session and decipher it. How it looks on scheme:

That’s all. We saw how to authenticate a user by using JWT itself and with combination with cookies using Knock.


P.S. Some useful articles that helped me to understand this topic

Top comments (1)

Collapse
 
panoscodes profile image
Panos Dalitsouris

Nice one !!