NodeJS bảo mật

This Working Group is not responsible for managing or responding to security reports against Node.js itself. That responsibility remains with the Node.js TSC.

Node.js Bug Bounty Program

The program is managed through the HackerOne platform at https://hackerone.com/nodejs with further details.

Xin chào tất cả các bạn, mình là Quân, hôm nay chúng ta sẽ cùng nhau đi tìm hiểu một tính năng bảo mật rất thú vị và phổ biến trong các ứng dụng ngày nay đó là xác thực bảo mật 2 lớp – Two-Factor Authentication (2FA) cũng như làm một cái ứng dụng thực tiễn Demo 2FA chuyên nghiệp bằng NodeJS nhé.

“Bài này nằm trong loạt bài Lập Trình NodeJS từ cơ bản đến nâng cao trên trang blog chính thức trungquandev.com“

Những nội dung có trong bài:

  1. Đào sâu vào lý thuyết một chút
  2. Phân tích & triển khai code ứng dụng 2FA với NodeJS
  3. Demo ứng dụng bảo mật 2 lớp – 2FA Authentication
  4. Chia sẻ full toàn bộ Source Code của bài hôm nay trên Github

1. Đào sâu vào lý thuyết một chút

Vẫn là phong cách quen thuộc của mình, tuy rằng việc đọc lý thuyết khá dễ buồn ngủ nhưng mà trước khi lao đầu vào code thì chúng ta nên phân tích, tìm hiểu kỹ lý thuyết một chút, để có thể hiểu sâu bản chất của việc mà chúng ta đang làm là gì nhé.

Phần lý thuyết này mình sẽ giải thích cho các bạn một cách cực kỳ dễ hiểu về những loại thuật ngữ sau:
Two-Factor Authentication (2FA)
One-time Password (OTP)
HMAC-based One-Time Password (HOTP)
Time-based One-time Password (TOTP)

Chắc hẳn đa phần các bạn đều đã từng gặp trường hợp đăng nhập vào tài khoản facebook hoặc google thì sau bước nhập email + password, các bạn vẫn phải nhập thêm một cái mã token (thường là 6 chữ số và có thời gian hiệu lực 30 giây -> 1 phút) thì mới có thể đăng nhập được vào tài khoản. Có nhiều cách để nhận được mã này ví dụ như thông qua email, số điện thoại hoặc là được gennerate từ một ứng dụng tạo mã bên thứ ba như Google Authenticator hoặc Authy. Hình thức bảo mật trên được gọi là Xác thực 2 lớp – Two-Factor Authentication.

NodeJS bảo mật

Vậy về mặt lý thuyết thì Two-Factor Authentication là gì?

Two-Factor Authentication (thường viết tắt là 2FA hoặc TFA) là một phương pháp xác thực người dùng dựa trên 2 yếu tố, một là mật khẩu (thứ phổ biến nhất) và thứ hai là một thứ mà người dùng sở hữu, có quyền truy cập đến, ví dụ như dấu vân tay, tin nhắn SMS, gửi mã token tới Email hoặc tốt hơn nữa là One-time Password (OTP) (mật khẩu một lần có giới hạn hiệu lực theo thời gian).

One-Time Password sẽ là thứ mà chúng ta bàn đến trong ngày hôm nay, SMS và Email thì mình sẽ làm ở những bài riêng biệt khác sau. Và đúng như cái tên của nó, One-time Password hay còn được viết tắt là OTP là một loại mã token mà chỉ có thể được sử dụng một lần rồi sau đó nó sẽ bị hủy, không được phép sử dụng tới lần thứ hai.

Lúc này, câu hỏi tiếp theo mà chúng ta đặt ra ở đây là, làm sao để đảm bảo chúng ta có thể tạo một mã token mà thỏa mãn điều kiện là duy nhất (unique)?

Và câu trả lời sẽ tiếp tục dẫn dắt chúng ta đến với một khái niệm khác đó là: Thuật toán HMAC-based One-Time Password, hay còn được viết tắt là HOTP.

HMAC-based One-Time Password – HOTP là một thuật toán sinh mã OTP dựa trên hàm băm HMAC_SHA-1, nó sử dụng 2 thành phần: thứ nhất là một Chuỗi Secret cố định, còn thành phần thứ hai là một bộ đếm (Counter) bộ đếm này dùng một cái là “Moving-Factor” (mình tạm dịch ra là một yếu tố di chuyển, các bạn cũng có thể coi nó tương tự một chuỗi random ngẫu nhiên cho dễ hiểu cũng được.)

Để mà đi sâu vào cái HOTP trên thì nó lại là cả một bầu trời kiến thức về thuật toán khác, mình sẽ để link chính thức ở đây cho các bạn tham khảo thêm rồi tập trung tiếp vào nội dung của chúng ta nhé.
https://tools.ietf.org/html/rfc4226

– Tiếp theo, vì kết quả output của hàm băm HMAC_SHA-1 ở trên là một giá trị có độ dài 160 bit = 20 bytes nên chúng ta sẽ cần thêm một bước làm ngắn gọn output để mắt người dùng có thể dễ dàng đọc được. Việc cắt ngắn này sẽ dẫn chúng ta đến với thuật toán TOTP – Time-based One-time Password algorithm để tạo ra chuỗi có độ dài như chúng ta mong muốn, ví dụ "170 795" mà các bạn thường thấy ở app Google Authenticator hoặc Authy.

Time-based One-time Password – TOTP về cơ bản chỉ khác HOTP ở chỗ là TOTP sẽ sử dụng “thời gian” (Time) để làm bộ đếm (Counter) thay vì “Moving Factor” như HOTP. Chính vì việc sử dụng counter là thời gian nên phía Server lẫn Client khi đã có chung Secret Key rồi thì không cần có sự tương tác qua lại nữa. Vì cả 2 phía đều có quyền truy cập vào thời gian. Điều này cũng trả lời luôn cho một thắc mắc khá thú vị mà lâu nay mình vẫn tự hỏi, đó là tại sao khi mình thử tắt mạng, không kết nối internet cho cái điện thoại vậy mà Token sinh ra của mấy cái app Google Authenticator hay Authy vẫn sử dụng được ngon lành chả vấn đề gì =))

Giải thích cụ thể hơn cho các bạn đó là: phía Server sẽ so sánh giá trị token mà người dùng submit từ phía client lên với tất cả các token được sinh ra trong cùng một khoảng thời gian nhất định trên Server. (thường là 30 giây cho đến 1 phút), và dĩ nhiên là nếu trùng nhau thì bạn sẽ pass qua vòng xác thực 2 lớp này. Đọc đến đây nhiều bạn có thể sẽ thắc mắc tiếp là: Ủa thế server và client khác múi giờ (Time zone) thì làm sao mà khoảng thời gian của 2 phía có thể đồng nhất được nhỉ?
Giải pháp là chúng ta có thể convert thời gian của cả 2 phía về dạng Unix Timestamp (hay còn gọi với tên khác là Epoch Time) rồi so sánh chúng. Hiểu một cách đơn giản thì Unix Timestamp là số giây đếm tăng dần từ một điểm thời gian cố định trong quá khứ đó là ngày 01/01/1970 (UTC) 00:00:00
Mình sẽ để link tham khảo về Unix Time cho các bạn tìm hiểu thêm ở đây https://www.unixtimestamp.com/

Mình cũng sẽ để link tài liệu chính thức của TOTP cho các bạn nào muốn tìm hiểu chuyên sâu về thuật toán nhé, chứ nói thật viết ra mớ lý thuyết dài dòng theo ý hiểu của mình tới tận khúc này là mình cũng oải + buồn ngủ lắm rồi =)))
https://tools.ietf.org/html/rfc6238

Và tiếp theo, tới phần thú vị đỡ buồn ngủ hơn rồi, chúng ta sẽ đi code một ví dụ demo bảo mật 2 lớp với NodeJS như tiêu đề của bài viết nhé. Mình có làm giao diện ứng dụng rất sát với thực tế cho các bạn dễ hình dung đấy, cứ kéo xuống dưới là thấy 😀

(Ngoài lề quen thuộc: Cảnh báo này dành cho mấy bạn admin của mấy trang TopDev, TechBlog… chuyên đi copy rồi xào bài, hoặc bất kể trang nào khác mà đã đi copy bài không phải của các bạn thì hãy tôn trọng người viết bài chân chính, tuyệt đối không được xào nấu, chỉnh sửa linh tinh bài viết của mình, cấm xóa những liên kết (link) trong bài của mình cũng như tự ý xóa các câu thoại của mình trong toàn bộ bài viết rồi post lại lên trang của các bạn như kiểu đây là bài của các bạn vậy, mình sẽ thường xuyên dùng tool để check, và nếu phát hiện ra thì cứ đơn giản là chắc chắn sẽ ăn report DMCA nhé.)


2. Phân tích & triển khai code ứng dụng 2FA với NodeJS

Trước khi vào code, mình đã phân tích cũng như vẽ lại workflow tổng quát của ứng dụng cho các bạn dễ hình dung nhất như thế này:

NodeJS bảo mật

Cấu trúc thư mục dự án của chúng ta sẽ trông như sau:

src

controllers

HomeController.js

AuthController.js

helpers

2fa.js

routes

api.js

views

home.html

enable2FA.html

login.html

verify2FA.html

server.js

package.json


Việc khởi tạo ứng dụng nodejs thì mình không làm lại, các bạn có thể xem cách làm ở các bài viết trước của mình tại link bên dưới đây:
https://trungquandev.com/series-lap-trinh-nodejs/

Tiếp theo, trong ví dụ ngày hôm nay, chúng ta sẽ cài đặt 3 module là: express, otplibqrcode

npm install --save express otplib qrcode

Một lưu ý nhỏ nhưng khá quan trọng nữa, code NodeJS trong bài này mình viết là Javascript ES Modules, nên các bạn sẽ thấy có cú pháp import – export thay vì require và module.exports của CommonJS thông thường. Và để code chạy được thì yêu cầu máy các bạn cần phiên bản nodejs tối thiểu là v12.0.0 trở lên. Cụ thể ở thời điểm mình viết bài này thì bản NodeJS LTS là 12.18.3, còn máy của mình thì mình đang dùng bản gần mới nhất là 14.7.0

Các bạn có thể tham khảo bài viết sau của mình về việc Quản lý (upgrade/downgrade) phiên bản NodeJS dễ dàng trên mọi hệ điều hành {MacOS, Linux, Window} nhé.

Và bài viết hướng dẫn cách sử dụng ES Modules – cú pháp import/export trong NodeJS.

Mình sẽ hướng dẫn lần lượt các file code như sau:

File src/helpers/2fa.js

Trong file này mình sử dụng module otplib để viết 3 function đảm nhiệm 3 chức năng độc lập:

  • generateUniqueSecret: Tạo mã Secret Key.
  • generateOTPToken: Tạo mã OTP token.
  • verifyOTPToken: Xác thực (verify) mã OTP token.

Và module qrcode để tạo mã QR gửi về phía client cho user quét mã:

  • /**
     * Created by trungquandev.com's author on 08/10/2020.
     * src/routes/api.js
     */
    import express from 'express'
    import {
      getLoginPage,
      postLogin,
      getEnable2FAPage,
      postEnable2FA,
      getverify2FAPage,
      postVerify2FA,
    } from '../controllers/AuthController.js'
    import { getHomePage } from '../controllers/HomeController.js'
    
    const router = express.Router()
    
    /**
     * Init all APIs on your application
     * @param {*} app from express framework
     */
    const initAPIs = (app) => {
      // Gọi ra trang chủ home page
      router.get('/', getHomePage)
    
      // Trang login
      router.get('/login', getLoginPage)
      router.post('/login', postLogin)
    
      // Trang bật tính năng bảo mật 2 lớp
      router.get('/enable-2fa', getEnable2FAPage)
      router.post('/enable-2fa', postEnable2FA)
    
      // Trang yêu cầu xác thực 2 lớp
      router.get('/verify-2fa', getverify2FAPage)
      router.post('/verify-2fa', postVerify2FA)
    
      return app.use('/', router)
    }
    
    export { initAPIs }
    0: Tạo mã QR Code
/**
 * Created by trungquandev.com's author on 08/10/2020.
 * src/helpers/2fa.js
 */
import qrcode from 'qrcode'
import otplib from 'otplib'

/** Gọi ra để sử dụng đối tượng "authenticator" của thằng otplib */
const { authenticator } = otplib

/** Tạo secret key ứng với từng user để phục vụ việc tạo otp token.
  * Lưu ý: Secret phải được gen bằng lib otplib thì những app như
    Google Authenticator hoặc tương tự mới xử lý chính xác được.
  * Các bạn có thể thử để linh linh cái secret này thì đến bước quét mã QR sẽ thấy có lỗi ngay.
*/
const generateUniqueSecret = () => {
  return authenticator.generateSecret()
}

/** Tạo mã OTP token */
const generateOTPToken = (username, serviceName, secret) => {
  return authenticator.keyuri(username, serviceName, secret)
}

/** Kiểm tra mã OTP token có hợp lệ hay không
 * Có 2 method "verify" hoặc "check", các bạn có thể thử dùng một trong 2 tùy thích.
*/
const verifyOTPToken = (token, secret) => {
  return authenticator.verify({ token, secret })
  // return authenticator.check(token, secret)
}

/** Tạo QR code từ mã OTP để gửi về cho user sử dụng app quét mã */
const generateQRCode = async (otpAuth) => {
  try {
    const QRCodeImageUrl = await qrcode.toDataURL(otpAuth)
    return `
NodeJS bảo mật

Trang bật xác thực 2 lớp “/enable-2fa”: sau khi nhấn button Enable bên dưới thì các bạn sẽ nhận được mã QR Code.

NodeJS bảo mật

Sử dụng Google Authenticator và Authy để quét mã QR ở trên. Lưu ý: như mình đã nói ở phần hai của bài viết, vì Secret Key mà mình demo nó đang được làm mới mỗi khi các bạn tắt server đi bật lại, nên mã QR Code này chỉ đúng trong một phiên xử lý của chúng ta thôi nhé. Nếu có lỡ tắt server đi bật lại thì các bạn sẽ phải tạo lại QR Code và quét lại mã để lấy đúng Token mới.

NodeJS bảo mật
NodeJS bảo mật

Tiếp theo là trang đăng nhập “/login”: các bạn sử dụng đúng username, password đều là “trungquandev” như trong AuthController của mình có note nhé.

NodeJS bảo mật

Mình có làm tính năng check tài khoản nếu chính xác và tài khoản có bật xác thực 2 lớp

/**
 * Created by trungquandev.com's author on 08/10/2020.
 * src/routes/api.js
 */
import express from 'express'
import {
  getLoginPage,
  postLogin,
  getEnable2FAPage,
  postEnable2FA,
  getverify2FAPage,
  postVerify2FA,
} from '../controllers/AuthController.js'
import { getHomePage } from '../controllers/HomeController.js'

const router = express.Router()

/**
 * Init all APIs on your application
 * @param {*} app from express framework
 */
const initAPIs = (app) => {
  // Gọi ra trang chủ home page
  router.get('/', getHomePage)

  // Trang login
  router.get('/login', getLoginPage)
  router.post('/login', postLogin)

  // Trang bật tính năng bảo mật 2 lớp
  router.get('/enable-2fa', getEnable2FAPage)
  router.post('/enable-2fa', postEnable2FA)

  // Trang yêu cầu xác thực 2 lớp
  router.get('/verify-2fa', getverify2FAPage)
  router.post('/verify-2fa', postVerify2FA)

  return app.use('/', router)
}

export { initAPIs }
5 thì sẽ redirect qua trang verify token:

NodeJS bảo mật

Màn hình nhập mã OTP Token để kiểm tra “/verify-2fa” lưu ý nhập đúng mã trên app Google Authenticator hoặc Authy của các bạn nhé:

NodeJS bảo mật

Sau khi kiểm tra token đúng hoặc sai thì mình sẽ demo thông báo về như thế này:

NodeJS bảo mật
NodeJS bảo mật

4. Chia sẻ full toàn bộ Source Code của bài hôm nay trên Github

Vậy là kết thúc bài hôm nay, mình đã hướng dẫn đầy đủ từ lý thuyết cho đến thực hành code về việc xác thực bảo mật 2 lớp và ứng dụng code với NodeJS