@warriorteam/redai-zalo-sdk 1.12.0 → 1.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,854 +1,853 @@
1
- # RedAI Zalo SDK - Authentication Guide
2
-
3
- ## Tổng quan
4
-
5
- RedAI Zalo SDK hỗ trợ đầy đủ các authentication flows của Zalo, bao gồm:
6
-
7
- - **Official Account (OA) Authentication** - Để truy cập OA APIs (hỗ trợ PKCE)
8
- - **Social API Authentication** - Để truy cập thông tin user social (hỗ trợ PKCE)
9
- - **Token Management** - Refresh và validate tokens
10
- - **PKCE Support** - Security enhancement cho cả OA và Social API
11
-
12
- ---
13
-
14
- ## Official Account Authentication
15
-
16
- ### 1. Tạo Authorization URL
17
-
18
- #### Cách 1: Basic Authentication (không PKCE)
19
-
20
- ```typescript
21
- import { ZaloSDK } from "@warriorteam/redai-zalo-sdk";
22
-
23
- const zalo = new ZaloSDK({
24
- appId: "your-oa-app-id",
25
- appSecret: "your-oa-app-secret"
26
- });
27
-
28
- // Tạo authorization URL cho OA - state sẽ được tự động sinh với prefix 'zalo_oa_'
29
- const authResult = zalo.auth.createOAAuthUrl(
30
- "https://your-app.com/auth/callback" // redirect_uri
31
- );
32
-
33
- console.log("Redirect user to:", authResult.url);
34
- console.log("Generated state:", authResult.state);
35
- // Output:
36
- // - url: https://oauth.zaloapp.com/v4/oa/permission?app_id=xxx&redirect_uri=xxx&state=zalo_oa_abc123...
37
- // - state: zalo_oa_abc123def456...
38
-
39
- // Hoặc với custom state
40
- const customAuthResult = zalo.auth.createOAAuthUrl(
41
- "https://your-app.com/auth/callback",
42
- "my-custom-state"
43
- );
44
- ```
45
-
46
- #### Cách 2: Enhanced Security với PKCE (Khuyến nghị)
47
-
48
- ##### Option A: Manual PKCE Configuration
49
-
50
- ```typescript
51
- // Bước 1: Tạo PKCE configuration
52
- const pkce = zalo.auth.generatePKCE();
53
- console.log("PKCE Config:", {
54
- code_verifier: pkce.code_verifier, // Lưu trữ an toàn - cần cho bước exchange token
55
- code_challenge: pkce.code_challenge, // Sẽ được gửi trong URL
56
- code_challenge_method: pkce.code_challenge_method // "S256"
57
- });
58
-
59
- // Bước 2: Tạo authorization URL với manual PKCE
60
- const authResult = zalo.auth.createOAAuthUrl(
61
- "https://your-app.com/auth/callback",
62
- "my-secure-state", // optional custom state
63
- pkce, // manual PKCE config
64
- true // usePkce = true
65
- );
66
-
67
- console.log("Secure auth URL:", authResult.url);
68
- console.log("State to verify:", authResult.state);
69
- console.log("PKCE used:", authResult.pkce);
70
-
71
- // ⚠️ QUAN TRỌNG: Lưu trữ code_verifier và state để sử dụng ở bước exchange token
72
- sessionStorage.setItem('pkce_code_verifier', pkce.code_verifier);
73
- sessionStorage.setItem('auth_state', authResult.state);
74
- ```
75
-
76
- ##### Option B: Auto-Generated PKCE (Đơn giản nhất)
77
-
78
- ```typescript
79
- // Tạo authorization URL với auto-generated PKCE
80
- const authResult = zalo.auth.createOAAuthUrl(
81
- "https://your-app.com/auth/callback",
82
- undefined, // state sẽ được auto-generate
83
- undefined, // pkce sẽ được auto-generate
84
- true // usePkce = true
85
- );
86
-
87
- console.log("Secure auth URL:", authResult.url);
88
- console.log("Auto-generated state:", authResult.state);
89
- console.log("Auto-generated PKCE:", authResult.pkce);
90
-
91
- // ⚠️ QUAN TRỌNG: Lưu trữ auto-generated values
92
- sessionStorage.setItem('pkce_code_verifier', authResult.pkce!.code_verifier);
93
- sessionStorage.setItem('auth_state', authResult.state);
94
- ```
95
-
96
- ### 2. Xử lý Callback và Lấy Access Token
97
-
98
- #### Cách 1: Basic Token Exchange
99
-
100
- ```typescript
101
- // Trong route callback của bạn
102
- app.get('/auth/callback', async (req, res) => {
103
- const { code, state } = req.query;
104
-
105
- try {
106
- // Lấy access token từ authorization code
107
- const tokenResponse = await zalo.auth.getOAAccessToken({
108
- app_id: "your-oa-app-id",
109
- app_secret: "your-oa-app-secret",
110
- code: code as string,
111
- redirect_uri: "https://your-app.com/auth/callback"
112
- });
113
-
114
- console.log("OA Access Token:", tokenResponse.access_token);
115
- console.log("Refresh Token:", tokenResponse.refresh_token);
116
- console.log("Expires In:", tokenResponse.expires_in); // seconds
117
-
118
- // Lưu tokens vào database/session
119
- await saveTokens(tokenResponse);
120
-
121
- res.redirect('/dashboard');
122
- } catch (error) {
123
- console.error("Auth error:", error);
124
- res.redirect('/auth/error');
125
- }
126
- });
127
- ```
128
-
129
- #### Cách 2: Secure Token Exchange với PKCE
130
-
131
- ```typescript
132
- app.get('/auth/callback', async (req, res) => {
133
- const { code, state } = req.query;
134
-
135
- try {
136
- // Bước 1: Verify state để chống CSRF attack
137
- const storedState = sessionStorage.getItem('auth_state');
138
- if (state !== storedState) {
139
- throw new Error('State mismatch - possible CSRF attack');
140
- }
141
-
142
- // Bước 2: Lấy code_verifier đã lưu trữ
143
- const codeVerifier = sessionStorage.getItem('pkce_code_verifier');
144
- if (!codeVerifier) {
145
- throw new Error('Code verifier not found');
146
- }
147
-
148
- // Bước 3: Exchange authorization code với PKCE
149
- const tokenResponse = await zalo.auth.getOAAccessToken({
150
- app_id: "your-oa-app-id",
151
- app_secret: "your-oa-app-secret",
152
- code: code as string,
153
- redirect_uri: "https://your-app.com/auth/callback",
154
- code_verifier: codeVerifier // 🔐 PKCE code verifier
155
- });
156
-
157
- console.log("Secure OA Access Token:", tokenResponse.access_token);
158
-
159
- // Bước 4: Xóa temporary data
160
- sessionStorage.removeItem('auth_state');
161
- sessionStorage.removeItem('pkce_code_verifier');
162
-
163
- // Lưu tokens
164
- await saveTokens(tokenResponse);
165
-
166
- res.redirect('/dashboard');
167
- } catch (error) {
168
- console.error("Secure auth error:", error);
169
- res.redirect('/auth/error');
170
- }
171
- });
172
- ```
173
-
174
- ### 3. PKCE Security Benefits
175
-
176
- PKCE (Proof Key for Code Exchange) cung cấp các lợi ích bảo mật quan trọng:
177
-
178
- #### 🔐 Tại sao nên sử dụng PKCE?
179
-
180
- 1. **Chống Authorization Code Interception**:
181
- - Ngay cả khi authorization code bị đánh cắp, attacker không thể sử dụng mà không có `code_verifier`
182
-
183
- 2. **Không cần lưu trữ App Secret client**:
184
- - PKCE cho phép public clients (mobile apps, SPAs) thực hiện OAuth flow an toàn
185
-
186
- 3. **Chống CSRFReplay Attacks**:
187
- - Mỗi request có unique `code_verifier` và `code_challenge`
188
-
189
- #### 🛡️ PKCE Flow Security
190
-
191
- ```
192
- 1. Client tạo code_verifier (random string)
193
- 2. Client tạo code_challenge = SHA256(code_verifier)
194
- 3. Client gửi code_challenge trong authorization request
195
- 4. Authorization server lưu code_challenge
196
- 5. Client nhận authorization code
197
- 6. Client gửi code + code_verifier để exchange token
198
- 7. Server verify: SHA256(code_verifier) == stored code_challenge
199
- 8. Nếu match → trả về access token
200
- ```
201
-
202
- #### ⚠️ Best Practices
203
-
204
- - **Luôn sử dụng PKCE** cho production applications
205
- - **Lưu trữ code_verifier an toàn** (session, secure storage)
206
- - **Verify state parameter** để chống CSRF
207
- - **Sử dụng HTTPS** cho tất cả OAuth endpoints
208
- - **Set proper expiration** cho stored PKCE data
209
-
210
- ### 5. Token Response Structure
211
-
212
- ```typescript
213
- interface AccessToken {
214
- access_token: string; // Token để gọi API
215
- refresh_token: string; // Token để refresh
216
- expires_in: number; // Thời gian sống (seconds)
217
- token_type: "Bearer"; // Loại token
218
- scope: string; // Quyền được cấp
219
- }
220
- ```
221
-
222
- ### 6. Sử dụng Access Token
223
-
224
- ```typescript
225
- // Lấy thông tin OA
226
- const oaInfo = await zalo.getOAInfo(tokenResponse.access_token);
227
- console.log("OA Name:", oaInfo.name);
228
- console.log("Followers:", oaInfo.num_follower);
229
-
230
- // Gửi tin nhắn consultation
231
- await zalo.sendConsultationText(
232
- tokenResponse.access_token,
233
- "user-zalo-id",
234
- "Xin chào! Cảm ơn bạn đã quan tâm đến dịch vụ của chúng tôi."
235
- );
236
- ```
237
-
238
- ---
239
-
240
- ## Social API Authentication
241
-
242
- ### 1. Tạo Authorization URL (Basic)
243
-
244
- ```typescript
245
- const zalo = new ZaloSDK({
246
- appId: "your-social-app-id",
247
- appSecret: "your-social-app-secret"
248
- });
249
-
250
- // Tạo authorization URL cho Social API
251
- const authUrl = zalo.createSocialAuthUrl(
252
- "https://your-app.com/social/callback",
253
- "optional-state"
254
- );
255
-
256
- console.log("Redirect user to:", authUrl);
257
- ```
258
-
259
- ### 2. Tạo Authorization URL với PKCE (Khuyến nghị)
260
-
261
- ```typescript
262
- // Tạo PKCE parameters cho bảo mật cao hơn
263
- const pkce = zalo.generatePKCE();
264
- console.log("Code Verifier:", pkce.code_verifier);
265
- console.log("Code Challenge:", pkce.code_challenge);
266
-
267
- // Lưu code_verifier vào session/state
268
- req.session.codeVerifier = pkce.code_verifier;
269
-
270
- const authUrl = zalo.createSocialAuthUrl(
271
- "https://your-app.com/social/callback",
272
- "social-login"
273
- );
274
- ```
275
-
276
- ### 3. PKCE Generation
277
-
278
- ```typescript
279
- interface PKCEConfig {
280
- code_verifier: string; // Random string 43-128 chars
281
- code_challenge: string; // base64url(sha256(code_verifier))
282
- code_challenge_method: "S256";
283
- }
284
-
285
- // SDK tự động tạo PKCE theo chuẩn RFC 7636
286
- const pkce = zalo.generatePKCE();
287
- ```
288
-
289
- ### 4. Xử lý Social Callback
290
-
291
- ```typescript
292
- app.get('/social/callback', async (req, res) => {
293
- const { code, state } = req.query;
294
- const codeVerifier = req.session.codeVerifier; // Lấy từ session
295
-
296
- try {
297
- const tokenResponse = await zalo.getSocialAccessToken(
298
- code as string,
299
- "https://your-app.com/social/callback",
300
- codeVerifier // Bắt buộc nếu dùng PKCE
301
- );
302
-
303
- // Lấy thông tin user
304
- const userInfo = await zalo.getSocialUserInfo(
305
- tokenResponse.access_token,
306
- "id,name,picture,birthday,gender" // fields cần lấy
307
- );
308
-
309
- console.log("User Info:", userInfo);
310
-
311
- // Lưu vào database
312
- await createOrUpdateUser(userInfo, tokenResponse);
313
-
314
- res.redirect('/profile');
315
- } catch (error) {
316
- console.error("Social auth error:", error);
317
- res.redirect('/login?error=auth_failed');
318
- }
319
- });
320
- ```
321
-
322
- ### 5. Social User Info Response
323
-
324
- ```typescript
325
- interface SocialUserInfo {
326
- id: string; // Zalo user ID
327
- name: string; // Tên hiển thị
328
- picture?: {
329
- data: {
330
- url: string; // Avatar URL
331
- }
332
- };
333
- birthday?: string; // Ngày sinh (YYYY-MM-DD)
334
- gender?: number; // 1: Nam, 2: Nữ
335
- locale?: string; // Locale
336
- // Các fields khác tùy theo quyền được cấp
337
- }
338
- ```
339
-
340
- ---
341
-
342
- ## Token Management
343
-
344
- ### 1. Refresh OA Access Token
345
-
346
- ```typescript
347
- async function refreshOAToken(refreshToken: string): Promise<AccessToken> {
348
- try {
349
- const newTokens = await zalo.refreshOAAccessToken(refreshToken);
350
-
351
- // Lưu tokens mới
352
- await updateTokens(newTokens);
353
-
354
- return newTokens;
355
- } catch (error) {
356
- console.error("Failed to refresh OA token:", error);
357
- // Redirect to re-authentication
358
- throw new Error("Re-authentication required");
359
- }
360
- }
361
- ```
362
-
363
- ### 2. Refresh Social Access Token
364
-
365
- ```typescript
366
- async function refreshSocialToken(refreshToken: string): Promise<AccessToken> {
367
- try {
368
- const newTokens = await zalo.refreshSocialAccessToken(refreshToken);
369
- return newTokens;
370
- } catch (error) {
371
- console.error("Failed to refresh social token:", error);
372
- throw error;
373
- }
374
- }
375
- ```
376
-
377
- ### 3. Auto Token Refresh Middleware
378
-
379
- ```typescript
380
- // Express middleware để auto refresh tokens
381
- export const autoRefreshToken = async (req: Request, res: Response, next: NextFunction) => {
382
- const { accessToken, refreshToken, expiresAt } = req.user;
383
-
384
- // Kiểm tra token sắp hết hạn (trước 5 phút)
385
- const willExpireSoon = Date.now() > (expiresAt - 5 * 60 * 1000);
386
-
387
- if (willExpireSoon && refreshToken) {
388
- try {
389
- const tokenType = req.path.includes('/oa/') ? 'oa' : 'social';
390
-
391
- const newTokens = tokenType === 'oa'
392
- ? await zalo.refreshOAAccessToken(refreshToken)
393
- : await zalo.refreshSocialAccessToken(refreshToken);
394
-
395
- // Cập nhật user với tokens mới
396
- req.user.accessToken = newTokens.access_token;
397
- req.user.refreshToken = newTokens.refresh_token;
398
- req.user.expiresAt = Date.now() + (newTokens.expires_in * 1000);
399
-
400
- // Lưu vào database
401
- await updateUserTokens(req.user.id, newTokens);
402
-
403
- console.log("Token refreshed successfully");
404
- } catch (error) {
405
- console.error("Auto refresh failed:", error);
406
- return res.status(401).json({ error: "Authentication expired" });
407
- }
408
- }
409
-
410
- next();
411
- };
412
- ```
413
-
414
- ---
415
-
416
- ## Token Validation
417
-
418
- ### 1. Validate Access Token
419
-
420
- ```typescript
421
- // Validate OA token
422
- const isValidOA = await zalo.validateAccessToken(accessToken, 'oa');
423
- console.log("OA Token valid:", isValidOA);
424
-
425
- // Validate Social token
426
- const isValidSocial = await zalo.validateAccessToken(accessToken, 'social');
427
- console.log("Social Token valid:", isValidSocial);
428
- ```
429
-
430
- ### 2. Advanced Token Validation
431
-
432
- ```typescript
433
- // Sử dụng service trực tiếp để có thêm thông tin
434
- const validation = await zalo.auth.validateAccessToken(accessToken);
435
-
436
- interface TokenValidation {
437
- valid: boolean;
438
- expires_in?: number; // Thời gian còn lại (seconds)
439
- scope?: string; // Quyền hiện tại
440
- app_id?: string; // App ID của token
441
- user_id?: string; // User ID (nếu có)
442
- }
443
- ```
444
-
445
- ---
446
-
447
- ## Security Best Practices
448
-
449
- ### 1. State Parameter
450
-
451
- Luôn sử dụng `state` parameter để chống CSRF:
452
-
453
- ```typescript
454
- // Tạo random state
455
- const state = crypto.randomBytes(16).toString('hex');
456
- req.session.authState = state;
457
-
458
- const authUrl = zalo.createOAAuthUrl(redirectUri, state);
459
-
460
- // Trong callback, verify state
461
- if (req.query.state !== req.session.authState) {
462
- throw new Error("Invalid state parameter");
463
- }
464
- ```
465
-
466
- ### 2. PKCE cho Social API
467
-
468
- Luôn sử dụng PKCE cho Social API:
469
-
470
- ```typescript
471
- // Đúng - với PKCE
472
- const pkce = zalo.generatePKCE();
473
- req.session.codeVerifier = pkce.code_verifier;
474
-
475
- // Sai - không dùng PKCE
476
- const authUrl = zalo.createSocialAuthUrl(redirectUri);
477
- ```
478
-
479
- ### 3. Token Storage
480
-
481
- ```typescript
482
- // Đúng - mã hóa tokens khi lưu
483
- const encryptedToken = encrypt(accessToken);
484
- await db.users.update(userId, {
485
- access_token: encryptedToken,
486
- refresh_token: encrypt(refreshToken)
487
- });
488
-
489
- // Sai - lưu plain text
490
- await db.users.update(userId, {
491
- access_token: accessToken // Không an toàn
492
- });
493
- ```
494
-
495
- ### 4. Token Scope Validation
496
-
497
- ```typescript
498
- // Kiểm tra token có đúng scope không
499
- async function requireScope(requiredScope: string) {
500
- const validation = await zalo.auth.validateAccessToken(accessToken);
501
-
502
- if (!validation.scope?.includes(requiredScope)) {
503
- throw new Error(`Missing required scope: ${requiredScope}`);
504
- }
505
- }
506
-
507
- // Usage
508
- await requireScope('oa.message.send');
509
- ```
510
-
511
- ---
512
-
513
- ## Error Handling
514
-
515
- ### 1. Common Auth Errors
516
-
517
- ```typescript
518
- try {
519
- const tokens = await zalo.getOAAccessToken(code, redirectUri);
520
- } catch (error) {
521
- if (error.code === -201) {
522
- // Invalid parameters (code expired, wrong redirect_uri, etc.)
523
- console.error("Invalid auth parameters:", error.message);
524
- } else if (error.code === -216) {
525
- // Invalid app credentials
526
- console.error("Invalid app_id or app_secret");
527
- } else {
528
- console.error("Unexpected auth error:", error);
529
- }
530
- }
531
- ```
532
-
533
- ### 2. Token Refresh Errors
534
-
535
- ```typescript
536
- async function handleTokenRefresh(refreshToken: string) {
537
- try {
538
- return await zalo.refreshOAAccessToken(refreshToken);
539
- } catch (error) {
540
- if (error.code === -216) {
541
- // Refresh token expired/invalid
542
- console.log("Refresh token expired, require re-authentication");
543
- redirectToLogin();
544
- } else {
545
- console.error("Refresh failed:", error);
546
- throw error;
547
- }
548
- }
549
- }
550
- ```
551
-
552
- ---
553
-
554
- ## Complete Authentication Flow Examples
555
-
556
- ### 1. OA Authentication với Express
557
-
558
- ```typescript
559
- import express from 'express';
560
- import { ZaloSDK } from '@warriorteam/redai-zalo-sdk';
561
-
562
- const app = express();
563
- const zalo = new ZaloSDK({
564
- appId: process.env.ZALO_OA_APP_ID!,
565
- appSecret: process.env.ZALO_OA_APP_SECRET!,
566
- debug: process.env.NODE_ENV === 'development'
567
- });
568
-
569
- // Route bắt đầu auth
570
- app.get('/auth/oa', (req, res) => {
571
- const state = generateRandomState();
572
- req.session.authState = state;
573
-
574
- const authUrl = zalo.createOAAuthUrl(
575
- `${process.env.BASE_URL}/auth/oa/callback`,
576
- state
577
- );
578
-
579
- res.redirect(authUrl);
580
- });
581
-
582
- // Callback handler
583
- app.get('/auth/oa/callback', async (req, res) => {
584
- try {
585
- const { code, state, error } = req.query;
586
-
587
- if (error) {
588
- return res.redirect(`/auth/error?reason=${error}`);
589
- }
590
-
591
- if (state !== req.session.authState) {
592
- return res.status(400).json({ error: 'Invalid state' });
593
- }
594
-
595
- const tokens = await zalo.getOAAccessToken(
596
- code as string,
597
- `${process.env.BASE_URL}/auth/oa/callback`
598
- );
599
-
600
- // Lấy thông tin OA
601
- const oaInfo = await zalo.getOAInfo(tokens.access_token);
602
-
603
- // Lưu vào database
604
- const oaAccount = await OAAccount.create({
605
- oa_id: oaInfo.oa_id,
606
- name: oaInfo.name,
607
- access_token: encrypt(tokens.access_token),
608
- refresh_token: encrypt(tokens.refresh_token),
609
- expires_at: new Date(Date.now() + tokens.expires_in * 1000),
610
- scope: tokens.scope
611
- });
612
-
613
- req.session.oaId = oaAccount.id;
614
- res.redirect('/oa/dashboard');
615
-
616
- } catch (error) {
617
- console.error('OA Auth error:', error);
618
- res.redirect('/auth/error');
619
- }
620
- });
621
- ```
622
-
623
- ### 2. Social Authentication với Next.js
624
-
625
- ```typescript
626
- // pages/api/auth/social/login.ts
627
- import type { NextApiRequest, NextApiResponse } from 'next';
628
- import { ZaloSDK } from '@warriorteam/redai-zalo-sdk';
629
-
630
- const zalo = new ZaloSDK({
631
- appId: process.env.ZALO_SOCIAL_APP_ID!,
632
- appSecret: process.env.ZALO_SOCIAL_APP_SECRET!
633
- });
634
-
635
- export default async function handler(req: NextApiRequest, res: NextApiResponse) {
636
- if (req.method !== 'GET') {
637
- return res.status(405).json({ error: 'Method not allowed' });
638
- }
639
-
640
- // Generate PKCE
641
- const pkce = zalo.generatePKCE();
642
- const state = generateRandomState();
643
-
644
- // Lưu vào session/cookie (hoặc Redis)
645
- res.setHeader('Set-Cookie', [
646
- `pkce_verifier=${pkce.code_verifier}; HttpOnly; Secure; SameSite=Strict`,
647
- `auth_state=${state}; HttpOnly; Secure; SameSite=Strict`
648
- ]);
649
-
650
- const authUrl = zalo.createSocialAuthUrl(
651
- `${process.env.NEXTAUTH_URL}/api/auth/social/callback`,
652
- state
653
- );
654
-
655
- res.redirect(authUrl);
656
- }
657
-
658
- // pages/api/auth/social/callback.ts
659
- export default async function handler(req: NextApiRequest, res: NextApiResponse) {
660
- try {
661
- const { code, state } = req.query;
662
- const cookies = parseCookies(req.headers.cookie || '');
663
-
664
- if (state !== cookies.auth_state) {
665
- throw new Error('Invalid state');
666
- }
667
-
668
- const tokens = await zalo.getSocialAccessToken(
669
- code as string,
670
- `${process.env.NEXTAUTH_URL}/api/auth/social/callback`,
671
- cookies.pkce_verifier
672
- );
673
-
674
- const userInfo = await zalo.getSocialUserInfo(
675
- tokens.access_token,
676
- 'id,name,picture,birthday'
677
- );
678
-
679
- // Tạo hoặc cập nhật user
680
- const user = await User.upsert({
681
- zalo_id: userInfo.id,
682
- name: userInfo.name,
683
- avatar: userInfo.picture?.data.url,
684
- birthday: userInfo.birthday
685
- });
686
-
687
- // Lưu tokens
688
- await UserToken.create({
689
- user_id: user.id,
690
- access_token: encrypt(tokens.access_token),
691
- refresh_token: encrypt(tokens.refresh_token),
692
- expires_at: new Date(Date.now() + tokens.expires_in * 1000),
693
- token_type: 'social'
694
- });
695
-
696
- // Tạo session
697
- const sessionToken = jwt.sign(
698
- { userId: user.id, zaloId: userInfo.id },
699
- process.env.JWT_SECRET!,
700
- { expiresIn: '7d' }
701
- );
702
-
703
- res.setHeader('Set-Cookie', `session=${sessionToken}; HttpOnly; Secure`);
704
- res.redirect('/profile');
705
-
706
- } catch (error) {
707
- console.error('Social auth error:', error);
708
- res.redirect('/login?error=auth_failed');
709
- }
710
- }
711
- ```
712
-
713
- ---
714
-
715
- ## Environment Configuration
716
-
717
- ```typescript
718
- // .env file
719
- ZALO_OA_APP_ID=your_oa_app_id
720
- ZALO_OA_APP_SECRET=your_oa_app_secret
721
- ZALO_SOCIAL_APP_ID=your_social_app_id
722
- ZALO_SOCIAL_APP_SECRET=your_social_app_secret
723
-
724
- // config.ts
725
- export const zaloConfig = {
726
- oa: {
727
- appId: process.env.ZALO_OA_APP_ID!,
728
- appSecret: process.env.ZALO_OA_APP_SECRET!,
729
- },
730
- social: {
731
- appId: process.env.ZALO_SOCIAL_APP_ID!,
732
- appSecret: process.env.ZALO_SOCIAL_APP_SECRET!,
733
- }
734
- };
735
-
736
- // Khởi tạo SDK instances
737
- export const zaloOA = new ZaloSDK(zaloConfig.oa);
738
- export const zaloSocial = new ZaloSDK(zaloConfig.social);
739
- ```
740
-
741
- ---
742
-
743
- ## Testing Authentication
744
-
745
- ### 1. Unit Tests
746
-
747
- ```typescript
748
- // auth.test.ts
749
- import { ZaloSDK } from '@warriorteam/redai-zalo-sdk';
750
-
751
- describe('Authentication', () => {
752
- const zalo = new ZaloSDK({
753
- appId: 'test_app_id',
754
- appSecret: 'test_app_secret'
755
- });
756
-
757
- it('should create OA auth URL', () => {
758
- const url = zalo.createOAAuthUrl('http://localhost:3000/callback', 'test-state');
759
- expect(url).toContain('oauth.zaloapp.com/v4/oa/permission');
760
- expect(url).toContain('app_id=test_app_id');
761
- expect(url).toContain('state=test-state');
762
- });
763
-
764
- it('should generate valid PKCE', () => {
765
- const pkce = zalo.generatePKCE();
766
- expect(pkce.code_verifier).toHaveLength(128);
767
- expect(pkce.code_challenge_method).toBe('S256');
768
- expect(pkce.code_challenge).toBeDefined();
769
- });
770
- });
771
- ```
772
-
773
- ### 2. Integration Tests
774
-
775
- ```typescript
776
- // auth.integration.test.ts
777
- describe('Authentication Integration', () => {
778
- it('should complete OA auth flow', async () => {
779
- // Sử dụng test credentials
780
- const testCode = 'test_authorization_code';
781
- const testRedirectUri = 'http://localhost:3000/callback';
782
-
783
- const tokens = await zalo.getOAAccessToken(testCode, testRedirectUri);
784
-
785
- expect(tokens.access_token).toBeDefined();
786
- expect(tokens.refresh_token).toBeDefined();
787
- expect(tokens.expires_in).toBeGreaterThan(0);
788
- });
789
- });
790
- ```
791
-
792
- ---
793
-
794
- ## Troubleshooting
795
-
796
- ### 1. Common Issues
797
-
798
- **Q: "Invalid redirect_uri" error**
799
- ```
800
- A: Đảm bảo redirect_uri trong callback chính xác giống với lúc tạo auth URL
801
- và đã được đăng ký trong Zalo Developer Console
802
- ```
803
-
804
- **Q: "Invalid code" error**
805
- ```
806
- A: Authorization code chỉ dùng được 1 lần có thời gian sống ngắn (~10 phút)
807
- Đảm bảo xử lý callback ngay sau khi user authorize
808
- ```
809
-
810
- **Q: "Invalid app_id or app_secret"**
811
- ```
812
- A: Kiểm tra credentials trong Zalo Developer Console
813
- Đảm bảo dùng đúng app_id/app_secret cho môi trường (dev/prod)
814
- ```
815
-
816
- ### 2. Debug Authentication
817
-
818
- ```typescript
819
- // Enable debug logging
820
- const zalo = new ZaloSDK({
821
- appId: 'your_app_id',
822
- appSecret: 'your_app_secret',
823
- debug: true // Bật debug logs
824
- });
825
-
826
- // Manual token validation
827
- async function debugToken(accessToken: string) {
828
- try {
829
- const validation = await zalo.auth.validateAccessToken(accessToken);
830
- console.log('Token validation:', validation);
831
-
832
- if (validation.valid) {
833
- console.log(`Token expires in: ${validation.expires_in} seconds`);
834
- console.log(`Token scope: ${validation.scope}`);
835
- }
836
- } catch (error) {
837
- console.error('Token validation failed:', error);
838
- }
839
- }
840
- ```
841
-
842
- ---
843
-
844
- ## Next Steps
845
-
846
- Sau khi hoàn thành authentication:
847
-
848
- 1. **[ZNS Service](./ZNS_SERVICE.md)** - Gửi notification messages
849
- 2. **[Message Services](./MESSAGE_SERVICES.md)** - Gửi các loại tin nhắn khác nhau
850
- 3. **[User Management](./USER_MANAGEMENT.md)** - Quản lý user profiles
851
- 4. **[Group Management](./GROUP_MANAGEMENT.md)** - QuảnZalo groups
852
- 5. **[Webhook Integration](./WEBHOOK_EVENTS.md)** - Xử lý real-time events
853
-
1
+ # RedAI Zalo SDK - Authentication Guide
2
+
3
+ ## Tổng quan
4
+
5
+ RedAI Zalo SDK hỗ trợ đầy đủ các authentication flows của Zalo, bao gồm:
6
+
7
+ - **Official Account (OA) Authentication** - Để truy cập OA APIs (hỗ trợ PKCE)
8
+ - **Social API Authentication** - Để truy cập thông tin user social (hỗ trợ PKCE)
9
+ - **Token Management** - Refresh và validate tokens
10
+ - **PKCE Support** - Security enhancement cho cả OA và Social API
11
+
12
+ ---
13
+
14
+ ## Official Account Authentication
15
+
16
+ ### 1. Tạo Authorization URL
17
+
18
+ #### Cách 1: Basic Authentication (không PKCE)
19
+
20
+ ```typescript
21
+ import { ZaloSDK } from "@warriorteam/redai-zalo-sdk";
22
+
23
+ const zalo = new ZaloSDK({
24
+ appId: "your-oa-app-id",
25
+ appSecret: "your-oa-app-secret"
26
+ });
27
+
28
+ // Tạo authorization URL cho OA - state sẽ được tự động sinh với prefix 'zalo_oa_'
29
+ const authResult = zalo.auth.createOAAuthUrl(
30
+ "https://your-app.com/auth/callback" // redirect_uri
31
+ );
32
+
33
+ console.log("Redirect user to:", authResult.url);
34
+ console.log("Generated state:", authResult.state);
35
+ // Output:
36
+ // - url: https://oauth.zaloapp.com/v4/oa/permission?app_id=xxx&redirect_uri=xxx&state=zalo_oa_abc123...
37
+ // - state: zalo_oa_abc123def456...
38
+
39
+ // Hoặc với custom state
40
+ const customAuthResult = zalo.auth.createOAAuthUrl(
41
+ "https://your-app.com/auth/callback",
42
+ "my-custom-state"
43
+ );
44
+ ```
45
+
46
+ #### Cách 2: Enhanced Security với PKCE (Khuyến nghị)
47
+
48
+ ##### Option A: Manual PKCE Configuration
49
+
50
+ ```typescript
51
+ // Bước 1: Tạo PKCE configuration
52
+ const pkce = zalo.auth.generatePKCE();
53
+ console.log("PKCE Config:", {
54
+ code_verifier: pkce.code_verifier, // Lưu trữ an toàn - cần cho bước exchange token
55
+ code_challenge: pkce.code_challenge, // Sẽ được gửi trong URL
56
+ code_challenge_method: pkce.code_challenge_method // "S256"
57
+ });
58
+
59
+ // Bước 2: Tạo authorization URL với manual PKCE
60
+ const authResult = zalo.auth.createOAAuthUrl(
61
+ "https://your-app.com/auth/callback",
62
+ "my-secure-state", // optional custom state
63
+ true, // usePkce = true
64
+ pkce // manual PKCE config
65
+ );
66
+
67
+ console.log("Secure auth URL:", authResult.url);
68
+ console.log("State to verify:", authResult.state);
69
+ console.log("PKCE used:", authResult.pkce);
70
+
71
+ // ⚠️ QUAN TRỌNG: Lưu trữ code_verifier và state để sử dụng ở bước exchange token
72
+ sessionStorage.setItem('pkce_code_verifier', pkce.code_verifier);
73
+ sessionStorage.setItem('auth_state', authResult.state);
74
+ ```
75
+
76
+ ##### Option B: Auto-Generated PKCE (Đơn giản nhất)
77
+
78
+ ```typescript
79
+ // Tạo authorization URL với auto-generated PKCE
80
+ const authResult = zalo.auth.createOAAuthUrl(
81
+ "https://your-app.com/auth/callback",
82
+ undefined, // state sẽ được auto-generate
83
+ true // usePkce = true, PKCE sẽ được auto-generate
84
+ );
85
+
86
+ console.log("Secure auth URL:", authResult.url);
87
+ console.log("Auto-generated state:", authResult.state);
88
+ console.log("Auto-generated PKCE:", authResult.pkce);
89
+
90
+ // ⚠️ QUAN TRỌNG: Lưu trữ auto-generated values
91
+ sessionStorage.setItem('pkce_code_verifier', authResult.pkce!.code_verifier);
92
+ sessionStorage.setItem('auth_state', authResult.state);
93
+ ```
94
+
95
+ ### 2. Xử lý Callback và Lấy Access Token
96
+
97
+ #### Cách 1: Basic Token Exchange
98
+
99
+ ```typescript
100
+ // Trong route callback của bạn
101
+ app.get('/auth/callback', async (req, res) => {
102
+ const { code, state } = req.query;
103
+
104
+ try {
105
+ // Lấy access token từ authorization code
106
+ const tokenResponse = await zalo.auth.getOAAccessToken({
107
+ app_id: "your-oa-app-id",
108
+ app_secret: "your-oa-app-secret",
109
+ code: code as string,
110
+ redirect_uri: "https://your-app.com/auth/callback"
111
+ });
112
+
113
+ console.log("OA Access Token:", tokenResponse.access_token);
114
+ console.log("Refresh Token:", tokenResponse.refresh_token);
115
+ console.log("Expires In:", tokenResponse.expires_in); // seconds
116
+
117
+ // Lưu tokens vào database/session
118
+ await saveTokens(tokenResponse);
119
+
120
+ res.redirect('/dashboard');
121
+ } catch (error) {
122
+ console.error("Auth error:", error);
123
+ res.redirect('/auth/error');
124
+ }
125
+ });
126
+ ```
127
+
128
+ #### Cách 2: Secure Token Exchange với PKCE
129
+
130
+ ```typescript
131
+ app.get('/auth/callback', async (req, res) => {
132
+ const { code, state } = req.query;
133
+
134
+ try {
135
+ // Bước 1: Verify state để chống CSRF attack
136
+ const storedState = sessionStorage.getItem('auth_state');
137
+ if (state !== storedState) {
138
+ throw new Error('State mismatch - possible CSRF attack');
139
+ }
140
+
141
+ // Bước 2: Lấy code_verifier đã lưu trữ
142
+ const codeVerifier = sessionStorage.getItem('pkce_code_verifier');
143
+ if (!codeVerifier) {
144
+ throw new Error('Code verifier not found');
145
+ }
146
+
147
+ // Bước 3: Exchange authorization code với PKCE
148
+ const tokenResponse = await zalo.auth.getOAAccessToken({
149
+ app_id: "your-oa-app-id",
150
+ app_secret: "your-oa-app-secret",
151
+ code: code as string,
152
+ redirect_uri: "https://your-app.com/auth/callback",
153
+ code_verifier: codeVerifier // 🔐 PKCE code verifier
154
+ });
155
+
156
+ console.log("Secure OA Access Token:", tokenResponse.access_token);
157
+
158
+ // Bước 4: Xóa temporary data
159
+ sessionStorage.removeItem('auth_state');
160
+ sessionStorage.removeItem('pkce_code_verifier');
161
+
162
+ // Lưu tokens
163
+ await saveTokens(tokenResponse);
164
+
165
+ res.redirect('/dashboard');
166
+ } catch (error) {
167
+ console.error("Secure auth error:", error);
168
+ res.redirect('/auth/error');
169
+ }
170
+ });
171
+ ```
172
+
173
+ ### 3. PKCE Security Benefits
174
+
175
+ PKCE (Proof Key for Code Exchange) cung cấp các lợi ích bảo mật quan trọng:
176
+
177
+ #### 🔐 Tại sao nên sử dụng PKCE?
178
+
179
+ 1. **Chống Authorization Code Interception**:
180
+ - Ngay cả khi authorization code bị đánh cắp, attacker không thể sử dụng mà không có `code_verifier`
181
+
182
+ 2. **Không cần lưu trữ App Secret ở client**:
183
+ - PKCE cho phép public clients (mobile apps, SPAs) thực hiện OAuth flow an toàn
184
+
185
+ 3. **Chống CSRF và Replay Attacks**:
186
+ - Mỗi request có unique `code_verifier` `code_challenge`
187
+
188
+ #### 🛡️ PKCE Flow Security
189
+
190
+ ```
191
+ 1. Client tạo code_verifier (random string)
192
+ 2. Client tạo code_challenge = SHA256(code_verifier)
193
+ 3. Client gửi code_challenge trong authorization request
194
+ 4. Authorization server lưu code_challenge
195
+ 5. Client nhận authorization code
196
+ 6. Client gửi code + code_verifier để exchange token
197
+ 7. Server verify: SHA256(code_verifier) == stored code_challenge
198
+ 8. Nếu match trả về access token
199
+ ```
200
+
201
+ #### ⚠️ Best Practices
202
+
203
+ - **Luôn sử dụng PKCE** cho production applications
204
+ - **Lưu trữ code_verifier an toàn** (session, secure storage)
205
+ - **Verify state parameter** để chống CSRF
206
+ - **Sử dụng HTTPS** cho tất cả OAuth endpoints
207
+ - **Set proper expiration** cho stored PKCE data
208
+
209
+ ### 5. Token Response Structure
210
+
211
+ ```typescript
212
+ interface AccessToken {
213
+ access_token: string; // Token để gọi API
214
+ refresh_token: string; // Token để refresh
215
+ expires_in: number; // Thời gian sống (seconds)
216
+ token_type: "Bearer"; // Loại token
217
+ scope: string; // Quyền được cấp
218
+ }
219
+ ```
220
+
221
+ ### 6. Sử dụng Access Token
222
+
223
+ ```typescript
224
+ // Lấy thông tin OA
225
+ const oaInfo = await zalo.getOAInfo(tokenResponse.access_token);
226
+ console.log("OA Name:", oaInfo.name);
227
+ console.log("Followers:", oaInfo.num_follower);
228
+
229
+ // Gửi tin nhắn consultation
230
+ await zalo.sendConsultationText(
231
+ tokenResponse.access_token,
232
+ "user-zalo-id",
233
+ "Xin chào! Cảm ơn bạn đã quan tâm đến dịch vụ của chúng tôi."
234
+ );
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Social API Authentication
240
+
241
+ ### 1. Tạo Authorization URL (Basic)
242
+
243
+ ```typescript
244
+ const zalo = new ZaloSDK({
245
+ appId: "your-social-app-id",
246
+ appSecret: "your-social-app-secret"
247
+ });
248
+
249
+ // Tạo authorization URL cho Social API
250
+ const authUrl = zalo.createSocialAuthUrl(
251
+ "https://your-app.com/social/callback",
252
+ "optional-state"
253
+ );
254
+
255
+ console.log("Redirect user to:", authUrl);
256
+ ```
257
+
258
+ ### 2. Tạo Authorization URL với PKCE (Khuyến nghị)
259
+
260
+ ```typescript
261
+ // Tạo PKCE parameters cho bảo mật cao hơn
262
+ const pkce = zalo.generatePKCE();
263
+ console.log("Code Verifier:", pkce.code_verifier);
264
+ console.log("Code Challenge:", pkce.code_challenge);
265
+
266
+ // Lưu code_verifier vào session/state
267
+ req.session.codeVerifier = pkce.code_verifier;
268
+
269
+ const authUrl = zalo.createSocialAuthUrl(
270
+ "https://your-app.com/social/callback",
271
+ "social-login"
272
+ );
273
+ ```
274
+
275
+ ### 3. PKCE Generation
276
+
277
+ ```typescript
278
+ interface PKCEConfig {
279
+ code_verifier: string; // Random string 43-128 chars
280
+ code_challenge: string; // base64url(sha256(code_verifier))
281
+ code_challenge_method: "S256";
282
+ }
283
+
284
+ // SDK tự động tạo PKCE theo chuẩn RFC 7636
285
+ const pkce = zalo.generatePKCE();
286
+ ```
287
+
288
+ ### 4. Xử lý Social Callback
289
+
290
+ ```typescript
291
+ app.get('/social/callback', async (req, res) => {
292
+ const { code, state } = req.query;
293
+ const codeVerifier = req.session.codeVerifier; // Lấy từ session
294
+
295
+ try {
296
+ const tokenResponse = await zalo.getSocialAccessToken(
297
+ code as string,
298
+ "https://your-app.com/social/callback",
299
+ codeVerifier // Bắt buộc nếu dùng PKCE
300
+ );
301
+
302
+ // Lấy thông tin user
303
+ const userInfo = await zalo.getSocialUserInfo(
304
+ tokenResponse.access_token,
305
+ "id,name,picture,birthday,gender" // fields cần lấy
306
+ );
307
+
308
+ console.log("User Info:", userInfo);
309
+
310
+ // Lưu vào database
311
+ await createOrUpdateUser(userInfo, tokenResponse);
312
+
313
+ res.redirect('/profile');
314
+ } catch (error) {
315
+ console.error("Social auth error:", error);
316
+ res.redirect('/login?error=auth_failed');
317
+ }
318
+ });
319
+ ```
320
+
321
+ ### 5. Social User Info Response
322
+
323
+ ```typescript
324
+ interface SocialUserInfo {
325
+ id: string; // Zalo user ID
326
+ name: string; // Tên hiển thị
327
+ picture?: {
328
+ data: {
329
+ url: string; // Avatar URL
330
+ }
331
+ };
332
+ birthday?: string; // Ngày sinh (YYYY-MM-DD)
333
+ gender?: number; // 1: Nam, 2: Nữ
334
+ locale?: string; // Locale
335
+ // Các fields khác tùy theo quyền được cấp
336
+ }
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Token Management
342
+
343
+ ### 1. Refresh OA Access Token
344
+
345
+ ```typescript
346
+ async function refreshOAToken(refreshToken: string): Promise<AccessToken> {
347
+ try {
348
+ const newTokens = await zalo.refreshOAAccessToken(refreshToken);
349
+
350
+ // Lưu tokens mới
351
+ await updateTokens(newTokens);
352
+
353
+ return newTokens;
354
+ } catch (error) {
355
+ console.error("Failed to refresh OA token:", error);
356
+ // Redirect to re-authentication
357
+ throw new Error("Re-authentication required");
358
+ }
359
+ }
360
+ ```
361
+
362
+ ### 2. Refresh Social Access Token
363
+
364
+ ```typescript
365
+ async function refreshSocialToken(refreshToken: string): Promise<AccessToken> {
366
+ try {
367
+ const newTokens = await zalo.refreshSocialAccessToken(refreshToken);
368
+ return newTokens;
369
+ } catch (error) {
370
+ console.error("Failed to refresh social token:", error);
371
+ throw error;
372
+ }
373
+ }
374
+ ```
375
+
376
+ ### 3. Auto Token Refresh Middleware
377
+
378
+ ```typescript
379
+ // Express middleware để auto refresh tokens
380
+ export const autoRefreshToken = async (req: Request, res: Response, next: NextFunction) => {
381
+ const { accessToken, refreshToken, expiresAt } = req.user;
382
+
383
+ // Kiểm tra token sắp hết hạn (trước 5 phút)
384
+ const willExpireSoon = Date.now() > (expiresAt - 5 * 60 * 1000);
385
+
386
+ if (willExpireSoon && refreshToken) {
387
+ try {
388
+ const tokenType = req.path.includes('/oa/') ? 'oa' : 'social';
389
+
390
+ const newTokens = tokenType === 'oa'
391
+ ? await zalo.refreshOAAccessToken(refreshToken)
392
+ : await zalo.refreshSocialAccessToken(refreshToken);
393
+
394
+ // Cập nhật user với tokens mới
395
+ req.user.accessToken = newTokens.access_token;
396
+ req.user.refreshToken = newTokens.refresh_token;
397
+ req.user.expiresAt = Date.now() + (newTokens.expires_in * 1000);
398
+
399
+ // Lưu vào database
400
+ await updateUserTokens(req.user.id, newTokens);
401
+
402
+ console.log("Token refreshed successfully");
403
+ } catch (error) {
404
+ console.error("Auto refresh failed:", error);
405
+ return res.status(401).json({ error: "Authentication expired" });
406
+ }
407
+ }
408
+
409
+ next();
410
+ };
411
+ ```
412
+
413
+ ---
414
+
415
+ ## Token Validation
416
+
417
+ ### 1. Validate Access Token
418
+
419
+ ```typescript
420
+ // Validate OA token
421
+ const isValidOA = await zalo.validateAccessToken(accessToken, 'oa');
422
+ console.log("OA Token valid:", isValidOA);
423
+
424
+ // Validate Social token
425
+ const isValidSocial = await zalo.validateAccessToken(accessToken, 'social');
426
+ console.log("Social Token valid:", isValidSocial);
427
+ ```
428
+
429
+ ### 2. Advanced Token Validation
430
+
431
+ ```typescript
432
+ // Sử dụng service trực tiếp để có thêm thông tin
433
+ const validation = await zalo.auth.validateAccessToken(accessToken);
434
+
435
+ interface TokenValidation {
436
+ valid: boolean;
437
+ expires_in?: number; // Thời gian còn lại (seconds)
438
+ scope?: string; // Quyền hiện tại
439
+ app_id?: string; // App ID của token
440
+ user_id?: string; // User ID (nếu có)
441
+ }
442
+ ```
443
+
444
+ ---
445
+
446
+ ## Security Best Practices
447
+
448
+ ### 1. State Parameter
449
+
450
+ Luôn sử dụng `state` parameter để chống CSRF:
451
+
452
+ ```typescript
453
+ // Tạo random state
454
+ const state = crypto.randomBytes(16).toString('hex');
455
+ req.session.authState = state;
456
+
457
+ const authUrl = zalo.createOAAuthUrl(redirectUri, state);
458
+
459
+ // Trong callback, verify state
460
+ if (req.query.state !== req.session.authState) {
461
+ throw new Error("Invalid state parameter");
462
+ }
463
+ ```
464
+
465
+ ### 2. PKCE cho Social API
466
+
467
+ Luôn sử dụng PKCE cho Social API:
468
+
469
+ ```typescript
470
+ // ✅ Đúng - với PKCE
471
+ const pkce = zalo.generatePKCE();
472
+ req.session.codeVerifier = pkce.code_verifier;
473
+
474
+ // ❌ Sai - không dùng PKCE
475
+ const authUrl = zalo.createSocialAuthUrl(redirectUri);
476
+ ```
477
+
478
+ ### 3. Token Storage
479
+
480
+ ```typescript
481
+ // ✅ Đúng - mã hóa tokens khi lưu
482
+ const encryptedToken = encrypt(accessToken);
483
+ await db.users.update(userId, {
484
+ access_token: encryptedToken,
485
+ refresh_token: encrypt(refreshToken)
486
+ });
487
+
488
+ // ❌ Sai - lưu plain text
489
+ await db.users.update(userId, {
490
+ access_token: accessToken // Không an toàn
491
+ });
492
+ ```
493
+
494
+ ### 4. Token Scope Validation
495
+
496
+ ```typescript
497
+ // Kiểm tra token có đúng scope không
498
+ async function requireScope(requiredScope: string) {
499
+ const validation = await zalo.auth.validateAccessToken(accessToken);
500
+
501
+ if (!validation.scope?.includes(requiredScope)) {
502
+ throw new Error(`Missing required scope: ${requiredScope}`);
503
+ }
504
+ }
505
+
506
+ // Usage
507
+ await requireScope('oa.message.send');
508
+ ```
509
+
510
+ ---
511
+
512
+ ## Error Handling
513
+
514
+ ### 1. Common Auth Errors
515
+
516
+ ```typescript
517
+ try {
518
+ const tokens = await zalo.getOAAccessToken(code, redirectUri);
519
+ } catch (error) {
520
+ if (error.code === -201) {
521
+ // Invalid parameters (code expired, wrong redirect_uri, etc.)
522
+ console.error("Invalid auth parameters:", error.message);
523
+ } else if (error.code === -216) {
524
+ // Invalid app credentials
525
+ console.error("Invalid app_id or app_secret");
526
+ } else {
527
+ console.error("Unexpected auth error:", error);
528
+ }
529
+ }
530
+ ```
531
+
532
+ ### 2. Token Refresh Errors
533
+
534
+ ```typescript
535
+ async function handleTokenRefresh(refreshToken: string) {
536
+ try {
537
+ return await zalo.refreshOAAccessToken(refreshToken);
538
+ } catch (error) {
539
+ if (error.code === -216) {
540
+ // Refresh token expired/invalid
541
+ console.log("Refresh token expired, require re-authentication");
542
+ redirectToLogin();
543
+ } else {
544
+ console.error("Refresh failed:", error);
545
+ throw error;
546
+ }
547
+ }
548
+ }
549
+ ```
550
+
551
+ ---
552
+
553
+ ## Complete Authentication Flow Examples
554
+
555
+ ### 1. OA Authentication với Express
556
+
557
+ ```typescript
558
+ import express from 'express';
559
+ import { ZaloSDK } from '@warriorteam/redai-zalo-sdk';
560
+
561
+ const app = express();
562
+ const zalo = new ZaloSDK({
563
+ appId: process.env.ZALO_OA_APP_ID!,
564
+ appSecret: process.env.ZALO_OA_APP_SECRET!,
565
+ debug: process.env.NODE_ENV === 'development'
566
+ });
567
+
568
+ // Route bắt đầu auth
569
+ app.get('/auth/oa', (req, res) => {
570
+ const state = generateRandomState();
571
+ req.session.authState = state;
572
+
573
+ const authUrl = zalo.createOAAuthUrl(
574
+ `${process.env.BASE_URL}/auth/oa/callback`,
575
+ state
576
+ );
577
+
578
+ res.redirect(authUrl);
579
+ });
580
+
581
+ // Callback handler
582
+ app.get('/auth/oa/callback', async (req, res) => {
583
+ try {
584
+ const { code, state, error } = req.query;
585
+
586
+ if (error) {
587
+ return res.redirect(`/auth/error?reason=${error}`);
588
+ }
589
+
590
+ if (state !== req.session.authState) {
591
+ return res.status(400).json({ error: 'Invalid state' });
592
+ }
593
+
594
+ const tokens = await zalo.getOAAccessToken(
595
+ code as string,
596
+ `${process.env.BASE_URL}/auth/oa/callback`
597
+ );
598
+
599
+ // Lấy thông tin OA
600
+ const oaInfo = await zalo.getOAInfo(tokens.access_token);
601
+
602
+ // Lưu vào database
603
+ const oaAccount = await OAAccount.create({
604
+ oa_id: oaInfo.oa_id,
605
+ name: oaInfo.name,
606
+ access_token: encrypt(tokens.access_token),
607
+ refresh_token: encrypt(tokens.refresh_token),
608
+ expires_at: new Date(Date.now() + tokens.expires_in * 1000),
609
+ scope: tokens.scope
610
+ });
611
+
612
+ req.session.oaId = oaAccount.id;
613
+ res.redirect('/oa/dashboard');
614
+
615
+ } catch (error) {
616
+ console.error('OA Auth error:', error);
617
+ res.redirect('/auth/error');
618
+ }
619
+ });
620
+ ```
621
+
622
+ ### 2. Social Authentication với Next.js
623
+
624
+ ```typescript
625
+ // pages/api/auth/social/login.ts
626
+ import type { NextApiRequest, NextApiResponse } from 'next';
627
+ import { ZaloSDK } from '@warriorteam/redai-zalo-sdk';
628
+
629
+ const zalo = new ZaloSDK({
630
+ appId: process.env.ZALO_SOCIAL_APP_ID!,
631
+ appSecret: process.env.ZALO_SOCIAL_APP_SECRET!
632
+ });
633
+
634
+ export default async function handler(req: NextApiRequest, res: NextApiResponse) {
635
+ if (req.method !== 'GET') {
636
+ return res.status(405).json({ error: 'Method not allowed' });
637
+ }
638
+
639
+ // Generate PKCE
640
+ const pkce = zalo.generatePKCE();
641
+ const state = generateRandomState();
642
+
643
+ // Lưu vào session/cookie (hoặc Redis)
644
+ res.setHeader('Set-Cookie', [
645
+ `pkce_verifier=${pkce.code_verifier}; HttpOnly; Secure; SameSite=Strict`,
646
+ `auth_state=${state}; HttpOnly; Secure; SameSite=Strict`
647
+ ]);
648
+
649
+ const authUrl = zalo.createSocialAuthUrl(
650
+ `${process.env.NEXTAUTH_URL}/api/auth/social/callback`,
651
+ state
652
+ );
653
+
654
+ res.redirect(authUrl);
655
+ }
656
+
657
+ // pages/api/auth/social/callback.ts
658
+ export default async function handler(req: NextApiRequest, res: NextApiResponse) {
659
+ try {
660
+ const { code, state } = req.query;
661
+ const cookies = parseCookies(req.headers.cookie || '');
662
+
663
+ if (state !== cookies.auth_state) {
664
+ throw new Error('Invalid state');
665
+ }
666
+
667
+ const tokens = await zalo.getSocialAccessToken(
668
+ code as string,
669
+ `${process.env.NEXTAUTH_URL}/api/auth/social/callback`,
670
+ cookies.pkce_verifier
671
+ );
672
+
673
+ const userInfo = await zalo.getSocialUserInfo(
674
+ tokens.access_token,
675
+ 'id,name,picture,birthday'
676
+ );
677
+
678
+ // Tạo hoặc cập nhật user
679
+ const user = await User.upsert({
680
+ zalo_id: userInfo.id,
681
+ name: userInfo.name,
682
+ avatar: userInfo.picture?.data.url,
683
+ birthday: userInfo.birthday
684
+ });
685
+
686
+ // Lưu tokens
687
+ await UserToken.create({
688
+ user_id: user.id,
689
+ access_token: encrypt(tokens.access_token),
690
+ refresh_token: encrypt(tokens.refresh_token),
691
+ expires_at: new Date(Date.now() + tokens.expires_in * 1000),
692
+ token_type: 'social'
693
+ });
694
+
695
+ // Tạo session
696
+ const sessionToken = jwt.sign(
697
+ { userId: user.id, zaloId: userInfo.id },
698
+ process.env.JWT_SECRET!,
699
+ { expiresIn: '7d' }
700
+ );
701
+
702
+ res.setHeader('Set-Cookie', `session=${sessionToken}; HttpOnly; Secure`);
703
+ res.redirect('/profile');
704
+
705
+ } catch (error) {
706
+ console.error('Social auth error:', error);
707
+ res.redirect('/login?error=auth_failed');
708
+ }
709
+ }
710
+ ```
711
+
712
+ ---
713
+
714
+ ## Environment Configuration
715
+
716
+ ```typescript
717
+ // .env file
718
+ ZALO_OA_APP_ID=your_oa_app_id
719
+ ZALO_OA_APP_SECRET=your_oa_app_secret
720
+ ZALO_SOCIAL_APP_ID=your_social_app_id
721
+ ZALO_SOCIAL_APP_SECRET=your_social_app_secret
722
+
723
+ // config.ts
724
+ export const zaloConfig = {
725
+ oa: {
726
+ appId: process.env.ZALO_OA_APP_ID!,
727
+ appSecret: process.env.ZALO_OA_APP_SECRET!,
728
+ },
729
+ social: {
730
+ appId: process.env.ZALO_SOCIAL_APP_ID!,
731
+ appSecret: process.env.ZALO_SOCIAL_APP_SECRET!,
732
+ }
733
+ };
734
+
735
+ // Khởi tạo SDK instances
736
+ export const zaloOA = new ZaloSDK(zaloConfig.oa);
737
+ export const zaloSocial = new ZaloSDK(zaloConfig.social);
738
+ ```
739
+
740
+ ---
741
+
742
+ ## Testing Authentication
743
+
744
+ ### 1. Unit Tests
745
+
746
+ ```typescript
747
+ // auth.test.ts
748
+ import { ZaloSDK } from '@warriorteam/redai-zalo-sdk';
749
+
750
+ describe('Authentication', () => {
751
+ const zalo = new ZaloSDK({
752
+ appId: 'test_app_id',
753
+ appSecret: 'test_app_secret'
754
+ });
755
+
756
+ it('should create OA auth URL', () => {
757
+ const url = zalo.createOAAuthUrl('http://localhost:3000/callback', 'test-state');
758
+ expect(url).toContain('oauth.zaloapp.com/v4/oa/permission');
759
+ expect(url).toContain('app_id=test_app_id');
760
+ expect(url).toContain('state=test-state');
761
+ });
762
+
763
+ it('should generate valid PKCE', () => {
764
+ const pkce = zalo.generatePKCE();
765
+ expect(pkce.code_verifier).toHaveLength(128);
766
+ expect(pkce.code_challenge_method).toBe('S256');
767
+ expect(pkce.code_challenge).toBeDefined();
768
+ });
769
+ });
770
+ ```
771
+
772
+ ### 2. Integration Tests
773
+
774
+ ```typescript
775
+ // auth.integration.test.ts
776
+ describe('Authentication Integration', () => {
777
+ it('should complete OA auth flow', async () => {
778
+ // Sử dụng test credentials
779
+ const testCode = 'test_authorization_code';
780
+ const testRedirectUri = 'http://localhost:3000/callback';
781
+
782
+ const tokens = await zalo.getOAAccessToken(testCode, testRedirectUri);
783
+
784
+ expect(tokens.access_token).toBeDefined();
785
+ expect(tokens.refresh_token).toBeDefined();
786
+ expect(tokens.expires_in).toBeGreaterThan(0);
787
+ });
788
+ });
789
+ ```
790
+
791
+ ---
792
+
793
+ ## Troubleshooting
794
+
795
+ ### 1. Common Issues
796
+
797
+ **Q: "Invalid redirect_uri" error**
798
+ ```
799
+ A: Đảm bảo redirect_uri trong callback chính xác giống với lúc tạo auth URL
800
+ đã được đăng trong Zalo Developer Console
801
+ ```
802
+
803
+ **Q: "Invalid code" error**
804
+ ```
805
+ A: Authorization code chỉ dùng được 1 lần và có thời gian sống ngắn (~10 phút)
806
+ Đảm bảo xử callback ngay sau khi user authorize
807
+ ```
808
+
809
+ **Q: "Invalid app_id or app_secret"**
810
+ ```
811
+ A: Kiểm tra credentials trong Zalo Developer Console
812
+ Đảm bảo dùng đúng app_id/app_secret cho môi trường (dev/prod)
813
+ ```
814
+
815
+ ### 2. Debug Authentication
816
+
817
+ ```typescript
818
+ // Enable debug logging
819
+ const zalo = new ZaloSDK({
820
+ appId: 'your_app_id',
821
+ appSecret: 'your_app_secret',
822
+ debug: true // Bật debug logs
823
+ });
824
+
825
+ // Manual token validation
826
+ async function debugToken(accessToken: string) {
827
+ try {
828
+ const validation = await zalo.auth.validateAccessToken(accessToken);
829
+ console.log('Token validation:', validation);
830
+
831
+ if (validation.valid) {
832
+ console.log(`Token expires in: ${validation.expires_in} seconds`);
833
+ console.log(`Token scope: ${validation.scope}`);
834
+ }
835
+ } catch (error) {
836
+ console.error('Token validation failed:', error);
837
+ }
838
+ }
839
+ ```
840
+
841
+ ---
842
+
843
+ ## Next Steps
844
+
845
+ Sau khi hoàn thành authentication:
846
+
847
+ 1. **[ZNS Service](./ZNS_SERVICE.md)** - Gửi notification messages
848
+ 2. **[Message Services](./MESSAGE_SERVICES.md)** - Gửi các loại tin nhắn khác nhau
849
+ 3. **[User Management](./USER_MANAGEMENT.md)** - Quản user profiles
850
+ 4. **[Group Management](./GROUP_MANAGEMENT.md)** - Quản lý Zalo groups
851
+ 5. **[Webhook Integration](./WEBHOOK_EVENTS.md)** - Xửreal-time events
852
+
854
853
  Tham khảo **[API Reference](./API_REFERENCE.md)** để biết chi tiết về tất cả methods có sẵn.