jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Node.js Production Engineering 05 — Authentication và API Security

Thiết kế authentication và API security theo threat model: password hashing, session/JWT, cookie, OAuth/OIDC, authorization và defense-in-depth.

Một access token có chữ ký hợp lệ không chứng minh người gọi được xóa resource. Nó chỉ chứng minh token thỏa điều kiện xác minh; authorization vẫn phải xét tenant, ownership, trạng thái tài khoản và policy hiện tại. Nhiều sự cố auth bắt đầu từ việc gộp tất cả các câu hỏi đó thành một middleware isLoggedIn.

Bài này dùng Node.js 24 LTS và Express 5 để xây baseline security theo threat model. Sau khi đọc, bạn có thể:

  • tách authentication, session management và authorization thành các boundary độc lập;
  • chọn password hashing parameters bằng benchmark trên hạ tầng thật;
  • chọn server-side session hoặc token theo revocation, topology và client;
  • thiết kế cookie, CSRF, refresh rotation và logout như protocol, không chỉ helper;
  • kiểm tra role/permission/ownership ở đúng data boundary;
  • trả auth error cho frontend mà không tạo vòng refresh hoặc lộ thông tin tài khoản.

Phạm vi: đây là nền tảng cho web/API thông thường. Hệ identity đa tenant, JWKS rotation và authorization sâu được mở rộng ở Phần 15. Với rủi ro cao, hãy dùng identity provider/thư viện trưởng thành và security review độc lập.

Trước hết, hai từ người ta hay nhập nhằng:

AUTHENTICATION (authn)          AUTHORIZATION (authz)
"Who are you?"                  "Are you allowed to do this?"
prove identity (login)          check permissions (role / ownership)
        │                                │
        ▼                                ▼
  issue a session/token          gate the route / the row

Authentication tạo principal; authorization quyết định principal đó được làm gì trên resource cụ thể. Session management nối principal với các request sau. Ba lớp liên quan nhưng không thay thế nhau.


Threat model trước thư viện

credentials ─▶ login ─▶ session/token ─▶ request principal ─▶ policy ─▶ resource
     │            │            │                 │               │
 credential    enumeration  theft/replay     forged context   IDOR/tenant leak
 stuffing      brute force  fixation/CSRF    stale privilege  missing ownership

Tài sản cần bảo vệ gồm password hash, session/refresh token, signing key, dữ liệu cá nhân và audit trail. Attacker có thể kiểm soát browser input, đánh cắp database, chèn script qua XSS, gửi cross-site request, replay token hoặc gọi thẳng API bỏ qua UI. Mỗi lớp phòng thủ nên giảm một rủi ro cụ thể và vẫn hữu ích khi lớp khác thất bại.

Các invariant tối thiểu:

  • credential không xuất hiện trong log, URL, analytics hoặc error response;
  • đổi privilege, khóa tài khoản hoặc logout có revocation semantics đã công bố;
  • mọi resource query bị giới hạn bởi tenant/ownership phía server;
  • auth endpoint có abuse control, audit event và response không giúp enumeration;
  • secret/signing key có owner, rotation và rollback plan.

Bản đồ thư viện auth của Node

Không tự thiết kế password hash, token format hay OAuth protocol. Chọn primitive/implementation được review, pin version, theo dõi advisory và hiểu boundary của nó:

Thư việnViệc
bcrypt / argon2hash mật khẩu
jose / jsonwebtokenký, mã hóa khi cần và xác minh JOSE/JWT
passport (+ strategies)middleware auth cắm được
express-sessionsession phía server
connect-redislưu session trong Redis
cookie-parserđọc/ký cookie
helmetđặt baseline security header
express-rate-limitkìm lạm dụng
csrf-csrf / csrf-synctoken CSRF
zodvalidate + type input

Phân chia trách nhiệm: Argon2id/bcrypt bảo vệ verifier khi database bị lộ; session/JWT mang bằng chứng danh tính giữa request; Helmet/rate limit/validation/CSRF là các lớp phòng thủ khác nhau. Thư viện không chọn policy, TTL, key rotation hoặc authorization thay hệ thống.


Hash mật khẩu — phải đúng chính xác

Không lưu plaintext password hoặc encryption có thể đảo ngược. Lưu verifier bằng password hashing function chậm và có salt. Với hệ thống mới, OWASP ưu tiên Argon2id; bcrypt phù hợp cho hệ legacy nhưng có giới hạn input 72 byte.

import argon2 from 'argon2';

const ARGON2_OPTIONS = {
  type: argon2.argon2id,
  memoryCost: 19_456, // KiB
  timeCost: 2,
  parallelism: 1,
} as const;

const hash = await argon2.hash(plainPassword, ARGON2_OPTIONS);
const ok = await argon2.verify(user.passwordHash, attempt);

Các giá trị trên là baseline tối thiểu tham khảo, không phải con số capacity cuối. Benchmark trên instance production nhỏ nhất, đặt latency budget cho login và kiểm tra memory/concurrency để endpoint auth không tự làm cạn worker pool. Khi tăng parameter, dùng argon2.needsRehash() và rehash sau lần login thành công.

Bcrypt vẫn cần được hiểu khi migrate hệ thống cũ:

import bcrypt from 'bcrypt';

// Đo trên hardware thật; OWASP khuyến nghị work factor tối thiểu 10 cho bcrypt.
const hash = await bcrypt.hash(plainPassword, 12);
const ok = await bcrypt.compare(attempt, user.passwordHash);

Một hash bcrypt thực chất chứa gì — salt và cost đi bên trong nó:

$2b$12$eImiTXuWVxfM37uY4JANjQ.gT1f...   ← stored in the DB
 │   │  └ 22-char salt + 31-char hash
 │   └ cost factor (2^12 = 4096 rounds)
 └ algorithm version (bcrypt)

Các chi tiết ảnh hưởng trực tiếp tới thiết kế:

  • salt đánh bại rainbow table — salt ngẫu nhiên riêng mỗi user; bcrypt/argon2 tự sinh và nhúng.
  • cost factor là núm vặn — tăng dần khi hardware nhanh hơn và rehash có kiểm soát.
  • bcrypt chỉ dùng 72 byte đầu trong phần lớn implementation — enforce limit theo byte, hoặc migrate sang Argon2id; không tự pre-hash nếu chưa đánh giá password-shucking/null-byte risk.
  • pepper có thể thêm defense-in-depth nếu nằm trong secret manager/HSM tách khỏi database, nhưng phải có rotation/recovery plan.
const stored = user?.passwordHash ?? process.env.DUMMY_PASSWORD_HASH!;
const passwordMatches = await argon2.verify(stored, attempt);
if (!user || !passwordMatches) {
  throw new InvalidCredentialsError(); // cùng status/code/message cho cả hai trường hợp
}

Dùng dummy hash cùng parameter khi user không tồn tại để giảm timing difference, trả cùng error contract, rồi rate-limit theo cả account key đã normalize và nguồn request. Timing hoàn toàn giống nhau qua mạng là mục tiêu khó; defense-in-depth còn gồm MFA, breached-password check và monitoring credential stuffing.


Dù dùng session hay JWT, cookie thường là cách chứng chỉ đi giữa trình duyệt và server. Mỗi thuộc tính chặn một kiểu tấn công cụ thể:

Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400
                         │         │       │             │       │
   JS can't read it ◄────┘         │       │             │       └ lifetime (seconds)
   (blunts XSS theft)              │       │             └ which paths send it
   HTTPS only ◄───────────────────┘       └ controls selected cross-site sends
   (no plaintext transport)                  (reduces, not eliminates, CSRF)
Thuộc tínhChống
HttpOnlyXSS reading the token via document.cookie
Securerò qua HTTP thường
SameSite=Lax/Strictgiảm cross-site cookie attachment theo navigation context
__Host- name prefixbuộc Secure, Path=/ và không có Domain; giảm cookie shadowing
res.cookie('__Host-sid', id, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax', // chọn theo flow; 'none' bắt buộc đi cùng Secure
  path: '/', // __Host- yêu cầu Path=/ và không có Domain
  maxAge: 86_400_000, // ms in Express
});

HttpOnly ngăn JavaScript đọc cookie nhưng XSS vẫn có thể gửi request dưới phiên của nạn nhân. SameSite=Lax vẫn gửi cookie trong một số top-level navigation an toàn; endpoint thay đổi state không được dùng GET. Với cookie-authenticated mutation, dùng SameSite phù hợp CSRF token/origin checks theo threat model.

Không lưu session id, refresh token hoặc bearer credential dài hạn trong localStorage/sessionStorage: JavaScript cùng origin đọc được chúng khi có XSS. Với browser app, ưu tiên cookie HttpOnly; Secure hoặc BFF; response chứa session/token cần Cache-Control: no-store.


Session (có trạng thái)

Mô hình web kinh điển: server giữ dữ liệu session; cookie chỉ giữ một id mờ.

 LOGIN                                       LATER REQUEST
 browser ──email+pw──▶ server                browser ──Cookie: sid=abc──▶ server
                       verify hash                                        look up abc
                       create session abc ──▶ Redis                       in Redis ──▶ user
         ◄─Set-Cookie: sid=abc──                       ◄── response ──
import session from 'express-session';
import { RedisStore } from 'connect-redis';
import { Redis } from 'ioredis';

app.use(
  session({
    store: new RedisStore({ client: new Redis(process.env.REDIS_URL) }), // shared across servers
    name: '__Host-sid',
    secret: process.env.SESSION_SECRET!, // ký cookie id; không mã hóa session data
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      path: '/',
      maxAge: 86_400_000,
    },
  })
);

Sau login hoặc privilege change, gọi req.session.regenerate() rồi mới gắn principal để chống session fixation. Logout phải destroy server-side session và clear cookie với cùng name/path/sameSite/secure attributes. Nếu Redis unavailable, policy thường là fail closed cho endpoint cần auth; đừng âm thầm tạo session memory cục bộ trên từng replica.

Session cho phép thu hồi bằng cách xóa record server-side và dễ quản lý “logout all devices” nếu lưu index theo user. Đổi lại, mỗi request cần store lookup, TTL touch policy và một shared store có capacity/availability rõ. SESSION_SECRET cần đủ entropy và rotation; một số middleware nhận mảng secret để verify bằng key cũ trong lúc ký bằng key mới.


JWT (không trạng thái) — giải phẫu trước

JWT là ba phần Base64URL nối bằng dấu chấm: header.payload.signature.

eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiIxMjMiLCJleHAiOjE3... . 3Txs9...K2
└──── HEADER ────────┘ └──────── PAYLOAD ───────────┘   └─ SIGNATURE ─┘
 { "alg":"HS256",        { "sub":"123",                   HMAC_SHA256(
   "typ":"JWT" }           "role":"admin",                  header.payload,
                           "iat":1700000000,                secret )
                           "exp":1700604800 }

Hai sự thật ngăn mọi lỗi JWT:

  1. payload được , không mã hóa — ai cũng decode đọc được. Đừng để bí mật trong JWT.
  2. chữ ký chứng minh toàn vẹn — đổi một ký tự payload là chữ ký không khớp secret nữa, server từ chối.

Các claim cần validate theo contract: iss (issuer), aud (đúng API nhận token), sub (subject ổn định), exp/nbf (cửa sổ thời gian), iatjti khi cần định danh token. Decode payload không phải verify.

import jwt from 'jsonwebtoken';
import { randomUUID } from 'node:crypto';

const accessToken = jwt.sign(
  { sub: user.id, scope: ['posts:read'] },
  process.env.JWT_SECRET!,
  {
    algorithm: 'HS256',
    expiresIn: '15m',
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
    jwtid: randomUUID(),
  }
);

// Pin algorithm, issuer và audience; không lấy policy verify từ header của token.
try {
  const payload = jwt.verify(token, process.env.JWT_SECRET!, {
    algorithms: ['HS256'],
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
    clockTolerance: 5,
  });
} catch (err) {
  // map lỗi verify thành 401 chung; log reason nội bộ, không echo raw token
}

HS256 dùng shared secret: mọi verifier giữ secret cũng có khả năng ký. Thuật toán bất đối xứng như RS256/ES256/EdDSA tách private signing key khỏi public verification key, phù hợp khi nhiều resource server verify qua JWKS. Dù chọn gì, cần kid, rotation overlap, cache policy và xử lý unknown key; đừng hard-code một key vĩnh viễn.

Access + refresh token — mẫu production

Access token ngắn hạn giới hạn thiệt hại khi rò; refresh token dài hạn (trong cookie HttpOnly) lấy token mới âm thầm.

access token  : ngắn hạn ── dùng cho resource API; scope/audience hẹp
refresh token : dài hơn  ── cookie HttpOnly, chỉ gửi tới refresh endpoint
                             └ lưu hash + family state server-side để rotate/revoke
import { createHash, randomBytes } from 'node:crypto';

const digest = (token: string) =>
  createHash('sha256').update(token).digest('base64url');

// refreshRepo.rotate phải atomic: lock record/family, mark token cũ used,
// tạo token mới; nếu token cũ đã used thì revoke cả family.
app.post('/auth/refresh', async (req, res) => {
  const presented = req.cookies['__Secure-refresh'];
  if (typeof presented !== 'string') throw new InvalidCredentialsError();

  const nextToken = randomBytes(32).toString('base64url');
  const result = await refreshRepo.rotate({
    presentedHash: digest(presented),
    nextHash: digest(nextToken),
    expiresAt: new Date(Date.now() + 7 * 86_400_000),
  });

  if (result.kind !== 'rotated') {
    // `reused` đã revoke family trong cùng transaction; client chỉ nhận 401 chung.
    throw new InvalidCredentialsError();
  }

  res.setHeader('Cache-Control', 'no-store');
  res.cookie('__Secure-refresh', nextToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    path: '/auth/refresh',
  });
  res.json({ accessToken: await issueAccessToken(result.userId) });
});

Một record “current jti theo user” chỉ cho một device và dễ race khi hai refresh tới cùng lúc. Dùng token family/session riêng cho mỗi device, hash token at rest, rotate atomic, giữ trạng thái đủ lâu để phát hiện replay, và có policy xử lý concurrent tab. Refresh endpoint dùng cookie nên vẫn cần CSRF/origin defense phù hợp.

JWT access token tự chứa claim thường không thu hồi được trước exp. Khi thêm denylist/introspection để logout hoặc privilege change có hiệu lực tức thì, hệ thống đã tái lập state — đó có thể là trade-off đúng, nhưng phải đưa store availability và lookup latency vào thiết kế.

Session vs JWT — chọn có cân nhắc

Server-side sessionSelf-contained access JWT
State quyết định authsession storeclaim trong token tới khi hết hạn; key/revocation vẫn có state vận hành
Thu hồixóa/đổi session recordngắn exp, denylist hoặc introspection
Chi phí/requestnetwork/store lookup + deserializeverify chữ ký + kích thước token/key distribution
Scale nhiều instanceshared store/partition strategymọi verifier cần key, issuer/audience và clock policy nhất quán
Phù hợp khifirst-party web cần revoke/control tập trungnhiều resource server cần verify cục bộ và chấp nhận staleness ngắn

JWT không mặc định an toàn hơn session. Chọn theo revocation latency, client type, trust boundary, key ownership và outage mode. Với first-party browser app, session hoặc BFF thường đơn giản hơn việc phơi access token cho JavaScript.


Passport.js — strategy cắm được

Passport hợp nhất tất cả sau strategy (mỗi phương thức một cái).

import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import bcrypt from 'bcrypt';

const normalizeEmail = (value: string) => value.trim().toLowerCase(); // cùng policy lúc register

passport.use(
  new LocalStrategy(
    { usernameField: 'email' },
    async (email, password, done) => {
      const user = await User.findOne({
        where: { email: normalizeEmail(email) },
      });
      const stored = user?.passwordHash ?? process.env.DUMMY_BCRYPT_HASH!;
      const matches = await bcrypt.compare(password, stored);
      if (!user || !matches) return done(null, false); // cùng failure contract
      return done(null, user);
    }
  )
);
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';

passport.use(
  new JwtStrategy(
    {
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET!,
      algorithms: ['HS256'],
      issuer: 'https://auth.example.com',
      audience: 'https://api.example.com',
    },
    async (payload: { sub: string }, done) => {
      const user = await User.findByPk(payload.sub);
      return user ? done(null, user) : done(null, false);
    }
  )
);

OAuth2 / OIDC — “Đăng nhập với Google”

OAuth 2.0 là delegated authorization; OpenID Connect (OIDC) thêm identity layer để login. Với browser/native client, dùng Authorization Code + PKCE (S256), transaction-specific state, và nonce khi OIDC flow yêu cầu:

1. your app ──redirect──▶ Google consent screen
2. user approves
3. Google ──redirect to callbackURL?code=XYZ──▶ your app
4. your server ──exchange code + code_verifier (và client auth nếu confidential)──▶ provider
5. verify issuer/audience/signature/exp/nonce của id_token → map (iss, sub) → local account
6. issue YOUR session/access token; provider token không mặc định là token cho API của bạn
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';

passport.use(
  new GoogleStrategy(
    {
      clientID: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      callbackURL: '/auth/google/callback',
      state: true, // cần session/state store; callback phải validate state
    },
    async (_at, _rt, profile, done) => {
      const user = await identityRepo.findOrCreate({
        issuer: 'https://accounts.google.com',
        subject: profile.id, // stable provider id; không dùng email làm identity key
        email: profile.emails?.[0]?.value,
        displayName: profile.displayName,
      });
      return done(null, user);
    }
  )
);

Snippet Passport minh họa account mapping, không tự chứng minh full OIDC/PKCE compliance. Ưu tiên OIDC client hỗ trợ discovery/JWKS và validate toàn bộ ID Token. Không auto-link account chỉ vì email trùng nếu chưa có email_verified và policy chống account takeover; thay đổi/link identity nên yêu cầu re-authentication.


Phân quyền — RBAC, permission & sở hữu

Phân quyền theo vai trò chặn route theo role:

const authorize =
  (...roles: string[]): RequestHandler =>
  (req, _res, next) => {
    if (!req.user) return next(new UnauthorizedError());
    if (!roles.includes(req.user.role)) return next(new ForbiddenError());
    next();
  };

app.delete('/users/:id', requireAuth, authorize('admin'), deleteUser);

Mịn hơn thì dùng theo permission (role permission như user:delete). Và quy tắc hay sót nhất: kiểm tra quyền sở hữu (IDOR), không chỉ đăng nhập:

// User thường: scope query bằng principal + tenant ngay tại data boundary.
const post =
  req.user.role === 'admin'
    ? await Post.findOne({
        where: { id: req.params.id, tenantId: req.user.tenantId },
      })
    : await Post.findOne({
        where: {
          id: req.params.id,
          tenantId: req.user.tenantId,
          userId: req.user.id,
        },
      });

// 404 tránh tiết lộ resource có tồn tại ở tenant/user khác hay không.
if (!post) throw new NotFoundError();

RBAC chỉ là coarse gate. Quyền cuối cùng thường phụ thuộc action, resource, tenant, ownership và state hiện tại. Scope query theo principal giúp giảm nguy cơ quên check sau khi fetch; database row-level security có thể là lớp bổ sung, nhưng application vẫn phải truyền tenant/principal context đúng và test fail-closed.


Các phòng thủ OWASP mọi API Node cần

import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import cors from 'cors';

app.use(helmet());
app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true }));
app.use(express.json({ limit: '1mb', strict: true }));
app.use('/api/', rateLimit({ windowMs: 15 * 60_000, limit: 100 }));
app.use(
  '/auth/login',
  rateLimit({
    windowMs: 15 * 60_000,
    limit: 10,
    standardHeaders: 'draft-7',
  })
);

Rate-limit cần shared/external store khi có nhiều replica và trust proxy chính xác. Chỉ khóa cứng theo account có thể bị lợi dụng để DoS nạn nhân; phối hợp IP/device/account signal, progressive delay, MFA và alert theo risk.

Các mối đe dọa và lớp phòng thủ:

  • XSS: encode/sanitize theo output context, tránh dangerous sink, triển khai CSP/Trusted Types khi phù hợp. HttpOnly giảm token exfiltration nhưng không vô hiệu XSS.
  • CSRF: SameSite phù hợp, CSRF token gắn session hoặc kiểm Origin/Fetch Metadata; không đổi state bằng GET.
  • Injection: parameterized query, schema validation và allowlist cho identifier/command.
  • SSRF: allowlist scheme/host/port, resolve và chặn private/link-local/metadata IP, kiểm redirect và egress network policy.
  • Credential stuffing: generic response, rate/risk signal, breached-password check, MFA và audit.
  • Secret exposure: secret manager/KMS, least privilege, rotation; env chỉ là delivery mechanism, không phải secret store hoàn chỉnh.
  • Supply chain: lockfile, review lifecycle script, dependency scanning và phản ứng advisory; npm audit chỉ là một tín hiệu.
import { z } from 'zod';
const Register = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});
// validate at the edge → trust inward (Phase 3)

Failure modes cần diễn tập

Failure modeTác độngControl chínhBằng chứng cần có
Account enumerationlộ danh sách usergeneric response + dummy hash + rate signalstatus/body gần như nhau; timing distribution được đo
Session fixationattacker biết session id sau loginregenerate id khi auth/privilege đổiintegration test id trước/sau khác nhau
Refresh token replaychiếm phiên dài hạnatomic rotation + family revoke + auditdùng lại token cũ làm cả family mất hiệu lực
Role/tenant claim staleuser giữ quyền đã bị thu hồiaccess TTL ngắn, version/introspection cho action nhạy cảmđổi role rồi đo revocation latency
IDOR/BOLAđọc/sửa resource người khácquery scope theo tenant/owner + policynegative authorization matrix
CSRF trên cookie authmutation dưới phiên nạn nhânSameSite + token/origin policyrequest cross-site thiếu proof bị chặn
Key rotation lỗioutage hoặc token cũ sống quá lâukid, overlap, JWKS cache/rollbacktest key mới/cũ/unknown trong rollout
Session store outagefail-open hoặc login loopexplicit fail-closed/degraded policyfault injection Redis timeout

Không log raw password, session id, access/refresh token, OAuth authorization code hoặc Authorization header trong bất kỳ failure drill nào. Audit event nên chứa actor, action, target, outcome, request id và risk metadata tối thiểu; tránh biến audit log thành kho PII mới.

Hợp đồng với frontend

  • 401 nghĩa là credential thiếu/hết hạn/không hợp lệ. Client chỉ thử refresh theo một single-flight dùng chung giữa request đồng thời; refresh thất bại thì dừng và chuyển login.
  • 403 nghĩa là principal hiện tại không có quyền; không refresh lặp để “sửa” authorization.
  • 429 tôn trọng Retry-After; UI không tự brute-force login.
  • Response login/refresh/logout dùng Cache-Control: no-store. Logout nên idempotent và clear client state kể cả session đã hết hạn.
  • Browser dùng cookie cross-origin phải đặt credentials: 'include', server allowlist exact origin và CSRF policy tương ứng. Không dùng * với credentials.
  • Frontend map machine-readable code như AUTHENTICATION_REQUIRED, FORBIDDEN, MFA_REQUIRED; không parse message hoặc biết user có tồn tại.
  • Nhiều tab có thể refresh cùng lúc. Rotation protocol phải định nghĩa grace/concurrency behavior thay vì coi mọi reuse là attacker một cách mù quáng.

Checklist bảo mật trước khi ship

  • Hash mạnh; lỗi+thời gian giống nhau.
  • Token trong cookie an toàn.
  • Access ngắn + refresh xoay vòng có phát hiện tái dùng.
  • Phân quyền kiểm sở hữu ở tầng data.
  • helmet, CORS khóa origin, rate limit.
  • Input validate; query parameterized; giới hạn body.
  • Secret qua cơ chế có rotation/audit; lỗi chung cho client.
  • Session id regenerate sau login; logout/revoke đã test trên nhiều device.
  • Token verify pin algorithm, issuer, audience và key lifecycle.
  • Negative authorization test bao phủ tenant, owner, role và trạng thái resource.
  • Auth response no-store; log/redaction và audit event đã kiểm tra.

6. Dự án thực hành

  1. Auth mật khẩu hoàn chỉnh: xác nhận giá trị lưu là hash và hai lỗi giống nhau.

  2. JWT access + refresh có xoay vòng: phát access 15m + refresh 7d trong cookie HttpOnly; dựng /auth/refresh có xoay vòng + phát hiện tái dùng và logout thu hồi.

  3. Session trong Redis: đổi sang session + Redis; chứng minh đăng xuất tức thì bằng cách xóa session.

  4. Đăng nhập OAuth2/OIDC: đấu Google/GitHub với find-or-create và hoàn tất luồng.

  5. RBAC + sở hữu: thêm role + permission; chứng minh IDOR trả 403.

  6. Gia cố vành đai: thêm helmet, CORS khóa, rate-limit (chứng minh 429), Zod, và token CSRF.

Tiêu chí hoàn thành: test phải chứng minh password plaintext/token không xuất hiện trong DB hoặc log; login user-không-tồn-tại và sai-password cùng contract; session id đổi sau login; refresh token chỉ dùng được một lần theo policy; token sai iss/aud/alg bị từ chối; user A không truy cập resource user B/tenant B; CSRF request bị chặn; logout all devices có revocation latency đo được.

Nếu chỉ nhớ năm điều

  • Authentication, session management và authorization là ba boundary khác nhau.
  • Password hashing parameter là capacity/security decision phải benchmark và rehash theo thời gian.
  • Cookie HttpOnly giảm token theft nhưng không loại XSS hoặc CSRF.
  • JWT chỉ đáng tin sau khi verify algorithm, key, issuer, audience và thời gian; claim vẫn có thể stale.
  • Authorization phải scope resource theo tenant/owner phía server và được kiểm bằng negative test.

Phần tiếp theo

API giờ có threat model và control có thể kiểm chứng, nhưng các dependency vẫn đang được khởi tạo và gọi trực tiếp. Ở Phần 6, ta tổ chức dependency, repository, resilience và background work thành boundary rõ ràng.

Đọc tiếp: Advanced Patterns & Packages.

Tài liệu chính thức và chuẩn ngành