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