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
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
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
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)
or to the middleware in config/application.rb when you use api-only application:
config.middleware.use ActionDispatch::Session::CookieStore, expire_after: 7.days
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
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
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
- Flavio Copes - JWT authentication: When and how to use it
- Jan Brennenstuhl— Stateless JWT authentification
- Sophie DeBenedetto — Great short article about adding JWT to Rails
Top comments (1)
Nice one !!