← Backend← Backend
BackendBackend19 Th7, 2026Jul 19, 202625 phút đọc20 min read

Authentication & AuthorizationAuthentication & Authorization

Thuộc bộ kiến thức Backend Roadmap.

Tổng quan

Hầu như mọi backend không tầm thường đều phải trả lời hai câu hỏi trên gần như mỗi request: “đây là ai?”“họ có được phép làm việc này không?”. Câu đầu là authentication (AuthN), câu sau là authorization (AuthZ). Làm sai hai thứ này là con đường nhanh nhất dẫn tới lộ dữ liệu, để một user hành động thay danh nghĩa user khác, hoặc trao chìa khóa hệ thống cho kẻ tấn công — đó là lý do “Broken Access Control” và “Identification and Authentication Failures” đều nằm gần đỉnh của OWASP Top 10.

Đây là một hướng dẫn mang tính phòng thủ (defensive). Mục tiêu là giúp bạn xây hệ thống identity và access khó bị lạm dụng: lưu password sao cho một lần database bị dump không phải là “toang” hoàn toàn, phát hành token không thể giả mạo hay replay mãi mãi, và thực thi permission theo hướng fail closed. Note này nối tiếp các ý tưởng thiết kế API trong ./06-apis.md và gắn chặt với các biện pháp phòng thủ rộng hơn trong ./12-web-security.md.

AuthN và AuthZ — phân biệt cốt lõi

Authentication (AuthN)Authorization (AuthZ)
Trả lời câu hỏiBạn là ai?Bạn được phép làm gì?
Xảy raTrước (xác lập danh tính)Sau khi đã biết danh tính
Tạo raMột identity / principal (user, service)Quyết định cho phép / từ chối
Đầu vào điển hìnhPassword, token, certificate, biometricRoles, attributes, quyền sở hữu, policies
Ví dụ lỗiKẻ tấn công đăng nhập thành người khácMột user đã đăng nhập đọc được invoice của user khác
HTTP status401 Unauthorized (đặt tên nhầm — thực chất là “chưa authenticate”)403 Forbidden

Một cách ghi nhớ hữu ích: AuthN là bước kiểm tra hộ chiếu ở cửa khẩu; AuthZ là chuyện visa của bạn có cho vào một căn phòng cụ thể hay không. Bạn luôn phải authenticate trước khi có thể authorize một cách có nghĩa, nhưng một danh tính hợp lệ không đồng nghĩa với việc có quyền — mỗi hành động được bảo vệ đều cần một lần kiểm tra AuthZ riêng, thực thi ở phía server.

Kiến thức nền tảng

Xử lý password (defensive)

Password là credential phổ biến nhất và cũng là thứ hay bị làm sai nhất. Nguyên tắc quan trọng nhất: bạn không bao giờ lưu password, và cũng không “encrypt” nó — bạn lưu một bản hash chậm và có salt.

Vì sao phải hash chứ không encrypt?

Encryption là có thể đảo ngược (reversible): bất kỳ ai có key đều có thể biến ciphertext trở lại password gốc. Điều đó khiến key trở thành một điểm chết người duy nhất, và bạn (hoặc kẻ tấn công lấy được key) có thể khôi phục password dạng plaintext của mọi user. Hashing là một chiều (one-way): từ hash bạn không thể khôi phục lại đầu vào một cách thực tế. Khi đăng nhập, bạn hash password người dùng gửi lên rồi so sánh với hash đã lưu. Bạn không bao giờ cần password gốc, nên bạn cũng không bao giờ nên có khả năng tạo ra nó.

Vì sao MD5 / SHA-256 dùng một mình là sai cho password

MD5, SHA-1, và cả SHA-256 đều là hash mật mã đa dụng, được thiết kế để chạy nhanh. Nhanh lại chính là điều sai cho password: một GPU hiện đại tính được hàng tỷ hash SHA-256 mỗi giây, nên kẻ tấn công lấy được bảng hash của bạn có thể brute-force hoặc dictionary-attack các password yếu gần như tức thì. Rainbow table (bảng tính trước) khiến các fast hash không salt trở nên dễ đảo ngược với những password thông dụng.

Password hashing cần tính chất ngược lại: phải cố tình chậmmemory-hard (tốn nhiều bộ nhớ), có thể điều chỉnh để giữ độ chậm khi phần cứng mạnh lên. Hãy dùng một password hashing function (còn gọi là KDF — key derivation function) được thiết kế cho việc này.

Thuật toánLoạiGhi chú
Argon2idKDF memory-hardNgười thắng Password Hashing Competition (2015). Lựa chọn mặc định được khuyến nghị hiện nay. Biến thể id chống cả side-channel lẫn tấn công GPU/ASIC.
scryptKDF memory-hardMemory-hard, lựa chọn tốt khi không có Argon2.
bcryptHash thích ứng (adaptive)Đã được kiểm chứng từ 1999, hỗ trợ rộng rãi. Cắt đầu vào ở 72 byte — nên pre-hash đầu vào dài. Vững chắc, dù cũ hơn.
PBKDF2Hash lặpĐược FIPS chấp thuận, nhưng không memory-hard (yếu hơn trước GPU). Chỉ dùng khi compliance bắt buộc, với số iteration cao.
MD5 / SHA-1 / SHA-256 (dùng một mình)Fast hashKhông bao giờ dùng cho password. Ổn cho checksum/toàn vẹn dữ liệu, không phải cho credential.

Salt, pepper và work factor

Một bản hash bcrypt đã lưu vốn đã mã hóa cả thuật toán, cost và salt trong một chuỗi:

Cấu tạo một bcrypt hash
$2b$12$eImiTMZG4T2s7A0.rXhVFO....R4L2q3s0Vf9x6kQeZmO/1u2gJ8Km
  • định danh thuật toán2b = bcrypt
  • cost = 122^12 = 4096 iteration
  • 22 ký tự salt (base64)
  • 31 ký tự hash

Quy tắc bổ sung: so sánh hash theo constant time (dùng hàm verify của thư viện, không bao giờ dùng ==), áp dụng password policy hợp lý (ưu tiên độ dài hơn là độ phức tạp tùy tiện — NIST khuyến khích passphrase dài), kiểm tra password gửi lên với danh sách password đã lộ (ví dụ API k-anonymity của Have I Been Pwned), và rate-limit / khóa tạm khi thất bại liên tục.

Mô hình cổ điển cho ứng dụng web trên browser: server giữ state, client chỉ giữ một ID mờ (opaque).

  1. User gửi credentials qua TLS.
  2. Server verify, rồi tạo một session record ở phía server (trong memory, Redis, hoặc DB) chứa user ID, roles, expiry, v.v.
  3. Server trả về một session ID — một giá trị ngẫu nhiên, dài, không thể đoán — trong header Set-Cookie.
  4. Browser tự động gửi cookie đó ở mọi request tiếp theo; server tra cứu session để nhận diện user.
  5. Khi logout (hoặc hết hạn), server xóa session record — revocation là tức thì và đơn giản vì server sở hữu state.

Vì cookie chỉ là một con trỏ ngẫu nhiên, nó không mang dữ liệu nào mà kẻ tấn công có thể đọc hay chỉnh sửa — niềm tin nằm ở kho lưu trữ phía server.

FlagMục đích
HttpOnlyJavaScript không đọc được cookie (document.cookie), làm giảm nguy cơ đánh cắp token qua XSS.
SecureCookie chỉ được gửi qua HTTPS, không bao giờ qua HTTP plaintext.
SameSite=Lax / StrictHạn chế gửi cookie trên request cross-site, một biện pháp chống CSRF quan trọng. Strict an toàn nhất; Lax là mặc định phổ biến.
Domain / PathGiới hạn phạm vi cookie xuống host/path hẹp nhất cần dùng.
Max-Age / ExpiresGiới hạn vòng đời cookie; bỏ đi để tạo session cookie mất theo browser.
Tiền tố __Host-Tiền tố tên cookie bắt buộc Secure, Path=/ và không có Domain — một mặc định hardening mạnh.
Set-Cookie: __Host-sid=9f2c...e71a; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600

Cái giá của session auth là tính stateful: mỗi server cần truy cập được kho session, điều này quan trọng khi scale ngang (nên mới có các kho chia sẻ như Redis). Điểm mạnh lớn là revocation dễ dàngkhông có dữ liệu nhạy cảm nằm ở client.

Token-based authentication (JWT)

Thay vì server nhớ session, server trao cho client một token đã ký (signed) chứa sẵn các claim. Ở mỗi request, client trình token (thường qua Authorization: Bearer <token>), và server verify chữ ký — không cần tra cứu. Đây là mô hình stateless, phù hợp cho API, mobile client và microservice không chia sẻ kho session chung.

Định dạng phổ biến nhất là JSON Web Token (JWT).

Cấu trúc JWT: header.payload.signature

Ba phần được encode base64url, nối bằng dấu chấm:

JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDB9.dQw4w9WgXcQ7...signature...
  • headereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  • payloadeyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDB9
  • signaturedQw4w9WgXcQ7...signature...

Sau khi decode:

// Header — thuật toán & loại token
{
  "alg": "HS256",
  "typ": "JWT"
}

// Payload — các claim
{
  "sub": "1234567890",   // subject (user ID)
  "name": "Alice",
  "role": "admin",
  "iss": "https://auth.example.com",  // issuer (bên phát hành)
  "aud": "https://api.example.com",   // audience (bên nhận dự kiến)
  "iat": 1700000000,     // issued at  (thời điểm phát hành, Unix time)
  "exp": 1700003600,     // expires    (hết hạn, Unix time) — 1 giờ sau
  "jti": "a3f1c9e2"      // ID duy nhất của token (hữu ích cho danh sách revoke)
}

// Signature (về mặt khái niệm):
// HMAC-SHA256( base64url(header) + "." + base64url(payload), secret )

Điểm cực kỳ quan trọng: payload chỉ được encode base64, KHÔNG encrypt — ai cũng đọc được. Không bao giờ đặt bí mật (password, số thẻ, PII bạn không muốn lộ) vào JWT. Chữ ký bảo đảm tính toàn vẹn (integrity) (không ai sửa được claim), chứ không phải tính bí mật (confidentiality).

Ký (signing): HS256 và RS256

HS256 (HMAC)RS256 (RSA)
KeyMột secret dùng chungKey private ký, key public verify
Ai ký đượcBất kỳ ai có secretChỉ người giữ private key
Ai verify đượcBất kỳ ai có secretBất kỳ ai có public key
Phù hợp nhấtMột service vừa phát hành vừa verifyNhiều service / bên thứ ba chỉ verify (ví dụ provider OAuth/OIDC)

Lưu ý bảo mật: luôn ghim cứng (pin) thuật toán mong đợi ở phía verify. Tấn công alg: none khét tiếng và tấn công nhầm lẫn “RS256 bị verify như HS256, dùng public key làm secret cho HMAC” đều xuất phát từ việc tin vào header alg của chính token. Hãy verify với một allow-list thuật toán tường minh.

Claims, expiry và refresh token

Các registered claim chuẩn (iss, sub, aud, exp, nbf, iat, jti) được định nghĩa bởi RFC 7519; luôn validate exp (và lý tưởng là cả iss/aud) ở mỗi request. Giữ access token có thời hạn ngắn (vài phút tới một giờ) để một token bị đánh cắp cũng nhanh chóng hết hạn.

Để tránh bắt user đăng nhập lại vài phút một lần, ghép một access token ngắn hạn với một refresh token dài hạn:

Lưu token ở phía client như thế nào

Nơi lưuƯu điểmNhược điểm
Cookie HttpOnlyJS không đọc được → chống đánh cắp qua XSS; tự động gửi kèmCần bảo vệ CSRF (SameSite, CSRF token)
localStorage / sessionStorageĐơn giản, hợp với SPA và header AuthorizationMọi JS đều đọc được → bất kỳ XSS nào = mất token; rủi ro XSS lớn hơn
Trong bộ nhớ (biến JS)Không persist; mất khi refresh → cửa sổ đánh cắp nhỏMất khi reload; thường ghép với một refresh cookie

Mẫu hiện đại thực dụng cho browser: refresh token trong cookie HttpOnly, Secure, SameSite; access token giữ trong bộ nhớ. Đó là lý do việc lưu token là một quyết định bảo mật, không chỉ là chi tiết cài đặt.

Bài toán revocation

Session auth revoke tức thì (xóa record). JWT stateless thì hợp lệ cho tới khi hết hạn — đó vừa là điểm mấu chốt, vừa là toàn bộ vấn đề. Bạn không thể “thu hồi” một token đã ký. Các biện pháp giảm thiểu:

Sự đánh đổi này — stateless và revocation dễ dàng — chính là quyết định trung tâm khi chọn giữa session và token.

HTTP Basic authentication

Scheme đơn giản nhất (RFC 7617): client gửi Authorization: Basic <base64(username:password)> ở mỗi request.

Authorization: Basic YWxpY2U6czNjcjN0   # base64("alice:s3cr3t")

base64encoding, không phải encryption — đảo ngược cực kỳ dễ. Vì vậy Basic auth gửi password (về cơ bản là dạng rõ) trên từng request một. Nó chỉ chấp nhận được khi dùng qua TLS (HTTPS), và ngay cả khi đó cũng không hợp với ứng dụng cho end-user (không có logout, credential bị gửi lại liên tục, dễ vô tình bị ghi vào log). Nó ổn cho tooling nội bộ hoặc server-to-server đơn giản sau TLS, nhưng với auth cho user thật thì hãy ưu tiên token hoặc session.

Khái niệm chính

OAuth 2.0 — delegated authorization

OAuth 2.0 (RFC 6749) là một framework cho delegated authorization (ủy quyền truy cập): nó cho phép một user cấp cho ứng dụng bên thứ ba quyền truy cập giới hạn vào tài nguyên của họ trên một service khác mà không chia sẻ password. Khi bạn bấm “Sign in with Google” hay cho một app “đăng bài thay bạn”, đó là OAuth cấp quyền truy cập có scope thông qua một token. Lưu ý: OAuth nói về authorization (truy cập tài nguyên), không phải chủ yếu về authentication — để login/danh tính, bạn xếp chồng OpenID Connect lên trên (xem bên dưới).

Các vai trò (roles)

Vai tròLà ai
Resource OwnerUser sở hữu dữ liệu.
ClientApp yêu cầu truy cập thay mặt user.
Authorization ServerPhát hành token sau khi authenticate user & lấy consent (ví dụ auth server của Google).
Resource ServerAPI giữ tài nguyên được bảo vệ; chấp nhận access token.

Token & scope

Các grant type

GrantTrường hợp dùng
Authorization Code + PKCEMặc định cho app hướng user — web, SPA, mobile, native. PKCE nay được khuyến nghị cho mọi loại client.
Client CredentialsMachine-to-machine (không có user); client tự authenticate như chính nó.
Refresh TokenĐổi refresh token lấy access token mới.
Device AuthorizationThiết bị hạn chế nhập liệu (TV, CLI) — user authorize trên thiết bị thứ hai.
ImplicitĐã deprecated — token nằm trong URL fragment quá dễ lộ. Dùng Auth Code + PKCE.
Resource Owner PasswordĐã deprecated — app cầm password thô, đi ngược mục đích của OAuth.

Authorization Code flow với PKCE — đi qua từng bước

PKCE (Proof Key for Code Exchange, RFC 7636) ngăn kẻ tấn công chặn được authorization code khỏi việc đổi nó lấy token, vì chỉ client gốc biết code_verifier bí mật.

OAuth 2.0 authorization code + PKCE
OAuth 2.0 authorization code + PKCE

Hai điểm bảo mật thiết yếu trong flow này: tham số state (một giá trị ngẫu nhiên được echo lại để chống CSRF trên bước redirect) và khớp redirect_uri chính xác (để code không thể bị chuyển hướng tới kẻ tấn công).

Client Credentials flow (machine-to-machine)

Không có user tham gia — một service tự authenticate như chính nó:

Service ──▶ POST /token
            grant_type=client_credentials
            client_id, client_secret, scope
        ◀── access_token
Service ──▶ GET /resource  (Authorization: Bearer access_token) ──▶ Resource Server

OpenID Connect (OIDC) — identity xếp chồng trên OAuth

OAuth 2.0 cho resource server biết client được làm gì, nhưng không cho biết một cách đáng tin user là ai. OpenID Connect là một lớp identity mỏng nằm trên OAuth 2.0, chuẩn hóa phần authentication. Đây mới là thứ thực sự vận hành “Sign in with …”.

Bổ sung then chốt là ID token — một JWT mô tả user đã được authenticate, phát hành cùng với access token trong cùng Authorization Code flow. Trong khi access token của OAuth dành cho resource server, thì ID token dành cho client để biết ai vừa đăng nhập.

// Payload của ID token sau khi decode
{
  "iss": "https://accounts.google.com",  // ai phát hành
  "sub": "10769150350006150715113082",   // user ID duy nhất, ổn định
  "aud": "your-client-id.apps...",        // token này dành cho client CỦA BẠN
  "email": "alice@example.com",
  "email_verified": true,
  "name": "Alice",
  "iat": 1700000000,
  "exp": 1700003600,
  "nonce": "n-0S6_WzA2Mj"                 // buộc token với auth request của bạn (chống replay)
}

OIDC còn thêm: các claim chuẩn (email, name, picture), scope openid, endpoint UserInfo, và Discovery (/.well-known/openid-configuration) để client tự cấu hình. Hãy ưu tiên một provider/thư viện OIDC trưởng thành thay vì tự viết login.

SAML — SSO doanh nghiệp (tóm tắt)

SAML 2.0 (Security Assertion Markup Language) là chuẩn cũ hơn, dựa trên XML, cho federated single sign-on, vẫn thống trị trong môi trường enterprise/B2B. Các vai trò: Identity Provider (IdP) (ví dụ Okta, Azure AD) authenticate user và phát hành một assertion XML đã ký; Service Provider (SP) (app của bạn) tin assertion đó và cho user đăng nhập. Nó giải cùng bài toán “đăng nhập một lần, truy cập nhiều app” như OIDC, nhưng bằng XML và POST redirect qua browser thay vì JSON/JWT. Với app consumer/API mới, hãy ưu tiên OIDC; hỗ trợ SAML khi khách hàng doanh nghiệp yêu cầu.

Các mô hình authorization

Khi đã biết user là ai, bạn quyết định họ được làm gì. Các mô hình phổ biến:

RBAC — Role-Based Access Control

Permission được gom vào các role; user được gán role. “Admin xóa được; editor ghi được; viewer đọc được.” Đơn giản, phổ biến, dễ suy luận và audit. Có thể gặp role explosion (bùng nổ role) khi luật chi tiết dần (editor-of-team-A-in-region-EU).

RBAC
  1. 01useralice
  2. 02rolesadmin
  3. 03permissions{read, write, delete}
  4. 04resource/articles

ABAC — Attribute-Based Access Control

Quyết định được tính từ attribute của subject, resource, action và environment. “Một bác sĩ được đọc hồ sơ nếu cùng khoa đang trong ca trực.” Diễn đạt mạnh hơn RBAC nhiều và rất hợp với luật phụ thuộc ngữ cảnh, nhưng khó test và audit hơn.

allow if user.department == resource.department
       and user.role == "doctor"
       and env.time in shift_hours

ACL — Access Control List

Permission gắn trực tiếp vào từng resource: một danh sách (principal → action được phép). Nghĩ tới permission của filesystem hay “tài liệu này được chia sẻ cho Bob (view) và Carol (edit)”. Rất hợp cho chia sẻ theo từng object (kiểu Google Docs); không scale tốt như một policy toàn hệ thống.

ReBAC / policy engine

Mô hìnhĐộ chi tiếtHợp nhất choCần cẩn thận
RBACThô (theo role)Đa số app, chức năng công việc rõ ràngRole explosion
ABACMịn (theo attribute)Luật phụ thuộc ngữ cảnhĐộ phức tạp, test
ACLTheo từng objectChia sẻ từng resourceScale toàn hệ thống
ReBACGraph quan hệSở hữu lồng nhau, chia sẻChi phí mô hình hóa & hạ tầng

Best Practices

So sánh các cơ chế authentication

Cơ chếStateRevocationHợp nhất choRủi ro chính
Session + cookieStateful (kho ở server)Dễ (xóa record)App web render phía serverCSRF; scale kho session
JWT (bearer)StatelessKhó (tới khi hết hạn)API, SPA, microserviceĐánh cắp token; không revoke được; lộ secret
Basic authStatelessKhông có (gửi credential mỗi lần)Nội bộ/server-to-server qua TLSPassword gửi mỗi request
OAuth 2.0Dựa trên tokenQua refresh/expiryTruy cập ủy quyền cho bên thứ baCấu hình sai (redirect_uri, scope)
OIDCDựa trên tokenQua refresh/expiryFederated login (“Sign in with…”)Như OAuth + validate ID token
SAMLDựa trên assertionSession ở SP/IdPSSO doanh nghiệpĐộ phức tạp XML, signature wrapping
API keyChuỗi statelessXoay/revoke keyAuth service đơn giảnSống lâu; lộ trong code/log
mTLSDựa trên certificateRevoke/hết hạn certService mesh zero-trustQuản lý vòng đời cert

Best practices chung

Tài liệu tham khảo

Part of the Backend Roadmap knowledge base.

Overview

Almost every non-trivial backend has to answer two questions on nearly every request: “who is this?” and “are they allowed to do this?”. The first is authentication (AuthN), the second is authorization (AuthZ). Getting these wrong is the fastest way to leak data, let one user act as another, or hand an attacker the keys to your system — which is why “Broken Access Control” and “Identification and Authentication Failures” both sit near the top of the OWASP Top 10.

This note is a defensive guide. The goal is to help you build identity and access systems that are hard to abuse: store passwords so that a database dump is not game over, issue tokens that cannot be forged or replayed forever, and enforce permissions that fail closed. It builds on the API design ideas in ./06-apis.md and pairs closely with the broader defenses in ./12-web-security.md.

AuthN vs AuthZ — the core distinction

Authentication (AuthN)Authorization (AuthZ)
Question answeredWho are you?What are you allowed to do?
HappensFirst (establishes identity)After identity is known
ProducesAn identity / principal (user, service)An allow / deny decision
Typical inputsPassword, token, certificate, biometricRoles, attributes, ownership, policies
Failure exampleAttacker logs in as someone elseA logged-in user reads another user’s invoices
HTTP status401 Unauthorized (misnamed — really “unauthenticated”)403 Forbidden

A useful mnemonic: AuthN is the passport check at the border; AuthZ is whether your visa lets you into a given room. You must always authenticate before you can meaningfully authorize, but a valid identity does not imply permission — every protected action needs its own AuthZ check, enforced on the server.

Fundamentals

Password handling (defensive)

Passwords are the most common credential and the most common thing done wrong. The single most important rule: you never store passwords, and you never “encrypt” them either — you store a slow, salted hash.

Why hash, not encrypt?

Encryption is reversible: anyone with the key can turn ciphertext back into the original password. That means the key becomes a single point of catastrophic failure, and you (or an attacker who steals the key) can recover every user’s plaintext password. Hashing is one-way: from the hash you cannot practically recover the input. At login you hash the submitted password and compare it to the stored hash. You never need the original, so you should never be able to produce it.

Why MD5 / SHA-256 alone are wrong for passwords

MD5, SHA-1, and even SHA-256 are general-purpose cryptographic hashes designed to be fast. Fast is exactly wrong for passwords: a modern GPU computes billions of SHA-256 hashes per second, so an attacker who steals your hash table can brute-force or dictionary-attack weak passwords almost instantly. Precomputed rainbow tables make unsalted fast hashes trivially reversible for common passwords.

Password hashing needs the opposite properties: it must be deliberately slow and memory-hard, tunable to stay slow as hardware improves. Use a password hashing function (a.k.a. a KDF — key derivation function) built for this.

AlgorithmTypeNotes
Argon2idMemory-hard KDFWinner of the Password Hashing Competition (2015). Recommended default today. id variant resists both side-channel and GPU/ASIC attacks.
scryptMemory-hard KDFMemory-hard, good choice where Argon2 isn’t available.
bcryptAdaptive hashBattle-tested since 1999, widely available. Truncates input at 72 bytes — pre-hash long inputs. Solid, if older.
PBKDF2Iterated hashFIPS-approved, but not memory-hard (weaker vs GPUs). Use only if compliance forces it, with a high iteration count.
MD5 / SHA-1 / SHA-256 (alone)Fast hashNever for passwords. Fine for checksums/integrity, not credentials.

Salt, pepper, and work factor

A stored bcrypt hash already encodes the algorithm, cost, and salt in one string:

Anatomy of a bcrypt hash
$2b$12$eImiTMZG4T2s7A0.rXhVFO....R4L2q3s0Vf9x6kQeZmO/1u2gJ8Km
  • algorithm identifier2b = bcrypt
  • cost = 122^12 = 4096 iterations
  • 22-char base64 salt
  • 31-char hash

Additional rules: compare hashes in constant time (use the library’s verify function, never ==), enforce reasonable password policies (length over arbitrary complexity — NIST favors long passphrases), check submitted passwords against breached-password lists (e.g. Have I Been Pwned’s k-anonymity API), and rate-limit / lock out on repeated failures.

The classic model for browser apps: the server keeps the state, the client holds only an opaque ID.

  1. User submits credentials over TLS.
  2. Server verifies them, then creates a session record server-side (in memory, Redis, or a DB) holding the user ID, roles, expiry, etc.
  3. Server returns a session ID — a long, random, unguessable value — in a Set-Cookie header.
  4. The browser automatically sends that cookie on every subsequent request; the server looks up the session to identify the user.
  5. On logout (or expiry) the server deletes the session record — revocation is instant and simple because the server owns the state.

Because the cookie is just a random pointer, it carries no data an attacker can read or tamper with — the trust lives in the server-side store.

FlagPurpose
HttpOnlyJavaScript cannot read the cookie (document.cookie), blunting XSS-based token theft.
SecureCookie is sent only over HTTPS, never plaintext HTTP.
SameSite=Lax / StrictRestricts sending the cookie on cross-site requests, a key CSRF defense. Strict is safest; Lax is a common default.
Domain / PathScope the cookie to the narrowest host/path that needs it.
Max-Age / ExpiresBound the cookie’s lifetime; omit for a session cookie that dies with the browser.
__Host- prefixA cookie name prefix that forces Secure, Path=/, and no Domain — a strong hardening default.
Set-Cookie: __Host-sid=9f2c...e71a; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600

Session auth’s cost is statefulness: every server needs to reach the session store, which matters when scaling horizontally (hence shared stores like Redis). Its big win is easy revocation and no sensitive data on the client.

Token-based authentication (JWT)

Instead of the server remembering the session, the server hands the client a signed token that contains the claims. On each request the client presents the token (usually Authorization: Bearer <token>), and the server verifies the signature — no lookup required. This is stateless, which suits APIs, mobile clients, and microservices that don’t share a session store.

The most common format is the JSON Web Token (JWT).

JWT structure: header.payload.signature

Three base64url-encoded parts joined by dots:

JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDB9.dQw4w9WgXcQ7...signature...
  • headereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  • payloadeyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDB9
  • signaturedQw4w9WgXcQ7...signature...

Decoded:

// Header — algorithm & token type
{
  "alg": "HS256",
  "typ": "JWT"
}

// Payload — the claims
{
  "sub": "1234567890",   // subject (the user ID)
  "name": "Alice",
  "role": "admin",
  "iss": "https://auth.example.com",  // issuer
  "aud": "https://api.example.com",   // audience
  "iat": 1700000000,     // issued at  (Unix time)
  "exp": 1700003600,     // expires    (Unix time) — 1 hour later
  "jti": "a3f1c9e2"      // unique token ID (useful for revocation lists)
}

// Signature (conceptually):
// HMAC-SHA256( base64url(header) + "." + base64url(payload), secret )

Critical point: the payload is only base64-encoded, not encrypted — anyone can read it. Never put secrets (passwords, card numbers, PII you wouldn’t expose) in a JWT. The signature guarantees integrity (nobody altered the claims), not confidentiality.

Signing: HS256 vs RS256

HS256 (HMAC)RS256 (RSA)
KeysOne shared secretPrivate key signs, public key verifies
Who can signAnyone with the secretOnly the private-key holder
Who can verifyAnyone with the secretAnyone with the public key
Best forSingle service that both issues & verifiesMany services / third parties that only verify (e.g. OAuth/OIDC providers)

Security note: always pin the expected algorithm on the verifier. The infamous alg: none attack and the “RS256 verified as HS256 using the public key as the HMAC secret” confusion attack both come from trusting the token’s own alg header. Verify with an explicit allow-list of algorithms.

Claims, expiry, and refresh tokens

Standard registered claims (iss, sub, aud, exp, nbf, iat, jti) are defined by RFC 7519; always validate exp (and ideally iss/aud) on every request. Keep access tokens short-lived (minutes to an hour) so a stolen token expires quickly.

To avoid making the user log in every few minutes, pair a short-lived access token with a long-lived refresh token:

Where to store tokens on the client

StorageProsCons
HttpOnly cookieJS can’t read it → resists XSS theft; sent automaticallyNeeds CSRF protection (SameSite, CSRF tokens)
localStorage / sessionStorageSimple, works well for SPAs and Authorization headerReadable by any JS → any XSS = token theft; no automatic CSRF but bigger XSS risk
In-memory (JS variable)Not persisted; gone on refresh → smaller theft windowLost on reload; usually paired with a refresh cookie

The pragmatic modern pattern for browsers: refresh token in an HttpOnly, Secure, SameSite cookie; access token kept in memory. This is why token storage is a security decision, not just an implementation detail.

The revocation problem

Session auth revokes instantly (delete the record). Stateless JWTs are valid until they expire — that’s the whole point, and the whole problem. You cannot “un-issue” a signed token. Mitigations:

This tradeoff — statelessness vs easy revocation — is the central decision when choosing sessions vs tokens.

HTTP Basic authentication

The simplest scheme (RFC 7617): the client sends Authorization: Basic <base64(username:password)> on each request.

Authorization: Basic YWxpY2U6czNjcjN0   # base64("alice:s3cr3t")

base64 is encoding, not encryption — trivially reversible. So Basic auth sends the password (effectively in the clear) on every single request. It is acceptable only over TLS (HTTPS), and even then it’s poorly suited to user-facing apps (no logout, credentials resent constantly, easy to log by accident). It’s fine for simple server-to-server or internal tooling behind TLS, but prefer tokens or sessions for real user auth.

Key Concepts

OAuth 2.0 — delegated authorization

OAuth 2.0 (RFC 6749) is a framework for delegated authorization: it lets a user grant a third-party application limited access to their resources on another service without sharing their password. When you click “Sign in with Google” or let an app “post on your behalf”, that’s OAuth granting scoped access via a token. Note: OAuth is about authorization (access to resources), not primarily authentication — for login/identity you layer OpenID Connect on top (see below).

Roles

RoleWho it is
Resource OwnerThe user who owns the data.
ClientThe app requesting access on the user’s behalf.
Authorization ServerIssues tokens after authenticating the user & getting consent (e.g. Google’s auth server).
Resource ServerThe API holding the protected resources; accepts access tokens.

Tokens & scopes

Grant types

GrantUse case
Authorization Code + PKCEThe default for user-facing apps — web, SPA, mobile, native. PKCE is now recommended for all clients.
Client CredentialsMachine-to-machine (no user); the client authenticates as itself.
Refresh TokenExchange a refresh token for a fresh access token.
Device AuthorizationInput-constrained devices (TVs, CLIs) — user authorizes on a second device.
ImplicitDeprecated — tokens in the URL fragment leak too easily. Use Auth Code + PKCE.
Resource Owner PasswordDeprecated — the app handles the raw password, defeating OAuth’s purpose.

Authorization Code flow with PKCE — walkthrough

PKCE (Proof Key for Code Exchange, RFC 7636) stops an attacker who intercepts the authorization code from redeeming it, because only the original client knows the secret code_verifier.

OAuth 2.0 authorization code + PKCE
OAuth 2.0 authorization code + PKCE

Two security essentials in this flow: the state parameter (a random value echoed back to defend against CSRF on the redirect) and exact redirect_uri matching (so codes can’t be redirected to an attacker).

Client Credentials flow (machine-to-machine)

No user involved — a service authenticates as itself:

Service ──▶ POST /token
            grant_type=client_credentials
            client_id, client_secret, scope
        ◀── access_token
Service ──▶ GET /resource  (Authorization: Bearer access_token) ──▶ Resource Server

OpenID Connect (OIDC) — identity on top of OAuth

OAuth 2.0 tells the resource server what a client may do, but not reliably who the user is. OpenID Connect is a thin identity layer on top of OAuth 2.0 that standardizes authentication. It’s what actually powers “Sign in with …”.

The key addition is the ID token — a JWT describing the authenticated user, issued alongside the access token in the same Authorization Code flow. Where an OAuth access token is meant for the resource server, the ID token is meant for the client to learn who logged in.

// Decoded ID token payload
{
  "iss": "https://accounts.google.com",  // who issued it
  "sub": "10769150350006150715113082",   // stable unique user ID
  "aud": "your-client-id.apps...",        // this token is for YOUR client
  "email": "alice@example.com",
  "email_verified": true,
  "name": "Alice",
  "iat": 1700000000,
  "exp": 1700003600,
  "nonce": "n-0S6_WzA2Mj"                 // ties token to your auth request (replay defense)
}

OIDC also adds: standard claims (email, name, picture), the openid scope, a UserInfo endpoint, and Discovery (/.well-known/openid-configuration) so clients can auto-configure. Prefer a mature OIDC provider/library over rolling your own login.

SAML — enterprise SSO (brief)

SAML 2.0 (Security Assertion Markup Language) is the older, XML-based standard for federated single sign-on, still dominant in enterprise/B2B. Its roles: the Identity Provider (IdP) (e.g. Okta, Azure AD) authenticates the user and issues a signed XML assertion; the Service Provider (SP) (your app) trusts that assertion and logs the user in. It solves the same “log in once, access many apps” problem as OIDC, but with XML and browser POST redirects instead of JSON/JWT. For new consumer/API apps, prefer OIDC; support SAML when enterprise customers require it.

Authorization models

Once you know who the user is, you decide what they can do. Common models:

RBAC — Role-Based Access Control

Permissions are grouped into roles; users get roles. “Admins can delete; editors can write; viewers can read.” Simple, ubiquitous, easy to reason about and audit. Can suffer role explosion when rules get fine-grained (editor-of-team-A-in-region-EU).

RBAC
  1. 01useralice
  2. 02rolesadmin
  3. 03permissions{read, write, delete}
  4. 04resource/articles

ABAC — Attribute-Based Access Control

Decisions are computed from attributes of the subject, resource, action, and environment. “A doctor may read a record if they are in the same department and it’s during a shift.” Far more expressive than RBAC and great for context-aware rules, but harder to test and audit.

allow if user.department == resource.department
       and user.role == "doctor"
       and env.time in shift_hours

ACL — Access Control List

Permissions attached directly to each resource: a list of (principal → allowed actions). Think filesystem permissions or “this document is shared with Bob (view) and Carol (edit)”. Great for per-object sharing (Google Docs style); doesn’t scale well as a system-wide policy.

ReBAC / policy engines

ModelGranularityBest forWatch out for
RBACCoarse (roles)Most apps, clear job functionsRole explosion
ABACFine (attributes)Context-dependent rulesComplexity, testing
ACLPer-objectSharing individual resourcesSystem-wide scaling
ReBACRelationship graphNested ownership, sharingModeling & infra overhead

Best Practices

Comparison of authentication mechanisms

MechanismStateRevocationBest forMain risk
Session + cookieStateful (server store)Easy (delete record)Server-rendered web appsCSRF; store scaling
JWT (bearer)StatelessHard (until expiry)APIs, SPAs, microservicesToken theft; can’t revoke; secret leakage
Basic authStatelessN/A (credentials each time)Internal/server-to-server over TLSPassword sent every request
OAuth 2.0Token-basedVia refresh/expiryDelegated 3rd-party accessMisconfig (redirect_uri, scopes)
OIDCToken-basedVia refresh/expiryFederated login (“Sign in with…”)Same as OAuth + ID-token validation
SAMLAssertion-basedSession at SP/IdPEnterprise SSOXML complexity, signature wrapping
API keysStateless stringRotate/revoke keySimple service authLong-lived; leaks in code/logs
mTLSCertificate-basedRevoke/expire certZero-trust service meshCert lifecycle management

General best practices

References