← Vibe coding với Next.js

Bài 11 · Nâng cao · 26 phút

Đăng nhập với Better-auth

Biên soạn bởi Nguyễn Anh Tuấn

Trọn vòng tài khoản khách bằng Better-auth: đăng ký, đăng nhập username/mật khẩu, OAuth qua Google và Zalo, và luồng quên mật khẩu (gửi email đặt lại). Khái niệm session, băm mật khẩu (password hashing), vì sao TUYỆT ĐỐI đừng tự chế phần xác thực. Cho Subagent bảo mật rà soát trước khi mở cho người dùng.

Web bán hàng giờ đã nhớ được dữ liệu, nhưng còn thiếu một thứ căn bản: biết khách là ai. Phải có đăng ký và đăng nhập thì mới gắn được ví, đơn hàng và số dư cho đúng người. Việc chứng minh "bạn đúng là chủ tài khoản này" gọi là authentication (xác thực).

Đây là chỗ nhạy cảm bậc nhất: làm sai là lộ tài khoản, lộ mật khẩu, mất niềm tin. Nên bài này không khuyến khích mèo con "sáng tạo" - mà dựa vào công cụ đã được kiểm chứng.

Tự viết đăng nhập nghe đơn giản nhưng đầy cạm bẫy: lưu mật khẩu sai cách, session đoán được, link đặt lại bị lợi dụng... Một lỗ hổng nhỏ ở đây là toang cả sản phẩm. Quy tắc: đừng phát minh lại phần xác thực - dùng thư viện đã được nhiều người dùng và rà soát.

Khoá dùng Better-auth - một thư viện xác thực cho TypeScript/Next.js. Nó lo sẵn các phần khó: băm mật khẩu, session, đăng nhập Google/Zalo, quên mật khẩu. Mèo con mô tả, Claude gắn nó vào app.

  • Xác thực là chỗ nhạy cảm - lỗ hổng nhỏ cũng nguy hiểm.
  • Đừng tự chế: dùng thư viện đã kiểm chứng (Better-auth) an toàn hơn đồ tự viết.
  • Better-auth lo: hashing, session, OAuth (Google/Zalo), quên mật khẩu.

Nguyên tắc số một: không bao giờ lưu mật khẩu nguyên văn. Thay vào đó, mật khẩu được băm (hash) - biến thành một chuỗi rối một chiều - rồi mới lưu. Lúc đăng nhập, hệ thống băm lại mật khẩu vừa nhập và so khớp hai hash:

🔑 Mật khẩumeo123băm 1 chiềuHasha3f9c1b2…lưu🗄️ DBchỉ giữ hash✗ không suy ngược ra mật khẩu gốc
Mật khẩu được băm một chiều rồi mới lưu; database chỉ giữ hash. Lộ database cũng không đọc ra mật khẩu gốc.

Vì băm là một chiều, kể cả khi database bị lộ, kẻ tấn công cũng không suy ngược ra được mật khẩu gốc. Better-auth làm phần này theo chuẩn (argon2/bcrypt) - mèo con không phải tự lo.

Sau khi đăng nhập đúng, chẳng lẽ mỗi lần bấm gì cũng bắt gõ lại mật khẩu? Không. Hệ thống cấp một session - như một tấm vé lưu trong cookie của trình duyệt. Các request sau kèm tấm vé đó, server biết ngay "à, người này đã đăng nhập" mà không hỏi lại mật khẩu.

Trung thực

Tấm vé này phải khó đoán và có hạn. Đăng xuất là huỷ vé. Đây cũng là phần Better-auth lo - mèo con chỉ cần biết khái niệm để mô tả đúng (vd "cho session hết hạn sau 7 ngày") và kiểm lại.

Nhiều khách ngại tạo thêm một mật khẩu mới. Cho họ đăng nhập bằng Google hoặc Zalo tiện hơn nhiều. Cơ chế là OAuth: khách bấm "đăng nhập bằng Google", Google tự hỏi mật khẩu ở trang của Google rồi chỉ báo lại cho site "người này hợp lệ, đây là email".

Điểm cần nhớ

Site của mèo con không bao giờ thấy mật khẩu Google của khách. Đây là cùng kiểu uỷ quyền như khi mèo con kết nối Claude với GitHub ở bài GitHub & vibe trên web/điện thoại: cấp đúng quyền cần, không giao mật khẩu.

Nút "đăng nhập bằng Google/Zalo" không tự chạy. Với MỖI nhà cung cấp, mèo con tạo một ứng dụng OAuth để lấy Client ID + Client Secret, và khai báo redirect URI - địa chỉ Better-auth nhận kết quả trả về. Làm một lần cho mỗi nhà cung cấp.

Google (Google Cloud Console):

  • Tạo một project ở Google Cloud Console.
  • Cấu hình "OAuth consent screen": tên app, email liên hệ, phạm vi cơ bản (email, profile).
  • Tạo "OAuth client ID" loại Web application.
  • Thêm Authorized redirect URI trỏ tới callback Better-auth (vd https://ten-mien/api/auth/callback/google).
  • Copy Client ID + Client Secret, đặt vào .env.

Zalo (Zalo Developers):

  • Tạo một ứng dụng ở Zalo Developers (developers.zalo.me).
  • Bật Zalo Login và khai báo redirect URL trỏ về Better-auth.
  • Lấy App ID + Secret Key, đặt vào .env.
  • Better-auth chưa có provider Zalo sẵn nên nối qua "generic OAuth" - đưa endpoint authorize/token Zalo cung cấp.

.env - khoá OAuth (đổi ten-mien theo của bạn); KHÔNG đẩy lên GitHub

# Redirect URI: khai báo ở Google/Zalo đúng đường dẫn callback Better-auth in ra.
# vd Google:  https://ten-mien/api/auth/callback/google
# (khi dev:   http://localhost:3000/api/auth/callback/google)

GOOGLE_CLIENT_ID="..."
GOOGLE_CLIENT_SECRET="..."
ZALO_APP_ID="..."
ZALO_APP_SECRET="..."

Tài liệu chính hãng

Tạo app theo đúng tài liệu của từng nhà cung cấp (giao diện hay đổi, nên đọc bản mới nhất): Google Cloud Console - console.cloud.google.com Hướng dẫn OAuth của Google - support.google.com/cloud/answer/6158849 Zalo Developers (Zalo Login) - developers.zalo.me Better-auth (social + generic OAuth) - better-auth.com/docs

Mẹo

Mèo con chỉ làm phần TẠO APP và dán Client ID/Secret vào .env; phần nối vào code để Claude lo. Đừng commit .env (đã có trong .gitignore từ bài dữ liệu).

Mật khẩu lộ là mất tài khoản. Thêm lớp thứ hai (2FA - two-factor authentication): ngoài mật khẩu, cần thêm một bằng chứng nữa. Với tài khoản dính tới tiền, khoá bắt buộc khách bật một trong hai:

  • Passkey (WebAuthn): khoá gắn thiết bị + sinh trắc (vân tay/Face ID); không có mã để lộ, chống lừa đảo (phishing) tốt nhất - khuyến nghị.
  • TOTP: mã 6 số đổi mỗi 30 giây trong app authenticator (Google Authenticator/Authy); phương án dự phòng khi không dùng được passkey.
  • Better-auth có sẵn plugin passkey và two-factor (TOTP) - bật là dùng, không tự chế.

Bắt buộc thiết lập ngay sau khi đăng nhập

Khoá đặt luật: đăng nhập thành công mà chưa có 2FA thì chưa cho vào sản phẩm - chặn lại và bắt thiết lập Passkey hoặc TOTP trước (forced enrollment). Với ví tiền, đây là mức tối thiểu.

Quan trọng không kém: mọi thao tác nhạy cảm phải xác nhận lại ngay lúc đó (step-up), không chỉ dựa vào việc đã đăng nhập từ trước - vì máy có thể bị bỏ quên lúc đang mở:

Thao tác nhạy cảmnạp/rút · đổi MK · adminXác nhận lạiPasskey / TOTP?đúng✓ Thực hiện(ghi log)sai✗ Chặnhỏi lại / khoá
Mọi thao tác nhạy cảm (nạp/rút tiền, đổi mật khẩu, thao tác admin) phải xác nhận lại bằng Passkey/TOTP ngay lúc đó - đúng thì thực hiện và ghi log, sai thì chặn.
  • Step-up áp cho: nạp/rút/chuyển tiền, đổi mật khẩu, đổi email, và mọi thao tác admin.
  • Mỗi lần xác nhận ghi log (ai, làm gì, khi nào) để truy vết khi cần.
  • Cho subagent bảo mật rà: đúng các key action nhạy cảm đều có step-up, không sót cái nào.

Khách quên mật khẩu là chuyện thường. Cách đúng không phải gửi lại mật khẩu cũ - database chỉ giữ hash nên cũng không đọc ra được, mà gửi mật khẩu qua email cũng không an toàn. Thay vào đó: gửi một link đặt lại có hạn (vd 30 phút), khách bấm vào và tạo mật khẩu mới.

Quan trọng: reset luôn 2FA

Khoá chọn: đặt lại mật khẩu qua email cũng vô hiệu 2FA cũ (Passkey/TOTP) và bắt khách thiết lập lại. Lý do: mất quyền vào mật khẩu nghĩa là tài khoản có thể đã bị nhòm ngó, nên dựng lại lớp bảo vệ từ đầu cho sạch. Nên cấp sẵn backup codes để khách không bị khoá ngoài.

Trung thực

Email đặt lại (và email biến động khác) nên gửi qua hàng đợi để không bắt khách chờ - phần đó để bài Hàng đợi BullMQ lo. Ở đây chỉ cần nắm: link có hạn, dùng một lần, tạo mật khẩu mới.

Better-auth lưu user và session vào PostgreSQL qua Drizzle - ăn khớp với mô hình dữ liệu mèo con đã dựng. Đại ý khi gắn vào, mô tả cho Claude rất gọn:

Mô tả cho Claude (không phải code mèo con tự viết) - Better-auth + Drizzle

Gắn Better-auth vào app, lưu user/session vào PostgreSQL qua Drizzle.
Bật:
  - đăng ký / đăng nhập bằng email + mật khẩu (băm argon2)
  - đăng nhập Google và Zalo (OAuth)
  - 2FA: passkey (WebAuthn) + TOTP; BẮT BUỘC thiết lập ngay sau đăng nhập
  - xác nhận lại (step-up) cho thao tác nhạy cảm: nạp/rút tiền, đổi mật khẩu, admin
  - quên mật khẩu: gửi link đặt lại có hạn 30 phút; đặt lại mật khẩu thì RESET 2FA
Sau đó cho subagent bảo mật rà phần xác thực này.

Bước tiếp theo

Trước khi mở cho người dùng, hãy cho subagent bảo mật (OWASP) rà lại phần xác thực. Xong xuôi, ta sang phần khách mong nhất: nạp tiền vào ví - Nạp tiền QR qua SePay & idempotency.

Câu hỏi thường gặp

Là một thư viện xác thực cho TypeScript/Next.js, lo phần khó và nhạy cảm của đăng nhập: lưu session, băm mật khẩu, đăng nhập qua Google/Zalo (OAuth), quên mật khẩu. Mèo con mô tả việc, Claude gắn Better-auth vào app thay vì tự viết phần này từ đầu.

Vì xác thực có rất nhiều cạm bẫy: lưu mật khẩu sai cách, session đoán được, lộ token đặt lại... Người viết thiếu kinh nghiệm rất dễ để lại lỗ hổng. Thư viện như Better-auth đã được nhiều người dùng và rà soát, nên an toàn hơn hẳn đồ tự chế. Đây là chỗ "đừng phát minh lại bánh xe".

Mã hoá hai chiều: có khoá thì giải ra lại nội dung gốc. Băm một chiều: từ mật khẩu ra hash thì được, nhưng từ hash KHÔNG suy ngược lại ra mật khẩu. Mật khẩu nên băm (không cần đọc lại bao giờ), chỉ cần băm lại lúc đăng nhập rồi so khớp hai hash.

Không. Mèo con bấm "đăng nhập bằng Google", Google tự hỏi mật khẩu ở trang của Google rồi chỉ báo lại cho site "người này hợp lệ, đây là email". Site không bao giờ thấy mật khẩu Google. Đó là OAuth - cùng kiểu uỷ quyền như khi kết nối Claude với GitHub ở bài trước.

Ở Google Cloud Console: tạo một project, cấu hình "OAuth consent screen", rồi tạo "OAuth client ID" loại Web application. Khai báo redirect URI trỏ về callback của Better-auth (vd https://ten-mien/api/auth/callback/google), copy Client ID + Client Secret đặt vào .env. Link chính hãng có trong callout của bài.

Tạo một ứng dụng ở Zalo Developers (developers.zalo.me), bật Zalo Login và khai báo redirect URL, rồi lấy App ID + Secret Key. Better-auth chưa có sẵn provider Zalo nên cấu hình qua "generic OAuth" - đưa các endpoint authorize/token mà Zalo cung cấp. Nhờ Claude làm phần nối; mèo con chỉ cần tạo app và dán khoá vào .env.

Passkey (WebAuthn) là khoá gắn với thiết bị, mở bằng sinh trắc (vân tay/Face ID) - không có mã nào để lộ hay bị lừa nhập, nên chống lừa đảo (phishing) tốt nhất. TOTP là mã 6 số đổi mỗi 30 giây trong app authenticator; tiện và phổ biến nhưng vẫn có thể bị dụ đọc mã. Khoá khuyến nghị passkey, để TOTP làm dự phòng.

Là yêu cầu xác nhận LẠI danh tính (Passkey/TOTP) ngay tại thời điểm làm một việc nhạy cảm - nạp/rút tiền, đổi mật khẩu, thao tác admin - thay vì chỉ tin vào lần đăng nhập trước. Phòng khi máy bị bỏ quên đang mở phiên: kẻ khác cũng không rút được tiền vì không qua được bước xác nhận lại.

Vì mất quyền vào mật khẩu thường là dấu hiệu tài khoản có thể đã bị xâm phạm. Khoá chọn dựng lại lớp bảo vệ từ đầu: đặt lại mật khẩu thì vô hiệu Passkey/TOTP cũ và bắt thiết lập lại. Để khách không bị khoá ngoài, cấp sẵn backup codes lúc bật 2FA.

Vì database chỉ giữ hash, không đọc ra được mật khẩu cũ (đúng như sơ đồ băm). Mà gửi mật khẩu qua email cũng không an toàn. Cách đúng: gửi một link đặt lại có hạn (vd 30 phút), khách bấm vào và tạo mật khẩu MỚI.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Authentication (xác thực) là gì?

Bài tập về nhà

  1. 1

    Gắn Better-auth

    Bật plan mode, nhờ Claude thêm Better-auth vào app và lưu user/session vào PostgreSQL qua Drizzle; duyệt kế hoạch rồi cho dựng.

    ✅ Hoàn thành khi: Database có bảng user/session của Better-auth; đăng ký được một tài khoản email/mật khẩu để test.

  2. 2

    Đăng ký rồi đăng nhập lại

    Tạo một tài khoản bằng email + mật khẩu, đăng xuất, rồi đăng nhập lại.

    ✅ Hoàn thành khi: Đăng nhập lại vào được, trang hiển thị trạng thái đã đăng nhập (vd tên/email khách).

  3. 3

    Kiểm mật khẩu đã được băm

    Xem bảng user trong database, tìm cột mật khẩu của tài khoản vừa tạo.

    ✅ Hoàn thành khi: Cột mật khẩu là một chuỗi hash (vd bắt đầu $argon2 hoặc $2b$...), KHÔNG phải "meo123" đọc được.

  4. 4

    Tạo Google OAuth app

    Ở Google Cloud Console: tạo project, cấu hình OAuth consent screen, tạo OAuth client ID (Web application), thêm redirect URI của Better-auth; dán Client ID/Secret vào .env rồi nhờ Claude bật đăng nhập Google.

    ✅ Hoàn thành khi: Đăng nhập được bằng một tài khoản Google mà không phải tạo mật khẩu mới ở site của mèo con.

  5. 5

    Tạo Zalo OAuth app

    Ở Zalo Developers: tạo ứng dụng, bật Zalo Login, khai báo redirect URL; lấy App ID + Secret Key đặt vào .env; nhờ Claude nối Zalo qua generic OAuth của Better-auth.

    ✅ Hoàn thành khi: Đăng nhập được bằng tài khoản Zalo (hoặc tới được màn hình cấp quyền của Zalo nếu app chưa được duyệt).

  6. 6

    Luồng quên mật khẩu

    Nhờ Claude bật chức năng quên mật khẩu; bấm "quên mật khẩu", nhận link đặt lại, tạo mật khẩu mới.

    ✅ Hoàn thành khi: Đặt được mật khẩu mới qua link và đăng nhập lại bằng mật khẩu mới đó.

  7. 7

    Bật 2FA & bắt buộc thiết lập

    Nhờ Claude bật plugin passkey + two-factor (TOTP) của Better-auth, và cấu hình bắt buộc thiết lập 2FA ngay sau khi đăng nhập.

    ✅ Hoàn thành khi: Đăng nhập xong bị bắt thiết lập Passkey hoặc TOTP; chưa thiết lập thì chưa vào được phần chính.

  8. 8

    Step-up cho thao tác nhạy cảm

    Nhờ Claude bắt buộc xác nhận lại Passkey/TOTP khi đổi mật khẩu (và sau này là nạp/rút tiền).

    ✅ Hoàn thành khi: Thử đổi mật khẩu → bị yêu cầu xác nhận Passkey/TOTP trước khi cho đổi.

  9. 9

    Forgot-password reset 2FA

    Đặt lại mật khẩu qua email rồi thử đăng nhập lại.

    ✅ Hoàn thành khi: 2FA cũ không còn dùng được; hệ thống bắt thiết lập Passkey/TOTP mới (và backup codes nếu có).

  10. 10

    Cho subagent bảo mật rà

    Gọi subagent rà bảo mật (OWASP) kiểm phần xác thực trước khi mở cho người dùng.

    ✅ Hoàn thành khi: Có danh sách phát hiện; mỗi mục được sửa hoặc mèo con xác nhận lý do an toàn.