UNPKG

@warriorteam/redai-zalo-sdk

Version:

Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account, ZNS, Consultation Service, Group Messaging, and Social APIs

854 lines (666 loc) 23.4 kB
# RedAI Zalo SDK - Authentication Guide ## Tổng quan RedAI Zalo SDK hỗ trợ đầy đủ các authentication flows của Zalo, bao gồm: - **Official Account (OA) Authentication** - Để truy cập OA APIs (hỗ trợ PKCE) - **Social API Authentication** - Để truy cập thông tin user social (hỗ trợ PKCE) - **Token Management** - Refresh và validate tokens - **PKCE Support** - Security enhancement cho cả OA và Social API --- ## Official Account Authentication ### 1. Tạo Authorization URL #### Cách 1: Basic Authentication (không PKCE) ```typescript import { ZaloSDK } from "@warriorteam/redai-zalo-sdk"; const zalo = new ZaloSDK({ appId: "your-oa-app-id", appSecret: "your-oa-app-secret" }); // Tạo authorization URL cho OA - state sẽ được tự động sinh với prefix 'zalo_oa_' const authResult = zalo.auth.createOAAuthUrl( "https://your-app.com/auth/callback" // redirect_uri ); console.log("Redirect user to:", authResult.url); console.log("Generated state:", authResult.state); // Output: // - url: https://oauth.zaloapp.com/v4/oa/permission?app_id=xxx&redirect_uri=xxx&state=zalo_oa_abc123... // - state: zalo_oa_abc123def456... // Hoặc với custom state const customAuthResult = zalo.auth.createOAAuthUrl( "https://your-app.com/auth/callback", "my-custom-state" ); ``` #### Cách 2: Enhanced Security với PKCE (Khuyến nghị) ##### Option A: Manual PKCE Configuration ```typescript // Bước 1: Tạo PKCE configuration const pkce = zalo.auth.generatePKCE(); console.log("PKCE Config:", { code_verifier: pkce.code_verifier, // Lưu trữ an toàn - cần cho bước exchange token code_challenge: pkce.code_challenge, // Sẽ được gửi trong URL code_challenge_method: pkce.code_challenge_method // "S256" }); // Bước 2: Tạo authorization URL với manual PKCE const authResult = zalo.auth.createOAAuthUrl( "https://your-app.com/auth/callback", "my-secure-state", // optional custom state pkce, // manual PKCE config true // usePkce = true ); console.log("Secure auth URL:", authResult.url); console.log("State to verify:", authResult.state); console.log("PKCE used:", authResult.pkce); // ⚠️ QUAN TRỌNG: Lưu trữ code_verifier và state để sử dụng ở bước exchange token sessionStorage.setItem('pkce_code_verifier', pkce.code_verifier); sessionStorage.setItem('auth_state', authResult.state); ``` ##### Option B: Auto-Generated PKCE (Đơn giản nhất) ```typescript // Tạo authorization URL với auto-generated PKCE const authResult = zalo.auth.createOAAuthUrl( "https://your-app.com/auth/callback", undefined, // state sẽ được auto-generate undefined, // pkce sẽ được auto-generate true // usePkce = true ); console.log("Secure auth URL:", authResult.url); console.log("Auto-generated state:", authResult.state); console.log("Auto-generated PKCE:", authResult.pkce); // ⚠️ QUAN TRỌNG: Lưu trữ auto-generated values sessionStorage.setItem('pkce_code_verifier', authResult.pkce!.code_verifier); sessionStorage.setItem('auth_state', authResult.state); ``` ### 2. Xử lý Callback và Lấy Access Token #### Cách 1: Basic Token Exchange ```typescript // Trong route callback của bạn app.get('/auth/callback', async (req, res) => { const { code, state } = req.query; try { // Lấy access token từ authorization code const tokenResponse = await zalo.auth.getOAAccessToken({ app_id: "your-oa-app-id", app_secret: "your-oa-app-secret", code: code as string, redirect_uri: "https://your-app.com/auth/callback" }); console.log("OA Access Token:", tokenResponse.access_token); console.log("Refresh Token:", tokenResponse.refresh_token); console.log("Expires In:", tokenResponse.expires_in); // seconds // Lưu tokens vào database/session await saveTokens(tokenResponse); res.redirect('/dashboard'); } catch (error) { console.error("Auth error:", error); res.redirect('/auth/error'); } }); ``` #### Cách 2: Secure Token Exchange với PKCE ```typescript app.get('/auth/callback', async (req, res) => { const { code, state } = req.query; try { // Bước 1: Verify state để chống CSRF attack const storedState = sessionStorage.getItem('auth_state'); if (state !== storedState) { throw new Error('State mismatch - possible CSRF attack'); } // Bước 2: Lấy code_verifier đã lưu trữ const codeVerifier = sessionStorage.getItem('pkce_code_verifier'); if (!codeVerifier) { throw new Error('Code verifier not found'); } // Bước 3: Exchange authorization code với PKCE const tokenResponse = await zalo.auth.getOAAccessToken({ app_id: "your-oa-app-id", app_secret: "your-oa-app-secret", code: code as string, redirect_uri: "https://your-app.com/auth/callback", code_verifier: codeVerifier // 🔐 PKCE code verifier }); console.log("Secure OA Access Token:", tokenResponse.access_token); // Bước 4: Xóa temporary data sessionStorage.removeItem('auth_state'); sessionStorage.removeItem('pkce_code_verifier'); // Lưu tokens await saveTokens(tokenResponse); res.redirect('/dashboard'); } catch (error) { console.error("Secure auth error:", error); res.redirect('/auth/error'); } }); ``` ### 3. PKCE Security Benefits PKCE (Proof Key for Code Exchange) cung cấp các lợi ích bảo mật quan trọng: #### 🔐 Tại sao nên sử dụng PKCE? 1. **Chống Authorization Code Interception**: - Ngay cả khi authorization code bị đánh cắp, attacker không thể sử dụng mà không có `code_verifier` 2. **Không cần lưu trữ App Secret ở client**: - PKCE cho phép public clients (mobile apps, SPAs) thực hiện OAuth flow an toàn 3. **Chống CSRF và Replay Attacks**: - Mỗi request có unique `code_verifier` và `code_challenge` #### 🛡️ PKCE Flow Security ``` 1. Client tạo code_verifier (random string) 2. Client tạo code_challenge = SHA256(code_verifier) 3. Client gửi code_challenge trong authorization request 4. Authorization server lưu code_challenge 5. Client nhận authorization code 6. Client gửi code + code_verifier để exchange token 7. Server verify: SHA256(code_verifier) == stored code_challenge 8. Nếu match → trả về access token ``` #### ⚠️ Best Practices - **Luôn sử dụng PKCE** cho production applications - **Lưu trữ code_verifier an toàn** (session, secure storage) - **Verify state parameter** để chống CSRF - **Sử dụng HTTPS** cho tất cả OAuth endpoints - **Set proper expiration** cho stored PKCE data ### 5. Token Response Structure ```typescript interface AccessToken { access_token: string; // Token để gọi API refresh_token: string; // Token để refresh expires_in: number; // Thời gian sống (seconds) token_type: "Bearer"; // Loại token scope: string; // Quyền được cấp } ``` ### 6. Sử dụng Access Token ```typescript // Lấy thông tin OA const oaInfo = await zalo.getOAInfo(tokenResponse.access_token); console.log("OA Name:", oaInfo.name); console.log("Followers:", oaInfo.num_follower); // Gửi tin nhắn consultation await zalo.sendConsultationText( tokenResponse.access_token, "user-zalo-id", "Xin chào! Cảm ơn bạn đã quan tâm đến dịch vụ của chúng tôi." ); ``` --- ## Social API Authentication ### 1. Tạo Authorization URL (Basic) ```typescript const zalo = new ZaloSDK({ appId: "your-social-app-id", appSecret: "your-social-app-secret" }); // Tạo authorization URL cho Social API const authUrl = zalo.createSocialAuthUrl( "https://your-app.com/social/callback", "optional-state" ); console.log("Redirect user to:", authUrl); ``` ### 2. Tạo Authorization URL với PKCE (Khuyến nghị) ```typescript // Tạo PKCE parameters cho bảo mật cao hơn const pkce = zalo.generatePKCE(); console.log("Code Verifier:", pkce.code_verifier); console.log("Code Challenge:", pkce.code_challenge); // Lưu code_verifier vào session/state req.session.codeVerifier = pkce.code_verifier; const authUrl = zalo.createSocialAuthUrl( "https://your-app.com/social/callback", "social-login" ); ``` ### 3. PKCE Generation ```typescript interface PKCEConfig { code_verifier: string; // Random string 43-128 chars code_challenge: string; // base64url(sha256(code_verifier)) code_challenge_method: "S256"; } // SDK tự động tạo PKCE theo chuẩn RFC 7636 const pkce = zalo.generatePKCE(); ``` ### 4. Xử lý Social Callback ```typescript app.get('/social/callback', async (req, res) => { const { code, state } = req.query; const codeVerifier = req.session.codeVerifier; // Lấy từ session try { const tokenResponse = await zalo.getSocialAccessToken( code as string, "https://your-app.com/social/callback", codeVerifier // Bắt buộc nếu dùng PKCE ); // Lấy thông tin user const userInfo = await zalo.getSocialUserInfo( tokenResponse.access_token, "id,name,picture,birthday,gender" // fields cần lấy ); console.log("User Info:", userInfo); // Lưu vào database await createOrUpdateUser(userInfo, tokenResponse); res.redirect('/profile'); } catch (error) { console.error("Social auth error:", error); res.redirect('/login?error=auth_failed'); } }); ``` ### 5. Social User Info Response ```typescript interface SocialUserInfo { id: string; // Zalo user ID name: string; // Tên hiển thị picture?: { data: { url: string; // Avatar URL } }; birthday?: string; // Ngày sinh (YYYY-MM-DD) gender?: number; // 1: Nam, 2: Nữ locale?: string; // Locale // Các fields khác tùy theo quyền được cấp } ``` --- ## Token Management ### 1. Refresh OA Access Token ```typescript async function refreshOAToken(refreshToken: string): Promise<AccessToken> { try { const newTokens = await zalo.refreshOAAccessToken(refreshToken); // Lưu tokens mới await updateTokens(newTokens); return newTokens; } catch (error) { console.error("Failed to refresh OA token:", error); // Redirect to re-authentication throw new Error("Re-authentication required"); } } ``` ### 2. Refresh Social Access Token ```typescript async function refreshSocialToken(refreshToken: string): Promise<AccessToken> { try { const newTokens = await zalo.refreshSocialAccessToken(refreshToken); return newTokens; } catch (error) { console.error("Failed to refresh social token:", error); throw error; } } ``` ### 3. Auto Token Refresh Middleware ```typescript // Express middleware để auto refresh tokens export const autoRefreshToken = async (req: Request, res: Response, next: NextFunction) => { const { accessToken, refreshToken, expiresAt } = req.user; // Kiểm tra token sắp hết hạn (trước 5 phút) const willExpireSoon = Date.now() > (expiresAt - 5 * 60 * 1000); if (willExpireSoon && refreshToken) { try { const tokenType = req.path.includes('/oa/') ? 'oa' : 'social'; const newTokens = tokenType === 'oa' ? await zalo.refreshOAAccessToken(refreshToken) : await zalo.refreshSocialAccessToken(refreshToken); // Cập nhật user với tokens mới req.user.accessToken = newTokens.access_token; req.user.refreshToken = newTokens.refresh_token; req.user.expiresAt = Date.now() + (newTokens.expires_in * 1000); // Lưu vào database await updateUserTokens(req.user.id, newTokens); console.log("Token refreshed successfully"); } catch (error) { console.error("Auto refresh failed:", error); return res.status(401).json({ error: "Authentication expired" }); } } next(); }; ``` --- ## Token Validation ### 1. Validate Access Token ```typescript // Validate OA token const isValidOA = await zalo.validateAccessToken(accessToken, 'oa'); console.log("OA Token valid:", isValidOA); // Validate Social token const isValidSocial = await zalo.validateAccessToken(accessToken, 'social'); console.log("Social Token valid:", isValidSocial); ``` ### 2. Advanced Token Validation ```typescript // Sử dụng service trực tiếp để có thêm thông tin const validation = await zalo.auth.validateAccessToken(accessToken); interface TokenValidation { valid: boolean; expires_in?: number; // Thời gian còn lại (seconds) scope?: string; // Quyền hiện tại app_id?: string; // App ID của token user_id?: string; // User ID (nếu có) } ``` --- ## Security Best Practices ### 1. State Parameter Luôn sử dụng `state` parameter để chống CSRF: ```typescript // Tạo random state const state = crypto.randomBytes(16).toString('hex'); req.session.authState = state; const authUrl = zalo.createOAAuthUrl(redirectUri, state); // Trong callback, verify state if (req.query.state !== req.session.authState) { throw new Error("Invalid state parameter"); } ``` ### 2. PKCE cho Social API Luôn sử dụng PKCE cho Social API: ```typescript // ✅ Đúng - với PKCE const pkce = zalo.generatePKCE(); req.session.codeVerifier = pkce.code_verifier; // ❌ Sai - không dùng PKCE const authUrl = zalo.createSocialAuthUrl(redirectUri); ``` ### 3. Token Storage ```typescript // ✅ Đúng - mã hóa tokens khi lưu const encryptedToken = encrypt(accessToken); await db.users.update(userId, { access_token: encryptedToken, refresh_token: encrypt(refreshToken) }); // ❌ Sai - lưu plain text await db.users.update(userId, { access_token: accessToken // Không an toàn }); ``` ### 4. Token Scope Validation ```typescript // Kiểm tra token có đúng scope không async function requireScope(requiredScope: string) { const validation = await zalo.auth.validateAccessToken(accessToken); if (!validation.scope?.includes(requiredScope)) { throw new Error(`Missing required scope: ${requiredScope}`); } } // Usage await requireScope('oa.message.send'); ``` --- ## Error Handling ### 1. Common Auth Errors ```typescript try { const tokens = await zalo.getOAAccessToken(code, redirectUri); } catch (error) { if (error.code === -201) { // Invalid parameters (code expired, wrong redirect_uri, etc.) console.error("Invalid auth parameters:", error.message); } else if (error.code === -216) { // Invalid app credentials console.error("Invalid app_id or app_secret"); } else { console.error("Unexpected auth error:", error); } } ``` ### 2. Token Refresh Errors ```typescript async function handleTokenRefresh(refreshToken: string) { try { return await zalo.refreshOAAccessToken(refreshToken); } catch (error) { if (error.code === -216) { // Refresh token expired/invalid console.log("Refresh token expired, require re-authentication"); redirectToLogin(); } else { console.error("Refresh failed:", error); throw error; } } } ``` --- ## Complete Authentication Flow Examples ### 1. OA Authentication với Express ```typescript import express from 'express'; import { ZaloSDK } from '@warriorteam/redai-zalo-sdk'; const app = express(); const zalo = new ZaloSDK({ appId: process.env.ZALO_OA_APP_ID!, appSecret: process.env.ZALO_OA_APP_SECRET!, debug: process.env.NODE_ENV === 'development' }); // Route bắt đầu auth app.get('/auth/oa', (req, res) => { const state = generateRandomState(); req.session.authState = state; const authUrl = zalo.createOAAuthUrl( `${process.env.BASE_URL}/auth/oa/callback`, state ); res.redirect(authUrl); }); // Callback handler app.get('/auth/oa/callback', async (req, res) => { try { const { code, state, error } = req.query; if (error) { return res.redirect(`/auth/error?reason=${error}`); } if (state !== req.session.authState) { return res.status(400).json({ error: 'Invalid state' }); } const tokens = await zalo.getOAAccessToken( code as string, `${process.env.BASE_URL}/auth/oa/callback` ); // Lấy thông tin OA const oaInfo = await zalo.getOAInfo(tokens.access_token); // Lưu vào database const oaAccount = await OAAccount.create({ oa_id: oaInfo.oa_id, name: oaInfo.name, access_token: encrypt(tokens.access_token), refresh_token: encrypt(tokens.refresh_token), expires_at: new Date(Date.now() + tokens.expires_in * 1000), scope: tokens.scope }); req.session.oaId = oaAccount.id; res.redirect('/oa/dashboard'); } catch (error) { console.error('OA Auth error:', error); res.redirect('/auth/error'); } }); ``` ### 2. Social Authentication với Next.js ```typescript // pages/api/auth/social/login.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { ZaloSDK } from '@warriorteam/redai-zalo-sdk'; const zalo = new ZaloSDK({ appId: process.env.ZALO_SOCIAL_APP_ID!, appSecret: process.env.ZALO_SOCIAL_APP_SECRET! }); export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'GET') { return res.status(405).json({ error: 'Method not allowed' }); } // Generate PKCE const pkce = zalo.generatePKCE(); const state = generateRandomState(); // Lưu vào session/cookie (hoặc Redis) res.setHeader('Set-Cookie', [ `pkce_verifier=${pkce.code_verifier}; HttpOnly; Secure; SameSite=Strict`, `auth_state=${state}; HttpOnly; Secure; SameSite=Strict` ]); const authUrl = zalo.createSocialAuthUrl( `${process.env.NEXTAUTH_URL}/api/auth/social/callback`, state ); res.redirect(authUrl); } // pages/api/auth/social/callback.ts export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const { code, state } = req.query; const cookies = parseCookies(req.headers.cookie || ''); if (state !== cookies.auth_state) { throw new Error('Invalid state'); } const tokens = await zalo.getSocialAccessToken( code as string, `${process.env.NEXTAUTH_URL}/api/auth/social/callback`, cookies.pkce_verifier ); const userInfo = await zalo.getSocialUserInfo( tokens.access_token, 'id,name,picture,birthday' ); // Tạo hoặc cập nhật user const user = await User.upsert({ zalo_id: userInfo.id, name: userInfo.name, avatar: userInfo.picture?.data.url, birthday: userInfo.birthday }); // Lưu tokens await UserToken.create({ user_id: user.id, access_token: encrypt(tokens.access_token), refresh_token: encrypt(tokens.refresh_token), expires_at: new Date(Date.now() + tokens.expires_in * 1000), token_type: 'social' }); // Tạo session const sessionToken = jwt.sign( { userId: user.id, zaloId: userInfo.id }, process.env.JWT_SECRET!, { expiresIn: '7d' } ); res.setHeader('Set-Cookie', `session=${sessionToken}; HttpOnly; Secure`); res.redirect('/profile'); } catch (error) { console.error('Social auth error:', error); res.redirect('/login?error=auth_failed'); } } ``` --- ## Environment Configuration ```typescript // .env file ZALO_OA_APP_ID=your_oa_app_id ZALO_OA_APP_SECRET=your_oa_app_secret ZALO_SOCIAL_APP_ID=your_social_app_id ZALO_SOCIAL_APP_SECRET=your_social_app_secret // config.ts export const zaloConfig = { oa: { appId: process.env.ZALO_OA_APP_ID!, appSecret: process.env.ZALO_OA_APP_SECRET!, }, social: { appId: process.env.ZALO_SOCIAL_APP_ID!, appSecret: process.env.ZALO_SOCIAL_APP_SECRET!, } }; // Khởi tạo SDK instances export const zaloOA = new ZaloSDK(zaloConfig.oa); export const zaloSocial = new ZaloSDK(zaloConfig.social); ``` --- ## Testing Authentication ### 1. Unit Tests ```typescript // auth.test.ts import { ZaloSDK } from '@warriorteam/redai-zalo-sdk'; describe('Authentication', () => { const zalo = new ZaloSDK({ appId: 'test_app_id', appSecret: 'test_app_secret' }); it('should create OA auth URL', () => { const url = zalo.createOAAuthUrl('http://localhost:3000/callback', 'test-state'); expect(url).toContain('oauth.zaloapp.com/v4/oa/permission'); expect(url).toContain('app_id=test_app_id'); expect(url).toContain('state=test-state'); }); it('should generate valid PKCE', () => { const pkce = zalo.generatePKCE(); expect(pkce.code_verifier).toHaveLength(128); expect(pkce.code_challenge_method).toBe('S256'); expect(pkce.code_challenge).toBeDefined(); }); }); ``` ### 2. Integration Tests ```typescript // auth.integration.test.ts describe('Authentication Integration', () => { it('should complete OA auth flow', async () => { // Sử dụng test credentials const testCode = 'test_authorization_code'; const testRedirectUri = 'http://localhost:3000/callback'; const tokens = await zalo.getOAAccessToken(testCode, testRedirectUri); expect(tokens.access_token).toBeDefined(); expect(tokens.refresh_token).toBeDefined(); expect(tokens.expires_in).toBeGreaterThan(0); }); }); ``` --- ## Troubleshooting ### 1. Common Issues **Q: "Invalid redirect_uri" error** ``` A: Đảm bảo redirect_uri trong callback chính xác giống với lúc tạo auth URL và đã được đăng ký trong Zalo Developer Console ``` **Q: "Invalid code" error** ``` A: Authorization code chỉ dùng được 1 lần và có thời gian sống ngắn (~10 phút) Đảm bảo xử lý callback ngay sau khi user authorize ``` **Q: "Invalid app_id or app_secret"** ``` A: Kiểm tra credentials trong Zalo Developer Console Đảm bảo dùng đúng app_id/app_secret cho môi trường (dev/prod) ``` ### 2. Debug Authentication ```typescript // Enable debug logging const zalo = new ZaloSDK({ appId: 'your_app_id', appSecret: 'your_app_secret', debug: true // Bật debug logs }); // Manual token validation async function debugToken(accessToken: string) { try { const validation = await zalo.auth.validateAccessToken(accessToken); console.log('Token validation:', validation); if (validation.valid) { console.log(`Token expires in: ${validation.expires_in} seconds`); console.log(`Token scope: ${validation.scope}`); } } catch (error) { console.error('Token validation failed:', error); } } ``` --- ## Next Steps Sau khi hoàn thành authentication: 1. **[ZNS Service](./ZNS_SERVICE.md)** - Gửi notification messages 2. **[Message Services](./MESSAGE_SERVICES.md)** - Gửi các loại tin nhắn khác nhau 3. **[User Management](./USER_MANAGEMENT.md)** - Quản lý user profiles 4. **[Group Management](./GROUP_MANAGEMENT.md)** - Quản lý Zalo groups 5. **[Webhook Integration](./WEBHOOK_EVENTS.md)** - Xử lý real-time events Tham khảo **[API Reference](./API_REFERENCE.md)** để biết chi tiết về tất cả methods có sẵn.