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