Menu
CoddyTech

Kiểm tra mật khẩu

Thuộc phần Xác thực và bảo mật trong hành trình Node.js của Coddy. Bài học 6 trên 24.

Mỗi lần đăng nhập, hệ thống tạo một khóa từ mật khẩu được nhập, dùng salt đã lưu và độ dài khóa ban đầu. Salt mới sẽ tạo ra khóa khác, ngay cả khi mật khẩu nhập đúng. Hệ thống không bao giờ giải mã mật khẩu gốc từ dữ liệu đã lưu.

// Mã mời được lưu dưới dạng salt.keyHex
const [salt, keyHex] = savedInvite.split('.');
const expected = Buffer.from(keyHex, 'hex');
const attempted = await scryptAsync(inviteCode, salt, 32);

So sánh các chuỗi byte thu được bằng timingSafeEqual sau khi xác nhận độ dài của chúng khớp nhau. Cách này tránh thoát sớm dựa trên nội dung, vốn có thể làm lộ thông tin qua thời gian xử lý. === của Buffer kiểm tra danh tính, còn phép so sánh chuỗi thông thường không đảm bảo thời gian xử lý không đổi. Hàm xử lý mật khẩu của bạn dùng dấu phân cách dấu hai chấm trong bản ghi mật khẩu và khóa dài 64 byte. Tuyến POST /login mà bạn thêm vào dùng kết quả để trả về mã 200 hoặc 401.

challenge icon

Thử thách

Dễ

Bước này mở dự án API ghi chú riêng tư đã chuẩn bị sẵn của khóa học, chứ không phải các tệp dự án từ Never Store Passwords của bạn (bao gồm passwords.js). Phần bạn đã làm trước đó vẫn nằm trong bài học riêng. Hãy đọc các tệp được cung cấp trước khi chỉnh sửa passwords.js. Bản sao này có các hàm trợ giúp băm mật khẩu và những route gỡ lỗi được cung cấp sẵn.

Giờ đây, đăng ký lưu các giá trị băm có salt. Hãy thêm chức năng đăng nhập để kiểm tra mật khẩu với giá trị đã lưu khi đăng ký.

  1. Trong passwords.js, hãy thêm và export một hàm async verifyPassword(password, stored). stored là giá trị đã lưu khi đăng ký, chẳng hạn f2bb...f065:c3f7...dbd2. Trả về true nếu password là mật khẩu đã tạo ra giá trị đó, nếu không thì trả về false:
    • Lấy salt ở trước dấu hai chấm.
    • Băm password với salt đó thành khóa 64 byte, theo cách tương tự như hashPassword.
    • Giải mã khóa đã lưu sau dấu hai chấm từ hệ thập lục phân. Nếu hai khóa có độ dài byte khác nhau, hãy trả về false mà không gọi timingSafeEqual, như trong bài Comparing Key Bytes. Nếu không, hãy trả về kết quả so sánh.
  2. Trong server.js, hãy import verifyPassword bên cạnh hashPassword. Thêm POST /login. Route này nhận cùng nội dung body như khi đăng ký, chẳng hạn {"username": "ada", "password": "correct horse"}.
    • Tìm người dùng có username đó. verifyPassword chạy chậm và trả về một promise, vì vậy hãy chờ hàm này hoàn tất.
    • Nếu người dùng tồn tại và mật khẩu chính xác, hãy phản hồi với mã trạng thái 200 và {"username": "ada"}.
    • Nếu không, hãy phản hồi với mã trạng thái 401 và {"error": "Wrong username or password"}. 401 Unauthorized nghĩa là yêu cầu chưa chứng minh được danh tính của người gửi.
    • Gửi cùng một phản hồi 401 cho cả trường hợp mật khẩu sai lẫn trường hợp chưa có ai đăng ký username đó. Như vậy, nội dung phản hồi sẽ không tiết lộ username nào tồn tại.

Thử nhé: Nhấn Run. Gửi POST /signup với {"username": "ada", "password": "correct horse"}: mã trạng thái 201. Gửi POST /login với cùng nội dung body: mã trạng thái 200 và {"username": "ada"}. Gửi POST /login với {"username": "ada", "password": "wrong horse"}: mã trạng thái 401 và {"error": "Wrong username or password"}. Gửi POST /login với {"username": "nobody", "password": "correct horse"}: cùng mã trạng thái 401.

Tự làm thử

import { randomBytes, scrypt } from 'node:crypto';
import { promisify } from 'node:util';

const scryptAsync = promisify(scrypt);

// Chuyển mật khẩu thành chuỗi mà quá trình đăng ký lưu lại.
export async function hashPassword(password) {
    const salt = randomBytes(16).toString('hex');
    const key = await scryptAsync(password, salt, 64);
    return `${salt}:${key.toString('hex')}`;
}

Tất cả bài học trong Xác thực và bảo mật

Tự luyện tập: Playground Express