Đang tải...

OAuth2 Account Takeovers: Xây dựng Kiến trúc Social Login Bất Khả Xâm Phạm

07/05/2026
7 phút đọc
OAuth2 Account Takeovers: Xây dựng Kiến trúc Social Login Bất Khả Xâm Phạm
Chào anh em, hôm nay tôi sẽ chia sẻ với anh em chủ đề security. **Bài viết này dành cho ai?** Dành cho các Software Engineer, Backend Developer và Architect đang tìm cách chu?...

Chào anh em, hôm nay tôi sẽ chia sẻ với anh em chủ đề security.

Bài viết này dành cho ai? Dành cho các Software Engineer, Backend Developer và Architect đang tìm cách chuẩn hóa hệ thống xác thực của mình, tránh việc phụ thuộc quá nhiều vào các thư viện "hộp đen" (black-box) và đảm bảo an toàn tuyệt đối trước các đợt tấn công Account Takeover.


Sự An Toàn Ảo Tưởng Của Social Login

Chúng ta thường nghĩ rằng: "Chỉ cần dùng Google/GitHub Login là xong phần bảo mật, Google lo hết rồi". Thực tế, lỗ hổng hiếm khi nằm ở Google, mà nằm ở cách chúng ta tích hợp (integration) hệ thống của mình với Provider.

Trong kiến trúc bảo mật gần đây, chúng tôi quyết định đập bỏ hoàn toàn các thư viện trung gian như Passport.js để tự xây dựng một luồng (flow) OAuth2 tùy chỉnh, dựa trên nguyên tắc Zero-Trust.

Hãy cùng phân tích tại sao.

1. Cạm bẫy Dependency: Tại sao tôi từ bỏ Passport.js?

Passport.js là một thư viện tuyệt vời để bắt đầu, nhưng trong kiến trúc Enterprise, nó đóng gói toàn bộ luồng OAuth thành một "hộp đen". Bạn không kiểm soát được chính xác request nào được gọi, xử lý lỗi ra sao ở tầng Domain, và quan trọng nhất là tăng diện mạo tấn công (Attack Surface) vì phụ thuộc vào một chuỗi các package nhỏ lẻ.

Thay vào đó, chúng tôi sử dụng Axios để tự thực hiện luồng trao đổi Token (Token Exchange):

// https://github.com/paudang/nodejs-social-auth/blob/main/src/infrastructure/auth/socialAuthService.ts
export class GoogleProvider implements ISocialProvider {
 name = 'Google';
 async getProfile(code: string, redirectUri: string): Promise<ISocialProfile> {
 try {
 const params = new URLSearchParams();
 params.append('code', code);
 params.append('client_id', process.env.GOOGLE_CLIENT_ID!);
 params.append('client_secret', process.env.GOOGLE_CLIENT_SECRET!);
 params.append('redirect_uri', redirectUri);
 params.append('grant_type', 'authorization_code');

 // Tự chủ động exchange token thay vì dùng black-box lib
 const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', params.toString(), {
 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
 });

 const { access_token } = tokenResponse.data;
 const profileResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', {
 headers: { Authorization: `Bearer ${access_token}` },
 });

 return {
 id: profileResponse.data.id,
 email: profileResponse.data.email,
 name: profileResponse.data.name,
 };
 } catch (error) {
 // Bắt lỗi và kiểm soát hoàn toàn ở tầng Infrastructure
 throw new Error('Failed to authenticate with Google');
 }
 }
}

2. Vấn đề Blind Linking & Account Takeover (ATO)

Một trong những lỗ hổng chết người của Social Login là Blind Linking (Liên kết tài khoản mù quáng). Nếu một kẻ tấn công tạo một tài khoản GitHub với email của bạn (dù chưa verify), và hệ thống tự động gộp (merge) tài khoản GitHub đó vào user có sẵn dựa trên email -> Kẻ tấn công vừa chiếm được tài khoản của bạn.

Để giải quyết vấn đề này, luồng kiểm tra của chúng ta tách bạch googleIdgithubId, đồng thời vô hiệu hóa (disabled) mật khẩu nếu user được sinh ra từ mạng xã hội:

// https://github.com/paudang/nodejs-social-auth/blob/main/src/usecases/auth/socialLoginUseCase.ts
// 1. Find or create user
let user = await this.userRepository.findByEmail(profile.email);

if (!user) {
 // Tạo user mới, trường Password là null để vô hiệu hóa đăng nhập truyền thống
 user = new User(
 null,
 profile.name,
 profile.email,
 null, // Password = null
 this.provider.name === 'Google' ? profile.id : null,
 this.provider.name === 'GitHub' ? profile.id : null,
 );
 user = await this.userRepository.save(user);
} else {
 // Link social ID một cách có kiểm soát
 let updated = false;
 if (this.provider.name === 'Google' && !user.googleId) {
 user.googleId = profile.id;
 updated = true;
 }
 // ... Update user 
}

3. Đồng bộ hóa với "Nuclear Revoke"

Trong bài viết trước về "The Illusion of Stateless Security", tôi đã đề cập đến Nuclear Revoke - cơ chế vô hiệu hóa phiên bản làm việc diện rộng bằng Redis.

Khi user đăng nhập bằng Google, chúng ta không dùng session của Google để duy trì đăng nhập. Chúng ta lập tức chuyển đổi (exchange) nó thành JWT nội bộ của chúng ta, được bảo vệ bằng Refresh Token Rotation và JTI Tracking:

// https://github.com/paudang/nodejs-social-auth/blob/main/src/interfaces/controllers/auth/authController.ts
// Sau khi xác thực Social thành công
const { user, accessToken, refreshToken } = await useCase.execute(code as string, redirectUri);
const refreshJti = JwtService.decodeToken(refreshToken)?.jti;

// Store refresh token vào Redis List (Nuclear Revoke System)
const cacheKey = `refresh_tokens:${userId}`;
const activeTokens = await cacheService.get<string[]>(cacheKey) || [];
activeTokens.push(refreshJti!);
await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);

// Trả về qua Cookie thay vì body để tránh XSS
res.cookie('accessToken', accessToken, { httpOnly: true, secure: true, sameSite: 'lax' });
res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, sameSite: 'lax' });
res.redirect('/');

4. Luồng tương tác hoàn chỉnh (Sequence Diagram)

Dưới đây là sơ đồ kiến trúc thể hiện toàn bộ quy trình từ lúc User click vào nút Login, quá trình Verify State (chống CSRF), đến khi phát hành Internal JWT:

Note: Quá trình Verify 'state' (chống CSRF) trong sơ đồ trên là kiến trúc lý tưởng. Tính năng sinh mã ngẫu nhiên bằng Cryptography cho state hiện đang được phát triển và sẽ được tự động hóa trong phiên bản cập nhật tiếp theo của tool.

Tổng kết

Nếu bạn muốn trải nghiệm luồng bảo mật hoàn chỉnh này (MVC hoặc Clean Architecture) mà không phải tự tay viết lại, bạn có thể chạy dòng lệnh sau từ dự án open-source của tôi:

npx nodejs-quickstart-structure@latest init -n "my-secure-app" -l "TypeScript" -a "Clean Architecture" -d "PostgreSQL" --db-name "demo" -c "REST APIs" --caching "Redis" --ci-provider "GitHub Actions" --auth JWT --social-auth Google GitHub --no-include-security --advanced-options

Tài nguyên cho Architect

📚 Nguồn: Viblo

Chia sẻ bài viết

Cần tư vấn?

Liên hệ với chúng tôi để được hỗ trợ

Liên hệ ngay

Bài viết liên quan

Simple Jewelry Styles That Never Feel Overdone
07/05/2026

Simple Jewelry Styles That Never Feel Overdone

Jewelry trends continue to change every season, yet many people still return to simple designs that feel comfortable, wearable, and easy to match with daily outfits. Clean lines, balanced settings, an...

Đọc thêm
Xây dựng Quản lý kho hàng (Listing Management) trong hệ thống Bất Động Sản
07/05/2026

Xây dựng Quản lý kho hàng (Listing Management) trong hệ thống Bất Động Sản

Trong ngành Bất động sản (Proptech), việc quản lý kho hàng (Listing Management) không đơn giản là CRUD mà là bài toán về Tính nhất quán dữ liệu và Quản lý trạng...

Đọc thêm
Tôi port game Godot 2D sang Android — đây là những gì làm fps tụt từ 60 xuống 22
07/05/2026

Tôi port game Godot 2D sang Android — đây là những gì làm fps tụt từ 60 xuống 22

![Tôi port game Godot 2D sang Android — fps từ 60 xuống 22 và cách sửa](https://i.imgur.com/EOaajf4.png) Tuần trước tôi port một prototype game 2D từ Godot 4.4 desktop sang Androi...

Đọc thêm

Bắt đầu dự án của bạn

Hãy để Flash Dev đồng hành cùng bạn

Liên hệ ngay