UNPKG

@warriorteam/redai-zalo-sdk

Version:

Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account v3.0, ZNS with Full Type Safety, Consultation Service, Broadcast Service, Group Messaging with List APIs, Social APIs, Enhanced Article Management, Promotion Service v3.0 with Multip

854 lines (666 loc) 24.3 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.