Cách Mình Xử Lý Authentication
Các pattern authentication và authorization để bảo mật ứng dụng web
Bảo mật không phải thứ "làm xong rồi gắn vào sau" được. Nó định hình mọi quyết định thiết kế, từ cách lưu session cho đến cách trả error message.
Phân Biệt Cốt Lõi
Authentication trả lời: "Bạn là ai?"
- Xác minh danh tính (đăng nhập)
- Quản lý thông tin đăng nhập
- Cấp session hoặc JWT
Authorization trả lời: "Bạn được làm gì?"
- Kiểm tra quyền hạn
- Xác minh quyền sở hữu
- Thực thi các quy tắc truy cập
Hai khái niệm này hay bị gộp chung, nhưng thực tế khác nhau hoàn toàn. Một người có thể đã authenticated (biết họ là ai) nhưng chưa authorized (chưa được truy cập tài nguyên đó).
Nguyên Tắc
1. Mặc Định Là Đóng
Mọi route đều được bảo vệ trừ khi mình chủ động mở công khai.
Sai ở đâu thì hay gặp nhất? Xây tính năng trước, tính bảo mật sau. Rồi quên một endpoint, thế là lộ. Cách đúng là bắt đầu với mọi thứ bị khóa -- dùng middleware chặn mặc định, duy trì whitelist các path công khai thay vì blacklist path cần bảo vệ.
Tư duy kiểu "đóng trước, mở sau" thì an toàn hơn nhiều so với "mở hết, rồi đi khóa từng cái."
2. Không Tin Bất Kỳ Gì Từ Client
Client không phải bạn của mình đâu. Coi nó là kẻ tấn công tiềm năng luôn cho chắc.
Không bao giờ tin:
- User ID gửi trong request body ("Tôi là user 123")
- Role claim từ client ("Tôi là admin")
- Hidden form field
- URL parameter cho quyết định authorization
Luôn xác minh trên server:
- Lấy user ID từ session, không phải từ request
- Kiểm tra quyền hạn dựa trên database
- Validate ownership trước mọi thao tác
3. Lọc Dữ Liệu Từ Nguồn
Đừng fetch hết rồi lọc trong code. Lọc ngay trong database query.
Lấy tất cả project → Lọc ra project của user trong code
Nếu bộ lọc có bug thì sao? Toàn bộ project bị lộ. Mỗi query phải bao gồm filter theo user -- database không bao giờ được trả về dữ liệu mà user không nên thấy. Kể cả khi code ứng dụng có bug, tầng database vẫn là lưới an toàn cuối cùng.
4. Ẩn Những Gì Không Được Xem
Khi user cố truy cập thứ họ không nên, mình tiết lộ gì?
"403 Forbidden" nói: "Cái này tồn tại, nhưng bạn không được truy cập."
"404 Not Found" nói: "Cái này không tồn tại (hoặc bạn không được biết nó tồn tại)."
Với tài nguyên thuộc về user, ưu tiên trả 404. Đừng xác nhận rằng user X có tài nguyên mà người request đang dò tìm. Kiểu như -- đừng cho kẻ trộm biết nhà nào có két sắt.
5. Đăng Nhập Rồi Chưa Đủ -- Phải Kiểm Tra Ownership
Nhiều người dừng lại ở bước "user đăng nhập rồi" là xong. Nhưng đăng nhập chỉ là bước đầu thôi.
Pattern cần tuân thủ:
- Kiểm tra: User đã đăng nhập chưa?
- Kiểm tra: Tài nguyên này có thuộc về họ không?
- Lúc đó mới: Thực hiện hành động
Lỗi hay gặp: kiểm tra authentication nhưng quên ownership. User A đổi ID trên URL là truy cập được dữ liệu của User B ngon lành.
Framework Quyết Định
"User nên đăng nhập bằng cách nào?"
Email/Password khi:
- Cần toàn quyền kiểm soát trải nghiệm
- User mong đợi tạo tài khoản riêng
- Đầu tư đúng mức vào bảo mật mật khẩu
OAuth/Social Login khi:
- Muốn giảm ma sát
- Không muốn quản lý mật khẩu
- User đã có tài khoản Google/GitHub/etc
Passwordless (magic link, SMS) khi:
- Mật khẩu tạo quá nhiều ma sát
- Có hệ thống gửi email/SMS đáng tin cậy
- Yêu cầu bảo mật cho phép
Multi-factor khi:
- Tài khoản có giá trị cao hoặc dữ liệu nhạy cảm
- Compliance yêu cầu
- User chấp nhận được thêm bước xác minh
"Session hay token?"
Session (cookie-based) khi:
- Ứng dụng web truyền thống
- Muốn thu hồi đơn giản (xóa session)
- Lưu trữ server-side chấp nhận được
Token (JWT) khi:
- Kiến trúc API-first
- Nhiều client (web, mobile, CLI)
- Cần server stateless
Đánh đổi: Session dễ thu hồi nhưng cần lưu trữ. Token không cần trạng thái nhưng khó thu hồi trước khi hết hạn. Không có lựa chọn nào hoàn hảo -- chọn cái phù hợp với kiến trúc.
"Session nên kéo dài bao lâu?"
Cân nhắc:
- Độ nhạy cảm: Ngân hàng = ngắn. Blog = dài.
- Ma sát cho user: Đăng nhập lại phiền đến mức nào?
- Cửa sổ rủi ro: Session dài hơn = lộ lâu hơn nếu bị xâm phạm
Pattern phổ biến:
- Ứng dụng nhạy cảm: Vài giờ, yêu cầu xác thực lại cho hành động quan trọng
- Ứng dụng bình thường: Vài ngày đến vài tuần
- "Remember me": Vài tuần đến vài tháng, với quyền hạn giảm bớt
Sliding expiration: Reset bộ đếm khi có hoạt động. User đang dùng thì cứ để đăng nhập, hết hoạt động thì mới tính.
"Nên kiểm tra authorization ở đâu?"
Trong middleware cho:
- Truy cập cấp route (trang này yêu cầu đăng nhập)
- Truy cập theo role (phần này yêu cầu admin)
Trong route handler cho:
- Ownership tài nguyên (project này thuộc về bạn)
- Quyền theo hành động cụ thể (bạn được xem nhưng không được sửa)
Trong tầng database cho:
- Lưới an toàn cuối cùng (query luôn filter theo user)
Tốt nhất? Cả ba. Phòng thủ nhiều lớp -- một tầng fail thì tầng kia bắt.
Sai Lầm Phổ Biến
Chỉ Kiểm Tra Phía Client
Ẩn nút không có nghĩa là bảo mật hành động. "User không thấy nút xóa, nên họ không xóa được" -- sai hoàn toàn. Bất kỳ ai cũng gọi API trực tiếp được. Nút chỉ là UI thôi, mà bảo mật thì phải ở server.
Tin User-Supplied ID
Đọc user_id từ request body hoặc URL để quyết định trả dữ liệu gì à? Kẻ tấn công sẽ thử mọi ID luôn. Lấy user ID từ session, không bao giờ từ request.
Enforcement Không Nhất Quán
Thêm auth check tùy hứng, bỏ sót vài route -- chuyện này xảy ra thường xuyên hơn tưởng. Giải pháp: middleware tập trung bảo vệ mặc định, whitelist rõ ràng cho các route công khai. Đừng để việc bảo mật phụ thuộc vào trí nhớ.
Error Message Quá Chi Tiết
"User 'admin' tồn tại nhưng sai mật khẩu." -- vậy là kẻ tấn công biết username đó hợp lệ rồi. Trả message chung chung thôi: "Thông tin đăng nhập không hợp lệ." User hợp lệ tự hiểu, còn kẻ tấn công phải đoán.
Token Quá Nhiều Quyền
Một token có full access và không bao giờ hết hạn -- đây là "chìa khóa vạn năng" mà không ai nên tạo ra.
Thay vào đó:
- Giới hạn scope token theo quyền cụ thể
- Thời gian hết hạn ngắn
- Có khả năng rotation và revocation
Cách Đánh Giá Bảo Mật
Auth đang ổn nếu:
- Giải thích được mọi auth check diễn ra ở đâu
- User chưa đăng nhập không truy cập được route được bảo vệ
- User A không truy cập được dữ liệu của User B
- Đổi ID trên URL không làm lộ dữ liệu
- Error message không tiết lộ chi tiết hệ thống
Auth cần xem lại nếu:
- Không chắc tất cả route đã được bảo vệ
- Không rõ ownership được xác minh ở đâu
- Đã có user báo thấy dữ liệu của người khác
- API endpoint hoạt động mà không cần authentication
- Token không bao giờ hết hạn
Tư Duy Bảo Mật
Giả định bị xâm phạm: Nếu một thứ fail, bán kính thiệt hại là bao nhiêu? Hạn chế được không?
Quyền tối thiểu: Cấp mức truy cập tối thiểu cần thiết. Mở rộng quyền chỉ khi thực sự cần -- chứ đừng cho sẵn "phòng khi cần."
Phòng thủ nhiều lớp: Một tầng fail thì tầng khác bắt. Đừng đặt cược hết vào một chỗ.
Nhật ký kiểm tra: Log các sự kiện authentication. Biết ai đã làm gì, khi nào -- đến lúc cần điều tra thì mới thấy giá trị.
Review thường xuyên: Bảo mật không bao giờ "xong." Review code auth định kỳ, cập nhật dependency, rotate secret. Nước chảy đá mòn -- kẻ tấn công cũng kiên nhẫn lắm.