Bảo mật API bằngToken based trong Rails 5

Vu
Edumall Engineering
6 min readNov 11, 2019
  1. Introduction

Với một ứng dụng Rails 5 API-only, việc authentication phổ biến đang là token-based. Trong bài viết này, mình sẽ nói về cách triển khai token-based vào một ứng dụng Rails 5 API-only.

1.1 What is Token-based Authentication?

Xác thực bằng token (Token-based authentication hoặc JSON Web token authentication) là một cách mới để xác thực người dùng khi truy cập vào ứng dụng. Nó thay thế cho xác thực bằng session (session-based authentication).

Điểm khác nhau lớn nhất giữa 2 hình thức xác thực này là khi xác thực bằng session ta cần phải tạo mới 1 bản ghi trên server ứng với mỗi lần người dùng login. Còn với token-based là không phụ thuộc vào server, nó không lưu lại một thông tin gì lên server. Việc của nó là tạo 1 mã token duy nhất với mỗi lần có request đến.

Không giống session-based, token-based tiếp cận không liên kết với người dùng thông qua việc đăng nhập mà thông qua mã token duy nhất để kiểm soát các giao dịch.

1.2 Benefits of Token-base Authentication?

  • Cross-domain / CORS

Sử dụng token-based sẽ cho phép call AJAX đến mọi server, mọi domain

  • Không phụ thuộc(Stateless)

Ta không cần phải lưu thông tin gì cả vì chính mã token đã chưa thông tin để xác thực rồi.

  • Không ràng buộc(Decoupling)

Mã token có thể sinh ở bất kì đâu và API cũng có thể gọi từ mọi nơi bằng 1 lệnh xác thực duy nhất.

  • Mobile ready

Cookies là một vấn đề khi lưu trữ thông tin người dùng trong các ứng dụng di động, nhưng với việc sử dụng token-based việc này trở lên đơn giản.

  • CSRF(Cross Site Request Forgery)

Vì cơ chế xác thực không dựa vào việc lưu cookies nên nó không thể bị tấn công bằng CSRF.

  • Hiệu năng (Perfomance)

Ở phía server, việc query một sesssion trên database chắc chắn sẽ tốn thời gian hơn việc tính toán một mã HMACSHA256 để xác thực token và phân tích nội dung của nó. Do đó việc dùng token-based authentication sẽ nhanh hơn cách truyền thống.

1.3 How does Token-based authentication work?

how to token-based authentication work?

Cách hoạt động của token-based authentication rất đơn giản. Đầu tiên người dùng sẽ gửi đến server thông tin đăng nhập. Nếu thông tin đó đúng, server sẽ tạo ra một mã HMACSHA256 duy nhất (Json Web Token). Sau đó người dùng lưu lại và mỗi lần gửi request đến server sẽ gửi kèm theo mã token đó. Sau đó server sẽ so sánh mã người dùng gửi với mã đã lưu ở database để xác thực.

2. Setting up Token-based authentication with Rails 5

Tạo mới một ứng dụng rails api với lệnh:

rails new api-app --api

Với tham số --api ứng dụng tạo ra sẽ loại bỏ những phần không cần thiết cho 1 ứng dụng API như helpers, assets, views. Cần có một số yêu cầu chính để xây dựng 1 ứng dụng token-based:

  • Một model.
  • Một cách mã hoá và giải mã JWT.
  • Phương pháp kiểm tra việc người dùng đã được xác thực hay chưa.
  • Controller, routes để kiểm soát việc creating và logging của user.

Creating the User model

Tạo một model User

rails g model User name email password_digest

Chạy lệnh migration

rails db:migrate

Mật khẩu cần được mã hoá khi lưu vào DB, ta dùng gem bcrypt . Thêm vào gemfile:

# Gemfile
gem 'bcrypt', '~> 3.1'

And install it:

bundle install

Khi cài đặt gem xong, ta thêm vào model:

# app/models/user.rb

class User < ApplicationRecord
has_secure_password
end

Mã hoá và giải mã JWT

Ta sử dụng gem jwt để mã hoá và giải mã HMACSHA256, cài đặt như sau:

# Gemfile
gem 'jwt'

Và chạy lệnh

bundle install

Khi cài đặt xong gem, ta sử dụng thông qua biến global JWT . Sử dụng một singleton class cho việc mã hoá và giải mã là cách tốt nhất vì nó sẽ chỉ sinh một đối tượng duy nhất.

# lib/json_web_token.rb

class JsonWebToken
class << self
def encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end

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

Ở phương thức mã hoá, ta truyền vào 3 tham số: user ID, thời gian expire, và unique key ứng dụng của bạn.

Ở phương thức giải mã, sử dụng mã token và key của ứng dụng để giải mã

Để chắc chắn 2 hàm này luôn sẵn sàng, ta thêm vào config của ứng dụng:

# config/application.rb
module ApiApp
class Application < Rails::Application
#.....
config.autoload_paths << Rails.root.join('lib')
#.....
end
end

Authenticating Users

Để thay thế việc sử dụng method private trong controller ta sử dụng gem simple_command . Gem này còn có tác dụng trong việc tạo các object services nhanh chóng. Việc sử dụng nó tương tự việc sử dụng helper nhưng thuận tiện hơn cho việc kết nối giữa controller và model.

Cài đặt gem bằng cách:

# Gemfile
gem 'simple_command'

và chạy lệnh

bundle install

Để sử dụng trong class ta cần thêm prepend SimpleCommand . Đây là một đoạn lệnh mẫu:

class AuthenticateUser
prepend SimpleCommand

def initialize()
#this is where parameters are taken when the command is called
end

def call
#this is where the result gets returned
end

end

Việc kiểm tra thông tin email và mật khẩu của user và trả về giá trị được thực hiện như sau:

# app/commands/authenticate_user.rb

class AuthenticateUser
prepend SimpleCommand

def initialize(email, password)
@email = email
@password = password
end

def call
JsonWebToken.encode(user_id: user.id) if user
end

private

attr_accessor :email, :password

def user
user = User.find_by_email(email)
return user if user && user.authenticate(password)

errors.add :user_authentication, 'invalid credentials'
nil
end
end

Class private user làm nhiệm vụ kiểm tra email đã tồn tại trong database hay chưa, sau đó kiểm tra password bằng việc mã hoá password qua hàm authenticate đã nói ở trên. Nếu khớp email và password hàm sẽ trả về user, nếu không trả về nil .

Checking User Authorization

Token đã được tạo nhưng ta chưa có cách kiểm tra xem nếu token được gửi cùng 1 request là valid hay không. Cách ta authorization là kiểm tra header của requesst và giải mã token sử dụng hàm decode của JWT singleton. Cách làm như sau:

# app/commands/authorize_api_request.rb

class AuthorizeApiRequest
prepend SimpleCommand

def initialize(headers = {})
@headers = headers
end

def call
user
end

private

attr_reader :headers

def user
@user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
@user || errors.add(:token, 'Invalid token') && nil
end

def decoded_auth_token
@decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
end

def http_auth_header
if headers['Authorization'].present?
return headers['Authorization'].split(' ').last
else
errors.add(:token, 'Missing token')
end
nil
end
end

Implementing into Controllers

Mọi logic để thực hiện JWT token đã xong, việc tiếp theo là thực hiện nó ở controller và gắn với các hành vi của user.

Login Users

# app/controllers/authentication_controller.rb

class AuthenticationController < ApplicationController
skip_before_action :authenticate_request

def authenticate
command = AuthenticateUser.call(params[:email], params[:password])

if command.success?
render json: { auth_token: command.result }
else
render json: { error: command.errors }, status: :unauthorized
end
end
end

Thêm vào routes:

# config/routes.rb
post 'authenticate', to: 'authentication#authenticate'

Authorizing Request

#app/controllers/application_controller.rb
class ApplicationController < ActionController::API
before_action :authenticate_request
attr_reader :current_user

private

def authenticate_request
@current_user = AuthorizeApiRequest.call(request.headers).result
render json: { error: 'Not Authorized' }, status: 401 unless @current_user
end
end

Does It work?

Đầu tiên ta tạo mới 1 user

User.create!(email: 'example@mail.com' , password: '123123123' , password_confirmation: '123123123')

Ta cần 1 resource để test chức năng xác thực:

rails g scaffold Item name:string description:textrails db:migrate

Ta chạy server trên local, và test lệnh lấy mã token:

curl -H "Content-Type: application/json" -X POST -d '{"email":"example@mail.com","password":"123123123"}' http://localhost:3000/authenticate

Thử truy cập vào 1 items khi không có dùng mã token:

$ curl http://localhost:3000/items
{"error":"Not Authorized"}

Thử truy cập vào items và dùng mã token đã lấy ở trên:

curl -H “Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1NjYwMzY1NTN9.F_byOWBNNx5msHfCnxBuhQ3ubFXzfwEUrzSxV4Cqhpo” http://localhost:3000/items
[]

Awesome! Everything works.

--

--