@warriorteam/redai-zalo-sdk
Version:
Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account v3.0, ZNS with Full Type Safety, Consultation Service, Broadcast Service, Group Messaging with List APIs, Social APIs, Enhanced Article Management, Promotion Service v3.0 with Multip
346 lines • 14.3 kB
JavaScript
;
/**
* Authentication service for Zalo API
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthService = void 0;
const auth_1 = require("../types/auth");
const common_1 = require("../types/common");
const crypto_1 = require("crypto");
/**
* Authentication service for handling OAuth flows and token management
*/
class AuthService {
constructor(client, appId, appSecret) {
this.client = client;
this.appId = appId;
this.appSecret = appSecret;
// Zalo OAuth & Graph endpoints - organized by functionality
this.endpoints = {
auth: {
oaPermission: "https://oauth.zaloapp.com/v4/oa/permission",
socialPermission: "https://oauth.zaloapp.com/v4/permission",
oaToken: "https://oauth.zaloapp.com/v4/oa/access_token",
socialToken: "https://oauth.zaloapp.com/v4/access_token",
refreshOaToken: "https://oauth.zaloapp.com/v4/oa/access_token",
refreshSocialToken: "https://openapi.zalo.me/v2.0/oauth/access_token",
},
social: {
me: "https://graph.zalo.me/v2.0/me",
},
};
}
/**
* Generate PKCE code verifier and challenge for Social API
*/
generatePKCE() {
const codeVerifier = (0, crypto_1.randomBytes)(32).toString("base64url");
const codeChallenge = (0, crypto_1.createHash)("sha256")
.update(codeVerifier)
.digest("base64url");
return {
code_verifier: codeVerifier,
code_challenge: codeChallenge,
code_challenge_method: "S256",
};
}
/**
* Create OAuth authorization URL for Official Account with PKCE support
*
* @param redirectUri - The redirect URI after authorization
* @param state - Optional state parameter for security. If not provided, auto-generates with 'zalo_oa_' prefix
* @param usePkce - Whether to use PKCE for enhanced security. If true and pkce not provided, will auto-generate
* @param pkce - Optional PKCE configuration for enhanced security. If usePkce=true and this is not provided, will be auto-generated
* @returns Object containing the authorization URL, state, and PKCE config (if used)
*/
createOAAuthUrl(redirectUri, state, pkce, usePkce = false) {
// Generate state with zalo_oa_ prefix if not provided
const finalState = state || `zalo_oa_${(0, crypto_1.randomBytes)(16).toString("hex")}`;
// Auto-generate PKCE if usePkce is true but pkce is not provided
let finalPkce = pkce;
if (usePkce && !pkce) {
finalPkce = this.generatePKCE();
}
const params = new URLSearchParams({
app_id: this.appId,
redirect_uri: redirectUri,
state: finalState,
});
// Add PKCE parameters if PKCE is being used
if (usePkce && finalPkce) {
params.append("code_challenge", finalPkce.code_challenge);
params.append("code_challenge_method", finalPkce.code_challenge_method);
}
const url = `${this.endpoints.auth.oaPermission}?${params.toString()}`;
return {
url,
state: finalState,
pkce: usePkce ? finalPkce : undefined,
};
}
/**
* Create OAuth authorization URL for Social API
*/
createSocialAuthUrl(redirectUri, state, pkce) {
const params = new URLSearchParams({
app_id: this.appId,
redirect_uri: redirectUri,
state: state || "social_auth",
});
if (pkce) {
params.append("code_challenge", pkce.code_challenge);
params.append("code_challenge_method", pkce.code_challenge_method);
}
return `${this.endpoints.auth.socialPermission}?${params.toString()}`;
}
/**
* Exchange authorization code for Official Account access token
* Now supports PKCE code_verifier for enhanced security
*/
async getOAAccessToken(params) {
try {
const url = this.endpoints.auth.oaToken;
// Prepare form data according to API docs
const formData = new URLSearchParams();
formData.append("code", params.code);
formData.append("app_id", params.app_id);
formData.append("grant_type", "authorization_code");
// Add code_verifier if provided (for PKCE)
if (params.code_verifier) {
formData.append("code_verifier", params.code_verifier);
}
let result;
try {
result = await this.client.oauthRequestWithUrl("POST", url, formData.toString(), {
"Content-Type": "application/x-www-form-urlencoded",
"secret_key": params.app_secret,
});
}
catch (error) {
// If the error contains access_token, it means the request was successful
// but Zalo API returned a warning/error code along with valid tokens
if (error instanceof common_1.ZaloSDKError && error.details?.access_token) {
result = error.details;
}
else {
throw error;
}
}
// Check if we have access_token (prioritize token presence over error codes)
if (result.access_token) {
return {
access_token: result.access_token,
expires_in: parseInt(result.expires_in) || result.expires_in,
refresh_token: result.refresh_token,
};
}
// If no access_token but we have data wrapper, check inside
if (result.data?.access_token) {
return {
access_token: result.data.access_token,
expires_in: parseInt(result.data.expires_in) || result.data.expires_in,
refresh_token: result.data.refresh_token,
};
}
// Only check error codes if we don't have access_token
if (result.error !== 0) {
throw new common_1.ZaloSDKError(result.error_description ||
result.message ||
"Failed to get OA access token", result.error, result);
}
throw new common_1.ZaloSDKError("No access token received from Zalo API", -1, result);
}
catch (error) {
if (error instanceof common_1.ZaloSDKError) {
throw error;
}
throw new common_1.ZaloSDKError(`Failed to get OA access token: ${error.message}`, -1, error);
}
}
/**
* Exchange authorization code for Social API access token
*/
async getSocialAccessToken(params) {
try {
const url = this.endpoints.auth.socialToken;
const formData = new URLSearchParams();
formData.append("code", params.code);
formData.append("app_id", params.app_id);
formData.append("grant_type", "authorization_code");
if (params.code_verifier) {
formData.append("code_verifier", params.code_verifier);
}
let result;
try {
result = await this.client.oauthRequestWithUrl("POST", url, formData.toString(), {
"Content-Type": "application/x-www-form-urlencoded",
secret_key: params.app_secret,
});
}
catch (error) {
// If the error contains access_token, it means the request was successful
// but Zalo API returned a warning/error code along with valid tokens
if (error instanceof common_1.ZaloSDKError && error.details?.access_token) {
result = error.details;
}
else {
throw error;
}
}
// Check if we have access_token (prioritize token presence over error codes)
if (result.access_token) {
return {
access_token: result.access_token,
expires_in: result.expires_in,
refresh_token: result.refresh_token,
};
}
// If no access_token but we have data wrapper, check inside
if (result.data?.access_token) {
return {
access_token: result.data.access_token,
expires_in: result.data.expires_in,
refresh_token: result.data.refresh_token,
};
}
// Only check error codes if we don't have access_token
if (result.error && result.error !== 0) {
throw new common_1.ZaloSDKError(result.error_description ||
result.message ||
"Failed to get Social access token", result.error, result);
}
throw new common_1.ZaloSDKError("No access token received from Zalo API", -1, result);
}
catch (error) {
if (error instanceof common_1.ZaloSDKError) {
throw error;
}
throw new common_1.ZaloSDKError(`Failed to get Social access token: ${error.message}`, -1, error);
}
}
/**
* Refresh Official Account access token
*/
async refreshOAAccessToken(params) {
try {
const url = this.endpoints.auth.refreshOaToken;
const formData = new URLSearchParams();
formData.append("refresh_token", params.refresh_token);
formData.append("app_id", params.app_id);
formData.append("grant_type", "refresh_token");
const result = await this.client.oauthRequestWithUrl("POST", url, formData.toString(), {
"Content-Type": "application/x-www-form-urlencoded",
secret_key: params.app_secret,
});
if (!result.access_token) {
throw new common_1.ZaloSDKError("No access token received when refreshing OA token", -1);
}
return {
access_token: result.access_token,
refresh_token: result.refresh_token,
expires_in: parseInt(result.expires_in),
};
}
catch (error) {
if (error instanceof common_1.ZaloSDKError) {
throw error;
}
throw new common_1.ZaloSDKError(`Failed to refresh OA access token: ${error.message}`, -1, error);
}
}
/**
* Refresh Social API access token
*/
async refreshSocialAccessToken(params) {
try {
const endpoint = this.endpoints.auth.refreshSocialToken;
const requestParams = {
app_id: params.app_id,
grant_type: "refresh_token",
refresh_token: params.refresh_token,
};
const result = await this.client.apiGet(endpoint, "", requestParams);
if (result.error !== 0) {
throw new common_1.ZaloSDKError(result.error_description ||
result.message ||
"Failed to refresh Social access token", result.error, result);
}
if (!result.data) {
throw new common_1.ZaloSDKError("No data received when refreshing Social token", -1);
}
return result.data;
}
catch (error) {
if (error instanceof common_1.ZaloSDKError) {
throw error;
}
throw new common_1.ZaloSDKError(`Failed to refresh Social access token: ${error.message}`, -1, error);
}
}
/**
* Get Social user information
*/
async getSocialUserInfo(accessToken, fields = "id,name,picture") {
try {
const url = this.endpoints.social.me;
const params = { fields };
const result = await this.client.oauthRequestWithUrl("GET", url, params, {
access_token: accessToken,
});
if (result.error !== 0) {
throw new common_1.ZaloSDKError(result.message || "Failed to get Social user info", result.error, result);
}
if (!result.id) {
throw new common_1.ZaloSDKError("No user data received from Social API", -1);
}
return {
id: result.id,
name: result.name,
picture: result.picture,
is_sensitive: result.is_sensitive,
};
}
catch (error) {
if (error instanceof common_1.ZaloSDKError) {
throw error;
}
throw new common_1.ZaloSDKError(`Failed to get Social user info: ${error.message}`, -1, error);
}
}
/**
* Validate access token by attempting to get user info
*/
async validateAccessToken(accessToken, scope = auth_1.AuthScope.SOCIAL) {
try {
if (scope === auth_1.AuthScope.SOCIAL) {
const userInfo = await this.getSocialUserInfo(accessToken, "id");
return {
valid: true,
user_info: userInfo,
};
}
else {
// For OA tokens, we could try to get OA info
// This is a simplified validation
return { valid: true };
}
}
catch (error) {
return { valid: false };
}
}
/**
* Get all authentication URLs with optional PKCE support
*/
getAuthUrls(redirectUri, usePkce = false, pkce) {
const oaAuthResult = this.createOAAuthUrl(redirectUri, undefined, pkce, usePkce);
return {
oa_auth_url: oaAuthResult.url,
social_auth_url: this.createSocialAuthUrl(redirectUri, undefined, pkce),
token_url: this.endpoints.auth.socialToken,
refresh_url: this.endpoints.auth.refreshOaToken,
};
}
}
exports.AuthService = AuthService;
//# sourceMappingURL=auth.service.js.map