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à ...
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.2b3df5c199c0b31d58c3dc4562a6e1ccb4a33cced726f3901ae44a04c8176f37Sử 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:'a@a.com', 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="a@a.com" -d password="changeme" http://localhost:3000/auth_user
response trả về nhận được
{"auth_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.po9twTrX99V7XgAk5mVskkiq8aa0lpYOue62ehubRY4","user":{"id":1,"email":"a@a.com"}}
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ế.