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. Show Node.js Bug Bounty ProgramThe 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útVẫ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: 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. 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. 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é. – 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ụ 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ỉ? 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 =))) 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 NodeJSTrướ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: 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: Tiếp theo, trong ví dụ ngày hôm nay, chúng ta sẽ cài đặt 3 module là: express, otplib và 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:
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/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 ` |