JWT and using JWT in Rails

Với sự xuất hiện của Single Page Applications(SPA) và mobile app, các API dần dần trở thành tiên phong trong quá trình phát triển web. Chúng ta thường phát triển các API để hỗ trợ cho các SPA và mobile app, vì vậy API chiếm vị trí quan trọng trong quá trình phát triển web. Token-based authentication là một trong những cơ chế authenticate được ưa chuộng nhất hiện nay. Tuy nhiên, các tokens dễ bị tấn công bằng nhiều cách khác nhau. Để khắc phục, một trong những cách để thực hiện, là hướng đến một giải pháp nhằm tạo ra các token có độ an toàn, bảo mật cao, tránh bị đánh cắp giữa các hệ thống khác nhau. JSON Web Tokens(JWT) đã được tạo ra nhằm xây dựng bộ tiêu chuẩn cơ sở cho việc handle và xác minh token làm cho quá trình trao đổi giữa các hệ thống khác nhau một cách an toàn.

WHAT IS JWT?

JWTs mang những thông tin(claims) thông qua JSON, vì vậy nó có tên là JSON Web Tokens. JWT là 1 tiêu chuẩn mở (RFC 7519) định nghĩa cách thức truyền tin an toàn giữa các thành viên bằng 1 đối tượng JSON. Thông tin này có thể được xác thực và đánh dấu tin cậy nhờ vào "chữ ký" của nó. Phần chữ ký của JWT sẽ được mã hóa lại bằng HMAC hoặc RSA. Nó được implement trên hầu hết những ngôn ngữ lập trình phổ biến hiện nay. Do vậy, ta có thể dễ dàng sử dụng và trao đổi trong các hệ thống được thực hiện trong các nền tảng khác nhau. JWT thực chất là 1 đoạn string, vì vậy chúng có thể dễ dàng được truyền thông qua URL hoặc HTTP header, chúng cũng mang theo những thông tin trong phần payload và signatures.

Anatomy of a JWT

Một JWT gồm 3 đoạn string được ngăn cách nhau bởi dấu "."

aaaaa.bbbbbbb.ccccccc

Header

Phần đầu tiên là header, phần thứ hai là payload và phần cuối là signatures.

Phần header gồm 2 thành phần:

  • Kiểu của token, ví dụ như JWT (để phân biệt với JWS hay JWE)
  • Thuật toán mã hóa sẽ dùng cho cái Token của chúng ta
{
  "typ": "JWT",
  "alg": "HS256"
}

Đoạn header trên sẽ được mã hóa bằng base64 và ta thu được kết quả phần header của JWT là:

aeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload

Phần thứ hai của JWT là payload. Phần này sẽ mang những thông tin thú vị trong token, còn được gọi bằng cái tên khác là JWT Claims. Claims có 3 kiểu là: private, public và registered.

1, Registered Claims là những claims mà tên của chúng được khai báo nhưng không bắt buộc phải sử dụng, ví dụ như: iss, sub,aud...Là những thông tin được quy đinh ở trong IANA JSON Web Token Claims registry. Chúng bao gồm: Chú ý rằng các khóa của claim đều chỉ dài 3 ký tự vì mục đích giảm kích thước của Token:

  • iss (issuer): tổ chức phát hành token
  • sub (subject): chủ đề của token
  • aud (audience): đối tượng sử dụng token
  • exp (expired time): thời điểm token sẽ hết hạn
  • nbf (not before time): token sẽ chưa hợp lệ trước thời điểm này
  • iat (issued at): thời điểm token được phát hành, tính theo UNIX time
  • jti: JWT ID

2, Private Claims là phần thông tin thêm dùng để truyền qua giữa các máy thành viên. Ví dụ:

{
  "sub": "1234567890",
  "name": "paduvi",
  "admin": true
}

3, Public Claims chứa thông tin mà chúng ta tạo ra mỗi khi authenticate như: username, thông tin của user... Ví dụ:

“https://www.abc.com/jwt_claims/is_admin”: true

Chúng ta có thể tạo một payload mẫu như sau:

{
  "iss": "sitepoint.com",
  "name": "Devdatta Kane",
  "admin": true
}

Và nó sẽ được mã hóa thành:

ew0KICAiaXNzIjogInNpdGVwb2ludC5jb20iLA0KICAibmFtZSI6ICJEZXZkYXR0YSBLYW5lIiwNCiAgImFkbWluIjogdHJ1ZQ0KfQ

Signatures

Phần thứ 3 của JWT và cũng là phần quan trọng nhất: signatures. Nó là phần chữ kí được tạo thành bằng cách kết hợp 3 thành phần: header, payload và secrets. Phần Header + Payload sẽ được tổng hợp và mã hóa lại bằng một giải thuật encode nào đó, càng phức tạp càng tốt như HMACSHA256 chẳng hạn với "secret" từ phía server-side secret. Giống như sau:

require "openssl"
require "base64"

var encodedString = Base64.encode64(header) + "." + Base64.encode64(payload);
hash  = OpenSSL::HMAC.digest("sha256", "secret", encodedString)

Vì chỉ có server biết được secret. Nên không ai có thể giả mạo để giải mã được signatures.

Phần chữ ký của chúng ta sau khi được mã hóa sẽ trông như sau:

2b3df5c199c0b31d58c3dc4562a6e1ccb4a33cced726f3901ae44a04c8176f37

Bây giờ, chúng ta đã có đầy đủ 3 phần của JWT. Tổng hợp lại ta được:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ew0KICAiaXNzIjogInNpdGVwb2ludC5jb20iLA0KICAibmFtZSI6ICJEZXZkYXR0YSBLYW5lIiwNCiAgImFkbWluIjogdHJ1ZQ0KfQ.2b3df5c199c0b31d58c3dc4562a6e1ccb4a33cced726f3901ae44a04c8176f37

Using JWT in Rails

JWT có những thư viện trên hầu hết các nền tảng và Ruby cũng không phải ngoại lệ. Chúng ta sẽ tạo một Rails app đơn giản, sử dụng gem devise cho authenticate và gem JWT cho việc tạo và xác minh JWT tokens.

    rails new jwt_on_rails

Sau khi app được khởi tạo xong, tạo Home controller để sử dụng cho việc check authenticate:

    rails g controller Home index

Và thu được code của Home controller:

    class HomeController < ApplicationController
     def index

     end
   end

Map trong config/routes.rb:

    Rails.application.routes.draw do
      get 'home' => 'home#index'
    end

Check server:

    rails s

Tiếp theo, chúng ta add 2 gem devise và jwt vào Gemfile:

    gem 'devise'
    gem 'jwt'

Và tiến hành bundle:

    bundle install

Tạo files config cho devise:

    rails g devise:install

Tạo Devise User model và migrate database

    rails g devise User
    rake db:migrate

Đây là lúc chúng ta tích hợp JWT vào ứng dụng. Đầu tiên, tạo một lớp có tên JsonWebToken trong lib/json_web_token.rb. Lớp này sẽ chứa logic encode và decode cho JWT tokens:

    class JsonWebToken
      def self.encode(payload)
        JWT.encode(payload, Rails.application.secrets.secret_key_base, 'HS256')
      end

      def self.decode(token)
        return hIndifferentAccess.new(JWT.decode(token, Rails.application.secrets.secret_key_base, true, { algorithm: 'HS256' })[0])
      rescue
        nil
      end
    end

Ở đây, mình sử dụng giải thuật HS256(HMAC) để mã hóa, các bạn có thể dùng các giải thuật khác như: RSASSA, ECDSA, RSASSA-PSS...

Sau đó, add một khởi tạo cho việc include lớp JsonWebToken trong config/initializers/jwt.rb:

    require 'json_web_token'

Bây giờ, chúng ta sẽ thêm một số method trong ApplicationController, chúng sẽ được sử dụng trong AuthenticateController:

    class ApplicationController < ActionController::Base
      attr_reader :current_user

      protected
      def authenticate_request!
        unless user_id_in_token?
          render json: { errors: ['Not Authenticated'] }, status: :unauthorized
          return
        end
        @current_user = User.find(auth_token[:user_id])
      rescue JWT::VerificationError, JWT::DecodeError
        render json: { errors: ['Not Authenticated'] }, status: :unauthorized
      end

      private
      def http_token
          @http_token ||= if request.headers['Authorization'].present?
            request.headers['Authorization'].split(' ').last
          end
      end

      def auth_token
        @auth_token ||= JsonWebToken.decode(http_token)
      end

      def user_id_in_token?
        http_token && auth_token && auth_token[:user_id].to_i
      end
    end

Chúng ta đã add method authenticate_request! như là before_filter mỗi khi user credential. Tiếp theo chúng ta sẽ tạo AuthenticationController để handle tất cả authenticate request đến API Trong app/controllers/authentication_controller.rb:

    class AuthenticationController < ApplicationController
      def authenticate_user
        user = User.find_for_database_authentication(email: params[:email])
        if user.valid_password?(params[:password])
          render json: payload(user)
        else
          render json: {errors: ['Invalid Username/Password']}, status: :unauthorized
        end
      end

      private

      def payload(user)
        return nil unless user and user.id
        {
          auth_token: JsonWebToken.encode({user_id: user.id}),
          user: {id: user.id, email: user.email}
        }
      end
    end

Sau đó update routes:

    Rails.application.routes.draw do
      post 'auth_user' => 'authentication#authenticate_user'
      get 'home' => 'home#index'
    end

Add before_filter vào Home controller:

    class HomeController < ApplicationController
      before_filter :authenticate_request!

      def index
        render json: {'logged_in' => true}
      end
    end

Bây giờ, ta tạo một user mẫu để test cơ chế authenticate trong rails console:

    rails c
    User.create(email:'[email protected]', password:'changeme', password_confirmation:'changeme')

Khi bạn request đến server bằng lệnh:

curl http://localhost:3000/home

thì server sẽ trả về:

{"errors":["Not Authenticated"]}

Bây giờ, nếu ta authenticate lại và nhận một JWT cho những lần request tiếp theo:

curl -X POST -d email="[email protected]" -d password="changeme" http://localhost:3000/auth_user

Bạn sẽ nhận được một response thành công kèm JWT với thông tin user đã được add thêm:

{"auth_token":"token_nhận_được","user":{"id":1,"email":"[email protected]"}}

Sử dụng auth_token đó và request đến ./home:

    curl --header "Authorization: Bearer token_nhận_được" http://localhost:3000/home

Và nhận được response thành công như sau:

{"logged_in":true}

Store token at client side

Các giải pháp để lưu trữ token ở phía client có một vài lựa chọn giúp bạn tìm được sự lựa chọn tốt nhât. Hãy điểm qua các option sau:

Option 1 - Web Storage (localStorage or sessionStorage)

Ưu điểm:

  • Trình duyệt không tự động include bất kì cái gì từ web storage vào trong http request. Vì vậy nó không thể bị tấn công bằng CSRF
  • Chỉ có thể được truy cập bằng javascript và chạy trên cùng domain.
  • Cho phép truyền token vào HTTP(phần header Authorization với Bearer schema)
  • Dễ dàng để pick các request cần authenticate

Nhược điểm

  • Không thể được truy cập bằng javascript chạy trên 1 sub-domain(một giá trị được viết trên example.com không thể được đọc trên sub.domain.com)
  • Có khả năng bị tấn công XSS
  • Để thực hiện authenticate, bạn chỉ có thể sử dụng trình duyệt hay các thư viện API cho phép bạn custome request(truyền token vào Authorization header)

Usage

Sử dụng localStorage hoặc sessionStorage để lưu và get token khi request được thực hiện:

localStorage.setItem('token', 'asY-x34SfYPk'); // write
console.log(localStorage.getItem('token')); // read

Option 2 - HTTP-only cookie

Ưu điểm:

  • Không bị tấn công bằng XSS
  • Trình duyệt tự động include token vào request
  • Cookie có thể được tạo tại main domian và được sử dụng ở cả các sub-domain

Nhược điểm:

  • Có nguy cơ bị tấn công bằng CSRF
  • Cần cân nhắc và luôn lưu ý việc sử dụng cookie tại các sub-domain
  • Việc chọn riêng các request cần include cookie hoàn toàn có thể làm được nhưng dễ gây lộn xộn
  • Bạn có thể gặp phải một số vấn đề giữa những ràng buộc của trình duyệt và cookie
  • Phía server cần validate cookie cho việc authenticate chứ không phải là Authorization header

Usage

Bạn không cần làm gì tại client side vì trình duyệt sẽ tự động làm tất cả

Option 3 - Javascript accessible cookie ignored by server-side

Ưu điểm:

  • Không bị tấn công bằng CSRF (vì nó bị ignore bởi server)
  • Cookie có thể được tạo tại main domian và được sử dụng ở cả các sub-domain
  • Cho phép truyền token vào HTTP(phần header Authorization với Bearer schema)
  • Dễ dàng để pick các request cần được authenticate

Nhược điểm:

  • Có thể bị tấn công bằng XSS
  • Nếu không cẩn thận thì trình duyệt sẽ tự động include cookie vào những request không cần thiết
  • Để thực hiện authenticate, bạn chỉ có thể sử dụng trình duyệt hay các thư viện API cho phép bạn custome request(truyền token vào Authorization header)

Usage

Sử dụng document.cookie để lưu trữ cũng như get token khi request được thực hiện:

document.cookie = "token=asY-x34SfYPk"; // write
console.log(document.cookie); // read

Nói chung, option 1 nên được lựa chọn và sử dụng vì:

  • Khi bạn tạo một web app, bạn cần deal với XSS, luôn luôn độc lập với nơi lưu trữ token
  • Không sử dụng cookie để authenticate, do vậy sẽ không lo một cuộc tấn công CSRF

Tham khảo: https://jwt.io/introduction/ https://www.sitepoint.com/introduction-to-using-jwt-in-rails/ https://github.com/jwt/ruby-jwt https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage