JWT authentication trong rails

Ngày nay với sự phát triển mạnh mẽ của các ứng dụng mobile và Single Page Applications (SPA) thì việc viết API cho các ứng dụng trên trở nên vô cùng quan trọng. Trong đó việc bảo mật thông tin luôn được tính đến đầu tiên khi viết API. Việc xác thực dựa vào token (Token-based authentication) là một cách phổ biến nhất , tuy nhiên vẫn có nhiều cách để có thể tấn công và xuyên thủng bảo mật , lấy đi token qua đó dễ dáng tấn công ứng dụng của chúng ta. Hôm nay chúng ta sẽ thử tìm hiẻu 1 biện pháp bảo mật khá hữu hiệu: JWT (JSON Web Tokens). JWT được thiết kế để xác thực trong đó có thể sử dụng trao đổi ở nhiều hệ thống khác nhau.

JWT là gì?

JWT mang thông tin dưới dạng JSON, có thể sử dụng ở hầu hết các ngôn ngữ lập trình phổ biến hiện nay nên nó có thể dễ dàng được sử dụng từ hệ thống này mang sang hệ thống khác mà không gặp vấn đề gì.

JWT là một chuỗi các ký tự đã được mã hoá nên có thể dễ dàng truyền theo dưới dạng param trên URL hoặc truyền trong header.

Cấu trúc của JWT

JWT được hép từ 3 phần và cách nhau bởi dấu chấm.

xxxxx.yyyyyy.zzzzz

Phần thứ nhất là header, phần 2 là payload và phần thứ 3 là signature

Header: bao gồm 2 phần

  • kiểu token (ví dụ: JWT)
  • kiểu thuật toán băm ( VD: HS256 ) -> header
{
  "typ": "JWT",
  "alg": "HS256"
}

và header sau khi mã hoá base64:

aeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload: phần này mang nhiều thông tin quan trọng cuả token, được chia làm 3 phần private, public, và registered.

  • registered: phần tên riêng của mỗi token, có thể có hoặc không
  • private: là phần thoả hiệp giữa 2 bên client và server cần xác thực, chú ý cẩn thận với phần này.
  • public : ta có thể tạo các thông tin xác thực thông qua phần này. ví dụ:
{
  "iss": "sitepoint.com",
  "name": "Devdatta Kane",
  "admin": true
}

Và sau khi mã hoá base64:

ew0KICAiaXNzIjogInNpdGVwb2ludC5jb20iLA0KICAibmFtZSI6ICJEZXZkYXR0YSBLYW5lIiwNCiAgImFkbWluIjogdHJ1ZQ0KfQ

Signature: Đây là phần quan trọng nhất, Nó được tạo ra bằng cách mã hoá HMACSHA256 từ 2 thành phần ở trên là header, payload và kết hợp với chuỗi secret lấy từ server

require "openssl"
require "base64"

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

Vì secret key được lưu trên server nên sẽ hạn chế việc tác động từ bên ngoài đến payload và mỗi tác động nào thì đều bị server phát hiện thông qua signature.

Phần signatur sẽ có dạng mã hoá như sau:

2b3df5c199c0b31d58c3dc4562a6e1ccb4a33cced726f3901ae44a04c8176f37

Tổng hợp 3 phần header, payload và signature vào ta sẽ được JWT hoàn chỉnh để gửi kèm trong mỗi request

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ew0KICAiaXNzIjogInNpdGVwb2ludC5jb20iLA0KICAibmFtZSI6ICJEZXZkYXR0YSBLYW5lIiwNCiAgImFkbWluIjogdHJ1ZQ0KfQ.2b3df5c199c0b31d58c3dc4562a6e1ccb4a33cced726f3901ae44a04c8176f37
Sử dụng JWT trong rails

Chúng ta sẽ test trên 1 ứng dụng đơn giản có chức năng authenticate user sử dụng gem devise.

Để sử dụng JWT, ta thêm trong Gemfile

gem 'jwt'

rồi bundle install.

Bây giờ ta sẽ tạo class JsonWebToken trong lib/json_web_token.rb. Trong class này ta sẽ viết các function để thực hiện encoding và decoding token

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

  def self.decode(token)
    return HashWithIndifferentAccess.new(JWT.decode(token, Rails.application.secrets.secret_key_base)[0])
  rescue
    nil
  end
end

Trong config/initializes/jwt.rb ta sẽ load thư viện bên trên

require 'json_web_token'

Bây giờ ta sẽ viết các function helper trong application_helper để sử dụng trong toàn ứng dụng

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

Trong đó authenticate_request có thể coi như 1 before_filter , sẽ kiểm tra điều kiện valid của request trước khi chấp nhận bất cứ request nào.

Tiếp đến ta sẽ tạo AuthenticationController để check tất cả các authenticate request đến API

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

Ứng dụng của chúng ta sẽ sử dụng devise để athenticate và tạo ra 1 JWT nếu request hợp lệ

Ta có thể sử dụng authenticate_request! để check mọi request trong ứng dụng thông qua before_filter. Ở đây ta sẽ filter trong home_controller

class HomeController < ApplicationController
  before_filter :authenticate_request!

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

Trước khi test cơ chế làm việc của JWT ta sẽ tạo trước 1 user thông qua console

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

Bây giờ ta sẽ thử request API xem sao

curl http://localhost:3000/home

response trả về nhận được sẽ là

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

Bây giờ ta sẽ tiến hàng authenticate user để nhận được mã JWT

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

response trả về nhận được

{"auth_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.po9twTrX99V7XgAk5mVskkiq8aa0lpYOue62ehubRY4","user":{"id":1,"email":"[email protected]"}}

Bây giờ ta sẽ truy cập lại trang home kèm theo header chứa mã JWT

curl --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.po9twTrX99V7XgAk5mVskkiq8aa0lpYOue62ehubRY4" http://localhost:3000/home

Và ta sẽ nhận được response success

{"logged_in":true}

Kết luận

Chúng ta có thể sử dụng API với bất kỳ ứng dụng viết bằng ngôn ngữ nào bừng cách lưu JWT key vào cookie hoặc đâu đó trong bộ nhớ để gửi nó kèm theo request. Đây là một công cụ khá tiện lợi dễ sử dụng và có tính an toàn cao nên hi vọng bào viết sẽ giúp mọi người áp dụng vào các ứng dụng trong thực tế.