Software Architecture

Identity & Access Management (IAM): Thiết kế hệ thống xác thực tập trung (OAuth2/OIDC)

7/19/2026 · 12p đọc


title: "Identity & Access Management (IAM): Thiết kế hệ thống xác thực tập trung (OAuth2/OIDC)"
series: "Software Architecture Masterclass"
part: "Phần 4 — Bảo mật & Chất lượng"
order: 33
audience: "Solutions Architect & Technical Lead"
reading_time: "13 phút"
tags: ["iam", "oauth2", "oidc", "authentication", "authorization", "sso", "identity-provider", "token-design", "zero-trust", "security"]

Identity & Access Management (IAM): Thiết kế hệ thống xác thực tập trung (OAuth2/OIDC)

Một công ty có 12 ứng dụng nội bộ — CRM, hệ thống kế toán, dashboard vận hành, cổng nhân sự, công cụ hỗ trợ khách hàng, và vài ứng dụng nữa được viết bởi các đội khác nhau qua nhiều năm. Mỗi ứng dụng có một bảng users, một cột password_hash, một luồng "quên mật khẩu" riêng, và một cơ chế session riêng — cái thì cookie, cái thì JWT tự ký, cái thì session lưu trong Redis với TTL tùy tiện đặt bằng tay.

Đến một ngày, nhân viên nghỉ việc. Phòng nhân sự báo IT thu hồi quyền truy cập. IT vào từng hệ thống một, tắt tài khoản. Ba tuần sau, một audit bảo mật phát hiện tài khoản đó vẫn còn hoạt động ở hệ thống thứ tư — một ứng dụng nội bộ ít ai nhớ tới, do một đội đã giải thể xây dựng, có logic auth riêng không ai còn hiểu rõ. Không ai cố ý để lỗ hổng này tồn tại — nó là hệ quả tất yếu của việc để 12 đội tự quyết định "auth" nghĩa là gì.

Đây không phải câu chuyện về một bug. Đây là hậu quả kiến trúc của một quyết định (hoặc sự thiếu quyết định) được đưa ra từ rất sớm: để mỗi ứng dụng tự xây dựng xác thực (authentication) và ủy quyền (authorization) của riêng nó, thay vì tập trung nó lại thành một năng lực hạ tầng dùng chung.

The Problem

Vấn đề kiến trúc cụ thể: khi mỗi ứng dụng trong hệ thống tự cài đặt logic đăng nhập riêng, tổ chức phải gánh ba loại chi phí song song, và cả ba đều tăng theo cấp số nhân với số lượng ứng dụng:

  1. Thực hành bảo mật không nhất quán. Đội A hash password bằng bcrypt với cost factor hợp lý; đội B dùng MD5 vì "nhanh hơn" và không ai review lại. Đội C implement rate-limiting cho login; đội D thì không, để lộ endpoint cho brute-force. Mỗi lần cài lại logic auth là một lần có xác suất khác-không mắc lỗi bảo mật cơ bản — và với 12 lần cài lại, xác suất tổng thể tiệm cận chắc chắn.

  2. Quản lý vòng đời người dùng (user lifecycle) across hệ thống trở thành cơn ác mộng vận hành. Không có một nguồn sự thật (source of truth) duy nhất cho câu hỏi "người này còn được phép truy cập hệ thống nào?". Onboarding một nhân viên mới nghĩa là tạo tài khoản thủ công ở N nơi; offboarding nghĩa là nhớ — và nhớ đúng — thu hồi ở N nơi đó. Con người quên. Quy trình thủ công luôn rò rỉ.

  3. Không có điểm kiểm soát tập trung cho chính sách. Muốn bắt buộc MFA (xác thực đa yếu tố) toàn công ty? Phải sửa 12 lần. Muốn audit "ai đã đăng nhập vào hệ thống tài chính trong 90 ngày qua"? Phải hỏi 12 hệ thống log khác nhau, mỗi cái một định dạng.

Gốc rễ: authentication và authorization là một cross-cutting concern — một mối quan tâm cắt ngang mọi domain nghiệp vụ, giống hệt logging hay observability (xem Bài 28). Nhưng khác với logging, sai sót ở đây không chỉ gây khó chịu vận hành — nó tạo ra lỗ hổng bảo mật có thể dẫn tới rò rỉ dữ liệu, mất quyền kiểm soát tài khoản, hoặc vi phạm compliance có hậu quả pháp lý.

The Theory

Giải pháp kiến trúc chuẩn là tập trung hóa danh tính (centralized identity): xây dựng (hoặc mua) một Identity Provider (IdP) duy nhất, và mọi ứng dụng ủy thác việc xác thực người dùng cho IdP này thay vì tự làm. Hai chuẩn công nghiệp làm nền cho mô hình này là OAuth2OIDC (OpenID Connect) — và điều quan trọng nhất cần hiểu là chúng giải quyết hai bài toán khác nhau, dù thường bị nhầm là một.

OAuth2 — chuẩn ủy quyền (authorization), không phải xác thực

OAuth2 trả lời câu hỏi: "Ứng dụng A có được phép truy cập tài nguyên của người dùng trên hệ thống B hay không, và với phạm vi (scope) nào?" Nó không được thiết kế để nói cho ứng dụng biết "người dùng này là ai" — nó chỉ cấp một access token đại diện cho một tập quyền hạn cụ thể, trong một khoảng thời gian giới hạn.

Luồng phổ biến và an toàn nhất là Authorization Code Flow: người dùng được redirect tới IdP, đăng nhập tại đó (mật khẩu không bao giờ đi qua ứng dụng gốc), IdP trả về một authorization code ngắn hạn qua redirect, ứng dụng dùng code đó đổi lấy access token (và tùy chọn, refresh token) qua một lời gọi backend-to-backend — bước này quan trọng vì nó giữ token không bao giờ lộ ra trên URL hay trình duyệt của client.

OIDC — lớp xác thực (authentication) xây trên OAuth2

OAuth2 gốc chỉ nói về quyền truy cập tài nguyên, không định nghĩa danh tính. Đây là chỗ nhiều hệ thống early-stage nhầm lẫn tai hại: dùng access token của OAuth2 để suy luận "ai đang đăng nhập" là dùng sai công cụ.

OIDC là một lớp mở rộng trên OAuth2, bổ sung một loại token thứ ba: ID Token — một JWT (JSON Web Token) có cấu trúc chuẩn hóa, chứa thông tin định danh người dùng (subject id, email, thời điểm xác thực...) và được ký số bởi IdP để ứng dụng có thể verify tính toàn vẹn mà không cần gọi ngược lại IdP mỗi lần. Nói ngắn gọn: access token trả lời "được làm gì", ID token trả lời "là ai". Một hệ thống IAM đúng chuẩn dùng cả hai, tách bạch rõ ràng.

SSO — hệ quả kiến trúc, không phải mục tiêu độc lập

Khi mọi ứng dụng đều ủy thác xác thực cho cùng một IdP, Single Sign-On (SSO) xuất hiện gần như miễn phí: người dùng đăng nhập một lần tại IdP, session ở đó được các ứng dụng khác nhận diện lại (qua cookie session của IdP hoặc token đã cấp), không cần đăng nhập lại. SSO không phải một tính năng cần xây riêng — nó là hệ quả tự nhiên của việc tập trung hóa danh tính đúng cách.

Vị trí IAM trong kiến trúc tổng thể

IAM tập trung thường đứng ở lớp biên (edge) của hệ thống — trước hoặc tích hợp cùng API Gateway (xem Bài 20) — xác thực request trước khi nó chạm tới bất kỳ service nghiệp vụ nào, và đây cũng chính là nền tảng thực thi của mô hình Zero Trust (xem Bài 32): không service nào được ngầm tin request chỉ vì nó đến từ trong mạng nội bộ, mọi request đều phải mang theo một token đã được IdP cấp và có thể verify độc lập.

The Trade-off

Đây là phần không được phép mơ hồ: tập trung hóa IAM là đánh đổi, không phải nâng cấp miễn phí.

Được gì:

  • Giảm logic auth trùng lặp gần như về không. 12 ứng dụng không còn 12 bản cài đặt password-hashing, session-management, MFA riêng — chỉ còn một bản, được review kỹ, được vá lỗi tập trung. Một lỗ hổng được phát hiện và vá một lần, có hiệu lực ở mọi ứng dụng ngay lập tức.
  • Nhất quán chính sách toàn hệ thống. Bật MFA bắt buộc, đặt độ dài token, cấu hình chính sách khóa tài khoản sau N lần thử sai — làm một lần ở IdP, áp dụng mọi nơi. Offboarding nhân viên trở thành một thao tác: vô hiệu hóa tài khoản ở IdP, mọi quyền truy cập bị cắt ngay lập tức ở mọi ứng dụng phụ thuộc.
  • Trải nghiệm người dùng tốt hơn nhờ SSO — không phải nhớ N mật khẩu cho N hệ thống nội bộ.
  • Audit tập trung. Một log duy nhất trả lời "ai đăng nhập vào đâu, khi nào" thay vì phải hợp nhất log từ N nguồn định dạng khác nhau.

Mất gì — và đây là cái giá thật, không phải rủi ro lý thuyết:

  • IAM trở thành single point of failure (điểm lỗi trọng yếu duy nhất) của toàn hệ thống. Trước đây nếu logic auth của ứng dụng A bị lỗi, chỉ ứng dụng A bị ảnh hưởng. Sau khi tập trung hóa, nếu IdP sập hoặc bị quá tải, toàn bộ 12 ứng dụng ngừng cho phép đăng nhập mới cùng lúc — một sự cố duy nhất giờ có bán kính ảnh hưởng (blast radius) bằng cả tổ chức. Đây không phải rủi ro có thể loại bỏ, chỉ có thể giảm nhẹ bằng kiến trúc HA riêng cho chính IdP (xem Bài 26) — và bản thân điều đó lại là một khoản đầu tư vận hành đáng kể, không miễn phí.
  • Độ trễ và điểm phụ thuộc mạng tăng thêm ở đường xác thực. Mỗi lần cần verify token hoặc refresh, có một lời gọi mạng tới IdP (hoặc ít nhất, một bước verify chữ ký) — nếu thiết kế caching/verify sai (ví dụ verify JWT signature cục bộ bằng public key thay vì gọi introspection endpoint mỗi lần), độ trễ này có thể kiểm soát được; nếu thiết kế sai, nó trở thành một tail-latency ẩn ảnh hưởng mọi request.
  • Thiết kế token/session đòi hỏi kỷ luật cao hơn nhiều so với session tự quản trong một ứng dụng đơn lẻ. Token giờ di chuyển qua nhiều biên tin cậy (trust boundary) — từ IdP tới client, từ client tới API Gateway, từ Gateway tới service nội bộ. Mỗi biên là một cơ hội để token bị đánh cắp, replay, hoặc dùng sai scope. Bài toán "token hết hạn thì làm sao refresh mà không bắt người dùng đăng nhập lại giữa chừng, nhưng cũng không giữ refresh token sống vĩnh viễn" là một bài toán thiết kế thật, không tầm thường như "session cookie sống 7 ngày" của một ứng dụng đơn lẻ.
  • Chi phí chuyển đổi (migration cost) không nhỏ nếu hệ thống đã có 12 ứng dụng với 12 kiểu auth khác nhau. Đây không phải việc bật một config — nó là một dự án tái cấu trúc thực sự, cần chạy song song hai hệ thống trong giai đoạn chuyển tiếp, và rủi ro khóa nhầm người dùng thật trong lúc migrate là có thật.

Không có lựa chọn nào miễn phí. Câu hỏi kiến trúc đúng không phải "có nên tập trung IAM hay không" — với hầu hết tổ chức có nhiều hơn 2-3 ứng dụng, câu trả lời gần như luôn là có — mà là "IdP của chúng ta có đủ độ tin cậy (HA, DR) để gánh vai trò single point of failure mà nó vừa được trao hay chưa".

The Architecture Diagram

sequenceDiagram
    participant User as Người dùng
    participant App as Ứng dụng (Client)
    participant IdP as Identity Provider (IdP)
    participant AuthEP as Authorization Endpoint
    participant TokenEP as Token Endpoint
    participant API as API nghiệp vụ

    User->>App: Truy cập ứng dụng, chưa đăng nhập
    App->>AuthEP: Redirect kèm client_id, redirect_uri, scope, state
    AuthEP->>User: Hiển thị màn hình đăng nhập (thuộc IdP, không thuộc App)
    User->>AuthEP: Nhập thông tin đăng nhập + MFA (nếu bật)
    AuthEP->>AuthEP: Xác thực thông tin, tạo authorization code ngắn hạn

    Note over AuthEP,App: Redirect kèm authorization code qua trình duyệt (không lộ token)
    AuthEP-->>App: Redirect về redirect_uri?code=XYZ&state=...

    rect rgb(230, 240, 255)
        Note over App,TokenEP: Bước backend-to-backend, không qua trình duyệt
        App->>TokenEP: Đổi code lấy token (kèm client_secret hoặc PKCE)
        TokenEP->>TokenEP: Verify code, client, redirect_uri khớp
        TokenEP-->>App: Trả về Access Token + ID Token (OIDC) + Refresh Token
    end

    App->>App: Verify chữ ký ID Token, đọc thông tin danh tính người dùng
    App->>API: Gọi API kèm Access Token (Bearer)
    API->>API: Verify Access Token (chữ ký + scope + hạn dùng)
    API-->>App: Trả dữ liệu nếu token hợp lệ và đủ scope

    Note over App,TokenEP: Khi Access Token hết hạn (thường ngắn: phút-giờ)
    App->>TokenEP: Dùng Refresh Token xin Access Token mới
    TokenEP-->>App: Access Token mới (không cần người dùng đăng nhập lại)

Implementation Guardrails

  1. Không bao giờ tự implement crypto hay logic auth riêng — dùng thư viện/chuẩn đã được kiểm chứng và bảo trì tích cực. Không tự viết hàm hash password, không tự chế "JWT" bằng cách nối chuỗi và ký tay, không tự nghĩ ra luồng OAuth "giản lược cho nhanh". Auth là lĩnh vực có hàng trăm cách làm sai tinh vi mà chỉ chuyên gia bảo mật toàn thời gian mới nhận ra — dùng implementation OAuth2/OIDC đã qua kiểm định (thư viện chuẩn của ngôn ngữ, hoặc IdP thương mại/mã nguồn mở đã được audit).

  2. Access token phải có thời hạn ngắn (phút đến vài giờ), kèm cơ chế refresh token riêng biệt để duy trì phiên mà không bắt người dùng đăng nhập lại liên tục. Access token sống ngắn giới hạn thiệt hại nếu bị đánh cắp — kẻ tấn công chỉ có một cửa sổ hẹp để lợi dụng nó. Refresh token sống lâu hơn nhưng phải được lưu trữ an toàn hơn (không lưu ở nơi JavaScript phía client truy cập được) và phải hỗ trợ thu hồi (revocation) tức thời từ phía IdP.

  3. Luôn xác thực chữ ký và các trường bắt buộc của JWT (issuer, audience, hạn dùng) ở phía nhận — không bao giờ tin nội dung token chỉ vì nó decode được. JWT có thể decode bằng bất kỳ ai (nó không được mã hóa, chỉ được ký) — an toàn của nó nằm hoàn toàn ở bước verify chữ ký bằng public key của IdP. Bỏ qua bước verify, hoặc verify sai issuer/audience, là lỗ hổng cho phép giả mạo token từ một IdP khác hoặc dùng nhầm token của ứng dụng khác.

  4. Với client chạy trên trình duyệt hoặc mobile (không giữ được bí mật client_secret an toàn), bắt buộc dùng PKCE (Proof Key for Code Exchange) thay vì Authorization Code Flow trần. PKCE chống lại việc authorization code bị đánh cắp giữa bước redirect và bước đổi token — một lỗ hổng thực tế đã từng bị khai thác trên các ứng dụng mobile/SPA không dùng PKCE.

Architect's Verdict

Tự xây hệ thống auth riêng gần như luôn là một sai lầm, và nó là loại sai lầm âm thầm — không lộ ra ngay, chỉ lộ ra khi bị khai thác hoặc khi audit phát hiện. OAuth2 và OIDC tồn tại không phải vì ngành công nghiệp thích thêm chuẩn cho vui, mà vì xác thực và ủy quyền khó làm đúng hơn vẻ ngoài của nó rất nhiều — mọi chi tiết nhỏ (redirect URI validation, token lifetime, chữ ký JWT, PKCE) đều là một lớp phòng thủ chống lại một lớp tấn công thật đã từng xảy ra với ai đó trước bạn. Nếu tổ chức có nhiều hơn một vài ứng dụng, đầu tư vào một IdP tập trung đáng tin cậy sớm — món nợ kỹ thuật của N hệ thống auth rời rạc chỉ tăng, không bao giờ tự giảm.

🔗 Bài viết liên quan: Zero Trust Architecture — IAM tập trung là nền tảng thực thi của việc "không tin ai theo mặc định"; Security by Design — nguyên tắc thiết kế bảo mật ngay từ đầu mà token/session design trong bài này là một ứng dụng cụ thể.


Bài trước: Zero Trust Architecture · Bài tiếp theo: Performance Engineering

Identity & Access Management (IAM): Thiết kế hệ thống xác thực tập trung (OAuth2/OIDC)