Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 34 — Permission & Auth Spec

Viết PRD & Product Spec cho PM Bài 34/60

Mở đầu — vì sao bài này quan trọng

Có một loại bug khiến PM mất ngủ nhiều nhất: không phải bug làm sai tính năng, mà là bug làm lộ dữ liệu của người dùng này cho người dùng khác. Bạn ship một màn hình "đơn hàng của tôi", và một tuần sau support báo lên rằng anh A nhìn thấy địa chỉ và số điện thoại của chị B. Đó không phải lỗi UI. Đó là lỗi của phần spec mà bạn đã bỏ trống: permission và auth.

Phần lớn PM viết spec rất kỹ cho "happy path" — người dùng đăng nhập đúng, có quyền, làm đúng thao tác. Nhưng spec về danh tính (authentication — bạn là ai) và quyền hạn (authorization — bạn được làm gì) thường bị bỏ qua hoặc viết một dòng chung chung kiểu "user phải đăng nhập". Câu đó vô nghĩa với engineer, vì nó không trả lời: đăng nhập bằng cách nào? Token sống bao lâu? Hết hạn thì sao? Người dùng có vai trò gì thì thấy gì? Nếu thiếu quyền thì ẩn nút hay hiện nút rồi báo lỗi?

Bài này dạy bạn viết phần Permission & Auth Spec đủ chặt để engineer không phải tự đoán, và đủ rõ để khi có sự cố bảo mật, người ta không hỏi "tại sao spec không nói gì về cái này". Đây là phần spec mà nếu viết tốt, không ai khen bạn; nhưng nếu viết tệ, nó có thể khiến công ty mất khách hàng, dính phạt, hoặc lên báo.

Khái niệm cốt lõi

Trước hết, phân biệt rạch ròi hai khái niệm mà rất nhiều người dùng lẫn lộn — đây là nền tảng của cả bài.

Authentication (auth-n) — Xác thực: trả lời câu hỏi "Bạn là ai?". Đây là quá trình hệ thống xác nhận danh tính người dùng: nhập email/mật khẩu, đăng nhập Google, bấm vào magic link trong email. Kết quả của authentication là một session hoặc token chứng minh "người này đúng là người họ nói".

Authorization (auth-z) — Phân quyền: trả lời câu hỏi "Bạn được làm gì?". Khi đã biết bạn là ai, hệ thống quyết định bạn có được xem trang admin không, có được xóa đơn hàng người khác không, có được xuất báo cáo tài chính không.

Một spec tốt phải tách bạch hai phần này. Nhầm lẫn chúng dẫn đến những câu mơ hồ như "chỉ user hợp lệ mới truy cập được" — "hợp lệ" ở đây là đăng nhập (auth-n) hay là có quyền (auth-z)? Hai thứ khác nhau hoàn toàn.

Auth flow types — các kiểu luồng xác thực

Trong phần spec, bạn cần chỉ rõ hệ thống hỗ trợ những phương thức đăng nhập nào. Mỗi phương thức có đánh đổi riêng:

  • Email + password — đường lui cơ bản (fallback baseline). Đơn giản, ai cũng hiểu, không phụ thuộc bên thứ ba. Nhưng kéo theo cả gánh nặng: quy tắc độ mạnh mật khẩu, luồng quên mật khẩu, lưu hash (không bao giờ lưu mật khẩu thô — dùng bcrypt/argon2). Hầu như mọi sản phẩm đều cần phương thức này làm phương án dự phòng khi OAuth lỗi.
  • OAuth / Social login — đăng nhập qua Google, Apple, Facebook. Người dùng không phải tạo mật khẩu mới, tỷ lệ hoàn tất đăng ký cao hơn. Lưu ý bối cảnh Việt Nam: Zalo Login rất phổ biến và nên cân nhắc cho sản phẩm hướng đại chúng trong nước, trong khi Apple Sign-In là bắt buộc nếu app iOS của bạn đã có social login khác (yêu cầu của App Store). Với OAuth, spec phải nói rõ: lấy những scope nào (email, tên, avatar?), và xử lý ra sao khi một email đã đăng ký bằng password giờ lại đăng nhập bằng Google (account linking).
  • Magic link — gửi một đường link đăng nhập một lần qua email, người dùng bấm vào là vào thẳng, không cần mật khẩu. Trải nghiệm mượt, không có mật khẩu để quên hay bị lộ. Đổi lại, spec cần định nghĩa: link sống bao lâu (thường 10–15 phút), dùng một lần rồi vô hiệu, và xử lý khi người dùng mở link trên thiết bị khác với thiết bị yêu cầu.
  • OTP / SMS — mã số gửi qua SMS, phổ biến ở Đông Nam Á vì nhiều người dùng quen số điện thoại hơn email. Cần spec rõ giới hạn gửi lại (resend cooldown) để tránh tốn phí SMS và chống lạm dụng.

Permission model — mô hình phân quyền

Có hai mô hình hay gặp, hãy chọn một và viết rõ trong spec:

RBAC (Role-Based Access Control): gán người dùng vào các vai trò (admin, editor, viewer), mỗi vai trò có một bộ quyền cố định. Đơn giản, dễ hiểu, phù hợp 80% trường hợp.

ABAC (Attribute-Based Access Control): quyền được tính dựa trên thuộc tính — ví dụ "user được sửa đơn hàng nếu đơn hàng đó thuộc về cửa hàng mà user là chủ". Linh hoạt hơn nhưng phức tạp hơn nhiều.

Một công cụ cực kỳ hữu ích để đưa vào spec là permission matrix — bảng ma trận liệt kê: hàng là vai trò, cột là hành động, ô là Có/Không. Bảng này xóa tan mọi mơ hồ.

Session & token lifecycle — vòng đời phiên

Đây là phần PM hay quên nhất. Spec cần trả lời: token sống bao lâu (access token thường ngắn, 15–60 phút; refresh token dài, vài ngày đến vài tuần)? Khi access token hết hạn, hệ thống tự làm mới ngầm (silent refresh) hay bắt đăng nhập lại? Khi người dùng đổi mật khẩu hoặc bấm "đăng xuất khỏi mọi thiết bị", các phiên cũ bị thu hồi ra sao?

Tình huống thực tế

Ví dụ 1 — Lỗ hổng "đổi số ID" tại một sàn TMĐT ở Việt Nam

Một sàn thương mại điện tử tầm trung (giả định: "ChợViệt", khoảng 2 triệu người dùng) ship tính năng xem chi tiết đơn hàng qua URL dạng /orders/48213. PM đã spec rất kỹ giao diện, trạng thái loading, empty state. Nhưng phần auth spec chỉ ghi: "User phải đăng nhập để xem đơn hàng".

Vấn đề: spec chỉ kiểm tra authentication (đã đăng nhập), chứ không kiểm tra authorization (đơn hàng này có thuộc về bạn không). Một người dùng tò mò thử đổi 48213 thành 48214 trên thanh URL — và thấy ngay đơn hàng của người lạ, đầy đủ tên, địa chỉ, số điện thoại. Đây là lỗ hổng kinh điển tên là IDOR (Insecure Direct Object Reference).

Sự cố lan trên mạng xã hội, đội ngũ phải tắt tính năng khẩn cấp trong đêm. Khi truy nguyên, không ai sai về mặt code — engineer làm đúng những gì spec yêu cầu. Spec mới là thứ thiếu một dòng sống còn: "Server phải kiểm tra order.user_id == current_user.id trước khi trả dữ liệu; nếu không khớp, trả 404 (không phải 403, để không lộ sự tồn tại của đơn hàng)."

Bài học: authentication không bao giờ thay thế được authorization. Mỗi khi spec nhắc đến "xem dữ liệu thuộc về một ai đó", phải có một dòng nói rõ kiểm tra quyền sở hữu ở phía server.

Ví dụ 2 — Magic link bị "vào nhầm tài khoản" ở một startup fintech

Một startup ví điện tử (giả định: "PayNhanh") quyết định dùng magic link làm phương thức đăng nhập chính để giảm ma sát. PM viết: "Gửi link đăng nhập qua email, user bấm vào là vào app."

Trong thực tế, nhiều người dùng yêu cầu link trên điện thoại nhưng vô tình mở email trên máy tính dùng chung ở văn phòng. Tệ hơn, vì spec không định nghĩa thời gian hết hạn, link sống vô thời hạn — một email cũ từ ba tuần trước vẫn đăng nhập được. Có trường hợp một nhân viên mở lại email cũ trong hộp thư chung và vào thẳng ví của đồng nghiệp.

Khi viết lại spec, đội bổ sung: link hết hạn sau 10 phút, dùng một lần (bấm xong là token vô hiệu ngay), và gắn với thiết bị/trình duyệt yêu cầu — nếu mở ở ngữ cảnh khác thì yêu cầu xác nhận thêm bằng OTP. Với fintech, họ còn thêm một lớp: thao tác chuyển tiền luôn cần xác thực lại (step-up authentication) dù phiên đăng nhập còn hiệu lực.

Bài học: một phương thức auth "mượt" mà thiếu spec về vòng đời (hết hạn, dùng một lần, ràng buộc ngữ cảnh) sẽ biến tiện lợi thành lỗ hổng. Với sản phẩm tiền bạc, hãy tách riêng "đăng nhập vào app" và "cho phép thao tác nhạy cảm".

Ví dụ 3 — Permission matrix cứu một dự án SaaS B2B

Một công ty SaaS quản lý nhân sự (giả định: "HR Cloud") xây tính năng phân quyền cho khách doanh nghiệp: mỗi công ty có Admin, Manager, và Employee. Ban đầu spec mô tả quyền bằng văn xuôi, kiểu "Manager có thể xem lương của nhân viên trong team mình, nhưng không sửa được; Admin thì làm được tất cả..." — dài ba đoạn và đầy chỗ mâu thuẫn. Engineer hỏi: "Manager có xem được lương của Manager khác không?" Spec không trả lời được.

PM ngồi lại và dựng một permission matrix:

Hành độngEmployeeManagerAdmin
Xem hồ sơ của mình
Xem lương người trong teamKhôngChỉ-xem
Sửa lươngKhôngKhông
Mời thành viên mớiKhôngKhông
Xuất báo cáo toàn công tyKhôngKhông
Bảng này giải quyết mọi câu hỏi trong một cái liếc mắt. Engineer biết chính xác cần kiểm tra gì, QA biết chính xác cần test bao nhiêu trường hợp (5 hàng × 3 cột = 15 ca kiểm thử rõ ràng).

Bài học: với phân quyền phức tạp, một cái bảng đáng giá hơn ba trang văn xuôi. Permission matrix vừa là spec, vừa là test plan, vừa là tài liệu cho support.

Hướng dẫn từng bước

Đây là quy trình viết một Permission & Auth Spec gọn gàng cho tính năng của bạn:

Bước 1 — Liệt kê các vai trò (actors). Ai sẽ tương tác với tính năng này? Khách vãng lai (chưa đăng nhập), người dùng đã đăng nhập, admin, nhân viên nội bộ, hệ thống bên ngoài (API). Đừng quên "khách chưa đăng nhập" — họ thấy gì khi vào trang yêu cầu quyền?

Bước 2 — Chọn và ghi rõ auth flow. Liệt kê các phương thức đăng nhập được hỗ trợ (email/password, OAuth nào, magic link, OTP). Với mỗi phương thức, ghi rõ luồng thành công và luồng thất bại. Đừng quên các luồng phụ: quên mật khẩu, account linking khi cùng email dùng hai phương thức.

Bước 3 — Dựng permission matrix. Hàng là vai trò, cột là từng hành động cụ thể (xem, tạo, sửa, xóa, xuất). Điền Có/Không/Có-điều-kiện vào từng ô. Nếu có "điều kiện" (chỉ với dữ liệu của mình), ghi chú rõ điều kiện đó.

Bước 4 — Định nghĩa hành vi khi thiếu quyền. Đây là quyết định UX quan trọng: khi người dùng không có quyền, bạn ẩn chức năng đi (họ không thấy nút) hay hiện rồi chặn (thấy nút, bấm vào báo "bạn không có quyền")? Quy tắc chung: ẩn nếu việc lộ ra gây rò rỉ thông tin nhạy cảm; hiện-rồi-chặn nếu muốn gợi mở để người dùng nâng cấp gói. Ghi rõ mã lỗi trả về (401 chưa đăng nhập, 403 không đủ quyền, 404 để giấu sự tồn tại).

Bước 5 — Spec vòng đời session/token. Access token sống bao lâu, refresh ra sao, đăng xuất thu hồi gì, đổi mật khẩu có hủy mọi phiên không. Với thao tác nhạy cảm, có cần xác thực lại (step-up) không.

Bước 6 — Liệt kê các kiểm tra phía server. Nhấn mạnh: mọi kiểm tra quyền phải xảy ra ở server, không bao giờ chỉ ở client. Ẩn nút ở giao diện chỉ là trang trí; kẻ tấn công gọi thẳng API. Viết rõ từng endpoint kiểm tra gì.

Bước 7 — Ghi audit & logging. Với hành động nhạy cảm (đổi quyền, xem dữ liệu nhạy cảm, đăng nhập admin), spec yêu cầu ghi log: ai, làm gì, lúc nào. Đây là yêu cầu thường gặp khi tuân thủ pháp lý.

Lỗi thường gặp & mẹo

Lỗi 1 — Chỉ kiểm tra quyền ở client. Ẩn nút "Xóa" trên UI nhưng API vẫn cho xóa khi bị gọi trực tiếp. Luôn ghi trong spec: "kiểm tra ở server" cho mọi hành động.

Lỗi 2 — Lẫn 401 và 403. 401 = "tôi không biết bạn là ai" (chưa/đã hết đăng nhập, cần đăng nhập lại). 403 = "tôi biết bạn là ai nhưng bạn không được phép". Dùng đúng giúp client xử lý đúng (401 thì đẩy ra màn login, 403 thì báo thiếu quyền).

Lỗi 3 — Quên luồng "hết hạn giữa chừng". Người dùng đang điền form 10 phút thì token hết hạn, bấm Submit thì sao? Mất sạch dữ liệu vừa nhập? Spec phải xử lý: silent refresh, hoặc giữ form và yêu cầu đăng nhập lại rồi tiếp tục.

Lỗi 4 — Bỏ quên trạng thái "đã đăng nhập nhưng chưa đủ điều kiện". Ví dụ đã đăng nhập nhưng chưa xác minh email, chưa hoàn tất hồ sơ, hoặc tài khoản bị khóa. Đây không phải đăng nhập hay chưa — là một trạng thái thứ ba cần spec riêng.

Mẹo 1 — Mặc định là từ chối (default deny). Khi viết permission matrix, hãy mặc định mọi ô là "Không", rồi mở dần những gì cần thiết. An toàn hơn nhiều so với mặc định "Có" rồi đóng lại.

Mẹo 2 — Dùng 404 thay 403 cho dữ liệu riêng tư. Trả 403 cho /orders/48214 vô tình tiết lộ "đơn hàng này tồn tại, chỉ là bạn không được xem". Trả 404 giấu luôn sự tồn tại — an toàn hơn với dữ liệu nhạy cảm.

Mẹo 3 — Tách "đăng nhập" và "thao tác nhạy cảm". Phiên đăng nhập có thể dài để tiện, nhưng những hành động như đổi mật khẩu, chuyển tiền, xóa tài khoản nên yêu cầu xác thực lại tại chỗ.

Bài tập thực hành

Hãy chọn một tính năng bạn đang làm hoặc tưởng tượng tính năng "Quản lý nhóm học tập" trên một nền tảng học trực tuyến, rồi viết phần Permission & Auth Spec gồm:

  • Liệt kê 3–4 vai trò (ví dụ: khách chưa đăng nhập, học viên, trợ giảng, giảng viên).
  • Chọn auth flow: ghi rõ hỗ trợ những phương thức đăng nhập nào và lý do chọn (gợi ý: với người dùng Việt Nam, cân nhắc Google + Zalo + email/password làm fallback).
  • Dựng một permission matrix với ít nhất 5 hành động (xem danh sách thành viên, mời thành viên, xóa thành viên, đăng bài, xóa bài của người khác). Điền Có/Không/Có-điều-kiện cho từng vai trò.
  • Viết 3 dòng về hành vi thiếu quyền: mỗi hành động bị chặn thì ẩn hay hiện-rồi-báo-lỗi, và trả mã lỗi nào.
  • Một đoạn về vòng đời session: token sống bao lâu, đăng xuất làm gì, có thao tác nào cần step-up không.
Sau khi viết xong, đưa cho một đồng nghiệp đóng vai engineer và yêu cầu họ tìm chỗ mơ hồ. Nếu họ hỏi được một câu mà spec không trả lời được, đó chính là chỗ bạn cần bổ sung.

Tóm tắt

Permission & Auth Spec là phần "vô hình" của PRD — viết tốt thì không ai để ý, viết tệ thì thành thảm họa bảo mật. Hãy nhớ ba ý cốt lõi: một, luôn tách bạch authentication (bạn là ai) và authorization (bạn được làm gì), và đừng bao giờ để cái này thay thế cái kia. Hai, dùng permission matrix để biến mọi sự mơ hồ về quyền thành một cái bảng rõ ràng mà engineer, QA và support đều đọc được. Ba, mọi kiểm tra quyền phải ở server, mặc định từ chối, và đừng quên những trạng thái biên: token hết hạn giữa chừng, đã đăng nhập nhưng chưa đủ điều kiện, và thao tác nhạy cảm cần xác thực lại.

Lần tới khi viết spec cho một tính năng có dữ liệu người dùng, hãy tự hỏi: "Nếu ai đó đổi số ID trên URL thì sao?" Nếu spec của bạn trả lời được câu đó một cách dứt khoát, bạn đã làm tốt phần khó nhất rồi.