@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
Markdown
# 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.