const [h, p] = token.split('.');
JSON.parse(Buffer.from(p, 'base64url').toString()); // anyone can read the claims
Authentication, OAuth2, OIDC & Authorization
draftedSession vs JWT, refresh rotation/revocation, OAuth 2.0 flows + PKCE, OIDC, SSO/SAML, passkeys, RBAC vs ABAC vs ReBAC, tenant-scoped roles, policy engines.
0/66 rated · mastery 0%
Bài học
12 bài · ~142 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~15 phút· 4 câuverify
Session, cookie và API key: chứng minh danh tính trên từng request
AuthN khác AuthZ ở đâu và vì sao 401/403/404 không thay thế cho nhau; session server-side và cookie flags; session fixation và vòng đời session (login, đổi quyền, logout); chọn session hay JWT cho web app; thiết kế API key cho đối tác. Có demo express-session thật cho fixation và API key hash.
- 02~14 phút· 6 câuverify
JWT: cấu trúc, chữ ký và cách verify an toàn
JWS là gì và vì sao payload không bí mật; registered claims; HS256 vs RS256 vs ES256/EdDSA với số đo thật; alg none và algorithm confusion chạy thật trên jsonwebtoken 8.5.1, 9.0.3 và jose 6; tấn công qua kid/jku/x5u/jwk; clock skew và leeway; checklist verify cho resource server.
- 03~13 phút· 9 câuverify
Access token, refresh token, rotation và revocation
Vì sao access token ngắn và refresh token chỉ đi tới authorization server; rotation + reuse detection theo family cài trên Postgres 17 và chạy thật (kể cả race hai tab và trường hợp attacker refresh trước); mặc định thật của Keycloak 26.4; các cách revoke JWT (TTL, denylist jti, token_version, introspection, phantom token); xử lý sự cố reuse và nhân viên bị xoá khỏi tenant.
- 04~11 phút· 5 câuverify
OAuth 2.0: vai trò, scope và các grant type
OAuth giải quyết bài toán ủy quyền nào; bốn vai trò áp vào ví dụ Google Calendar; scope khác role và permission ra sao; Client Credentials với client secret và private_key_jwt; Device Authorization Grant chạy thật trên Keycloak 26.4; vì sao Implicit và Password grant bị loại theo RFC 9700 và OAuth 2.1 đổi gì.
- 05~11 phút· 5 câuverify
Authorization Code + PKCE từng bước
Đi từng tham số của Authorization Code flow và nơi mỗi tham số được kiểm tra; PKCE chống code interception/injection (kiểm bằng test vector RFC 7636); state, nonce và PKCE khác nhau ra sao; mix-up attack và tham số iss (RFC 9207); redirect URI so khớp prefix bị khai thác thế nào; mobile redirect và RFC 8252. Toàn bộ flow chạy thật trên Keycloak 26.4.7.
- 06~12 phút· 6 câuverify
OpenID Connect: ID token, discovery và JWKS
OIDC thêm gì lên OAuth và vì sao login bằng OAuth thuần nguy hiểm; ID token khác access token ở audience và cách dùng; checklist verify ID token; discovery và JWKS đọc từ Google và Keycloak thật; mô phỏng key rotation với jose (cache, refetch khi gặp kid lạ, cooldown 30 giây) và sự cố 401 hàng loạt sau rotation; Sign in with Google: flow nào, kiểm gì, map identity thế nào.
- 07~11 phút· 3 câuverify
Token trong browser: localStorage, cookie, BFF và DPoP
XSS làm gì được với token ở từng nơi lưu (localStorage, memory, HttpOnly cookie); ba kiến trúc của IETF cho browser app (BFF, token-mediating backend, browser-only); BFF giải quyết và tạo ra vấn đề gì, cài trong Next.js ra sao; sender-constrained token với DPoP (RFC 9449) và mTLS (RFC 8705), có demo DPoP chạy thật với jose: replay, sai URL, token bị đánh cắp.
- 08~10 phút· 4 câuverify
Audience, service-to-service và identity propagation
Vì sao thiếu kiểm aud làm billing chấp nhận token của analytics (chạy thật), scope so khớp chuỗi con; resource indicators; ba cách truyền danh tính user giữa service (forward token, token exchange RFC 8693 chạy thật trên Keycloak 26.4, JWT nội bộ + mTLS); service identity trên Kubernetes (shared secret, mesh mTLS/SPIFFE, client credentials); identity trong Kafka event.
- 09~12 phút· 7 câuverify
SSO, SAML, logout và federation cho B2B
SSO hoạt động thế nào và vì sao deprovision không tự huỷ session ở app; SAML 2.0 vs OIDC và các bẫy SAML (signature wrapping, IdP-initiated, replay); ba kiểu logout của OIDC; Keycloak realm-per-tenant vs single realm + Organizations (chạy thật trên 26.4.7); thiết kế login cho B2B SaaS có tenant mang IdP riêng; migrate từ JWT HS256 tự viết sang OIDC; build vs buy identity.
- 10~11 phút· 4 câuverify
MFA, passkeys và account linking
Các nhóm factor, vì sao SMS OTP và cả TOTP vẫn phishable; WebAuthn/passkey hoạt động thế nào và tại sao chống được phishing (mô phỏng chạy thật bằng node:crypto với clientDataJSON và rpIdHash); synced vs device-bound; rollout passkey cho 5 triệu user; account pre-hijacking trong social login và quy tắc liên kết tài khoản an toàn.
- 11~11 phút· 7 câuverify
Authorization models: RBAC, ABAC, ReBAC và policy engine
RBAC và vì sao check permission chứ không check tên role; ABAC với điều kiện trên thuộc tính (Cedar 4.13 chạy thật); ReBAC và mô hình Zanzibar với tuple, check, list objects và new enemy problem (OpenFGA 1.21 chạy thật); so sánh OPA, Cedar, Casbin, OpenFGA; authorization cho list/search (policy thành filter, SQL chạy thật trên Postgres 17); PDP/PEP cho 15 microservice.
- 12~11 phút· 6 câuverify
Authorization multi-tenant: role theo membership, tenant context và cache quyền
Vì sao role phải gắn với membership (user, tenant) chứ không gắn với user; schema có custom role theo tenant và ràng buộc chống gán chéo tenant (chạy thật trên Postgres 17); lỗi refund chéo tenant và cách sửa; tenant context đến từ đâu (token, header, URL) và vì sao luôn kiểm membership; cache quyền có version và invalidation (đo thật); tách staff và customer trong B2B2C.
Tóm tắt & cheat sheet của track
TL;DR
- AuthN = bạn là ai; AuthZ = bạn được làm gì với resource nào. Phần lớn sự cố thật nằm ở AuthZ (thiếu check object/tenant), không ở login.
- OAuth 2.0 là framework ủy quyền (client lấy access token để gọi API thay user); OIDC thêm lớp định danh (ID token, userinfo, discovery). Flow chuẩn cho mọi client có user: Authorization Code + PKCE. Implicit và Password grant đã bị loại (RFC 9700, hướng OAuth 2.1).
- Session vs JWT không phải "cũ vs mới": session dễ revoke, JWT tiện cho phân tán nhưng khó thu hồi → access token ngắn + refresh token rotation + reuse detection.
- JWT pitfalls kinh điển: tin
algtrong header (none, HS/RS confusion), không checkaud/iss, dùngdecodethayverify, token sống quá lâu, nhét quá nhiều quyền vào token. - Authorization models: RBAC (role → permission), ABAC (thuộc tính + điều kiện), ReBAC (quan hệ, Zanzibar). Multi-tenant: role gắn với membership (user, tenant), không gắn với user.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Session | Server lưu state, client giữ session id trong cookie | __Host-sid=... → Redis sess:... |
| JWT (JWS) | Token tự chứa claims, ký (không mã hoá) | header.payload.signature, payload đọc được bằng base64 |
| Access token | Credential để gọi resource server | Authorization: Bearer eyJ..., sống 5–15 phút |
| Refresh token | Đổi lấy access token mới, sống lâu | Rotation: mỗi lần dùng cấp token mới, huỷ token cũ |
| ID token | JWT cho client biết user là ai (OIDC) | sub, iss, aud=client_id, nonce |
| PKCE | Ràng buộc code với client tạo ra nó | code_challenge = BASE64URL(SHA256(verifier)) |
| Scope | Quyền client được ủy quyền | orders:read |
| RBAC | User → role → permission | store_manager → order.refund |
| ABAC | Quyết định theo thuộc tính user/resource/context | resource.tenant == user.tenant && amount < 5M |
| ReBAC | Quyết định theo quan hệ đồ thị | user:alice là editor của folder:x → edit doc:y trong folder |
| Passkey | Credential WebAuthn (public-key) gắn với origin | Chống phishing, không có shared secret |
Cơ chế hoạt động
Authorization Code + PKCE (OIDC)
Refresh token rotation + reuse detection
Quyết định authorization trong một request multi-tenant
Trade-offs
| Quyết định | Lựa chọn A | Lựa chọn B | Ghi chú |
|---|---|---|---|
| Web session | Server session (cookie) | JWT stateless | Session: revoke tức thì; JWT: không cần lookup nhưng revoke khó |
| Token của SPA | BFF giữ token, browser chỉ có cookie | Token trong JS memory | BFF an toàn nhất; memory + refresh rotation chấp nhận được |
| Access token format | JWT (verify local) | Opaque + introspection | JWT nhanh; opaque revoke tức thì, không lộ claims |
| Ký JWT | Asymmetric (RS256/ES256/EdDSA) | Symmetric HS256 | Nhiều verifier → asymmetric; HS chỉ khi issuer = verifier |
| Quyền trong token | Nhét role/permission vào claims | Tra cứu lúc request | Claims nhanh nhưng stale tới khi hết hạn và token phình to |
| Model | RBAC | ABAC / ReBAC | RBAC đủ cho đa số; share/hierarchy → ReBAC; điều kiện động → ABAC |
| Enterprise SSO | OIDC | SAML 2.0 | OIDC cho app mới; SAML vì khách hàng enterprise yêu cầu |
| Policy | Code trong service | Policy engine (OPA/Cedar/OpenFGA) | Engine khi nhiều service/nhiều team cần một nguồn luật |
Pitfalls
jwt.decode()thayjwt.verify(); không truyềnalgorithmsallowlist; không checkaud/iss.- Dùng ID token để gọi API, hoặc chấp nhận access token cấp cho service khác (thiếu
aud). - Access token sống 24h, không có refresh rotation, không có cách revoke khi user bị xoá khỏi tenant.
- Lưu refresh token trong
localStorage. - Role lưu trên bảng
users(một role toàn cục) trong khi user thuộc nhiều tenant. - Check role ở route nhưng không check resource thuộc tenant nào.
- Tin
emailtừ IdP làm khoá định danh / auto-link account khiemail_verifiedkhông đúng. redirect_uriso khớp prefix/wildcard → rò code.- Không xử lý JWKS rotation (cache key mãi mãi, hoặc fetch JWKS mỗi request).
Cheat sheet
- Flows: có user → Auth Code + PKCE; machine-to-machine → Client Credentials; TV/CLI → Device Authorization Grant (RFC 8628). Không dùng Implicit/ROPC.
state= chống CSRF ở callback ·nonce= gắn ID token với request (chống replay/injection) · PKCE = chống code interception/injection.- Verify JWT: allowlist
alg→ key theokidtừ JWKS của issuer đã cấu hình → signature →iss→aud→exp/nbf(leeway ~60s) → claims nghiệp vụ. - Access token 5–15', refresh token rotation + reuse detection + absolute lifetime; revoke family khi reuse.
- Revoke JWT: TTL ngắn, denylist
jti,token_version/sessionstable, introspection. - Multi-tenant RBAC:
memberships(user_id, tenant_id)+membership_roles+role_permissions; check (tenant membership → permission → object). - SAML = XML assertion, enterprise legacy; OIDC = JSON/JWT, discovery, mobile-friendly.
- Passkeys: WebAuthn, phishing-resistant vì bound với RP ID (origin), private key không rời authenticator.
References
- RFC 6749 — The OAuth 2.0 Authorization Framework ↗
- RFC 7636 — Proof Key for Code Exchange (PKCE) ↗
- RFC 9700 — Best Current Practice for OAuth 2.0 Security ↗
- RFC 7519 — JSON Web Token (JWT) ↗
- RFC 8725 — JSON Web Token Best Current Practices ↗
- RFC 8628 — OAuth 2.0 Device Authorization Grant ↗
- RFC 9068 — JWT Profile for OAuth 2.0 Access Tokens ↗
- RFC 9449 — OAuth 2.0 Demonstrating Proof of Possession (DPoP) ↗
- RFC 8693 — OAuth 2.0 Token Exchange ↗
- RFC 7662 — OAuth 2.0 Token Introspection ↗
- OAuth 2.1 (IETF draft) ↗
- OAuth 2.0 for Browser-Based Applications (IETF draft) ↗
- OpenID Connect Core 1.0 ↗
- OpenID Connect Discovery 1.0 ↗
- OpenID Connect Back-Channel Logout 1.0 ↗
- W3C — Web Authentication (WebAuthn) Level 3 ↗
- Zanzibar — Google's Consistent, Global Authorization System (USENIX ATC 2019) ↗
- Open Policy Agent documentation ↗
- Cedar policy language ↗
- OpenFGA documentation ↗
- Casbin documentation ↗
- Keycloak documentation ↗
- OWASP Cheat Sheet — JSON Web Token for Java (concepts apply generally) ↗
- OWASP Cheat Sheet — Session Management ↗
Personal notes (Notion)
Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.
Interview Questions
Showing 66 / 66
// avoid: if (user.role === 'admin' || user.role === 'manager')
if (!(await authz.can(ctx, 'order.refund'))) throw new ForbiddenError();
curl -X POST https://auth.example.com/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials -d scope="inventory:read"
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://idp.example.com/.well-known/jwks.json'));
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://idp.example.com',
audience: 'orders-api',
algorithms: ['RS256'],
clockTolerance: 60,
});
CREATE TABLE memberships (
id uuid PRIMARY KEY, user_id uuid NOT NULL, tenant_id uuid NOT NULL,
status text NOT NULL DEFAULT 'active', UNIQUE (user_id, tenant_id), UNIQUE (id, tenant_id)
);
CREATE TABLE roles (id uuid PRIMARY KEY, tenant_id uuid NULL, key text NOT NULL);
CREATE TABLE membership_roles (
membership_id uuid NOT NULL, tenant_id uuid NOT NULL, role_id uuid NOT NULL REFERENCES roles(id),
PRIMARY KEY (membership_id, role_id),
FOREIGN KEY (membership_id, tenant_id) REFERENCES memberships (id, tenant_id)
);
-- app/trigger check: roles.tenant_id IS NULL OR roles.tenant_id = membership_roles.tenant_id
import jwt from 'jsonwebtoken';
export function auth(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
const claims = jwt.decode(token) as any; // "we verify at the gateway"
if (!claims || claims.exp * 1000 < Date.now()) return res.sendStatus(401);
req.user = { id: claims.sub, tenantId: claims.tid, roles: claims.roles };
next();
}
// elsewhere, the token service
jwt.verify(token, process.env.JWT_KEY!); // no algorithms option
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://idp.example.com',
algorithms: ['RS256'],
});
if (!payload.scope?.toString().includes('read')) throw new Forbidden();
app.post('/auth/refresh', async (req, res) => {
const rt = await db.refreshToken.findUnique({ where: { token: req.body.refreshToken } });
if (!rt || rt.expiresAt < new Date()) return res.sendStatus(401);
const newRt = randomToken();
await db.refreshToken.create({ data: { token: newRt, userId: rt.userId, expiresAt: addDays(new Date(), 30) } });
res.json({ accessToken: signAccess(rt.userId), refreshToken: newRt });
});
router.post('/orders/:id/refund', requireAuth, requirePermission('order.refund'), async (req, res) => {
const order = await orders.findById(req.params.id);
await payments.refund(order.paymentId, order.total);
res.json({ ok: true });
});
// requirePermission loads roles for the user across ALL memberships
async function userPermissions(userId: string) {
return db.query(`SELECT p.key FROM membership_roles mr
JOIN memberships m ON m.id = mr.membership_id
JOIN role_permissions p ON p.role_id = mr.role_id WHERE m.user_id = $1`, [userId]);
}
function isAllowedRedirect(client: Client, redirectUri: string) {
return client.redirectUris.some((allowed) => redirectUri.startsWith(allowed));
}
// registered: https://shop.example.com/callback