Skip to main content

Hướng dẫn lấy access token v4 qua Zalo API PHP

Giới thiệu về Zalo API

Zalo là ứng dụng nhắn tin và gọi điện miễn phí hoạt động trên nền tảng di động và máy tính.

Zalo được phát triển bởi đội ngũ kỹ sư của công ty VNG. Phiên bản đầu tiên được ra mắt vào ngày 08/08/2012 không nhận được sự quan tâm nhiều từ người dùng.

Zalo API được tạo ra cho phép ứng dụng của bên thứ 3 có thể tương tác với Zalo Platform và kết nối tới hơn 100 triệu user của Zalo.

Zalo API bao gồm

1. Social API

Cung cấp một cách thức để ứng dụng của bạn có thể truy cập dữ liệu trên nền tảng của Zalo. Thông qua Social API ứng dụng có thể truy vấn dữ liệu người dùng, dữ liệu bạn bè, đăng tin mới và nhiều tác vụ khác.

2. Official Account API

Official Account API cho phép doanh nghiệp quản lý tài khoản OA, quản lý người dùng quan tâm tài khoản OA và tương tác với người quan tâm OA sử dụng các mẫu thông báo được Zalo thiết lập sẵn.

3. Article API

Cung cấp một cách thức để ứng dụng của bạn có thể truy cập dữ liệu article trên nền tảng của Zalo. Thông qua Article API ứng dụng có thể tạo bài viết, chỉnh sửa, quản lý bài viết hoặc broadcast bài viết tới follower.

4. Store API (Zalo Shop)

Cung cấp một cách thức để ứng dụng của bạn có thể truy cập dữ liệu của Zalo Shop. Thông qua Store API ứng dụng có thể tạo tạo danh mục, tạo sản phẩm, quản lý sản phẩm, quản lý đơn hàng và nhiều tác vụ khác.

Tham khảo thêm tại: https://developers.zalo.me/docs

Zalo có hai loại access token, trong đó Official Account Access Token sử dụng cho các dịch vụ liên quan đến Official Account (OA, Article, Shop, ZNS API) và User Access Token dành cho tài khoản cá nhân (Social API).

Việc lấy access token cho Official Account và User là tương tự nhau, chỉ khác URL tới Zalo Service.

Dưới đây là workflow cách lấy Access token của Zalo API

Image
workflow - lấy access token từ Zalo API v4

Bước 1 - Lấy Authorization Code

Trước tiên sẽ ta tạo cặp mã code verifier (ngẫu nhiên) và code challenge để phục vụ cho phương thức PKCE theo yêu cầu bên Zalo, kèm theo một giá trị trạng thái ngẫu nhiên state ( do mình chỉnh định) để ngăn ngừa tấn công CSRF.

Giá trị code verifier và state sẽ còn được sử dụng thêm 01 lần nữa trong auth.php ngay sau đó, vì vậy ta sẽ lưu tạm chúng vào PHP session.

Từ index.php redirect tới Permission URL dưới đây để yêu cầu quyền truy cập Official Account.

 

Loại API Permission URL
Official Account API
Article API
Shop API
ZNS API
https://oauth.zaloapp.com/v4/oa/permission
Social API https://oauth.zaloapp.com/v4/permission

Tham số truyền vào query string cho URL nói trên gồm: 

Parameter Ý nghĩa
app_id ID của ứng dụng
redirect_uri Đường dẫn được cấu hình tại phần setting của Official Account đang liên kết với Ứng dụng của bạn. Ở đây là URL đầy đủ tới auth.php.
code_challenge Mã code challenge đã tạo trong index.php từ code verifier phục vụ cho phương thức PKCE.
Sẽ được trả về nguyên vẹn cho auth.php.
state Mã trạng thái state đã tạo trong index.php với mục đích ngăn tấn công CSRF.
Sẽ được trả về nguyên vẹn cho auth.php.

Ở đây giả sử ta sẽ lưu access token trong một biến PHP session là zalo_access_token, sau đó sẽ được lưu phía client trong một biến như ví dụ dưới đây là initialToken.

Bạn có thể lưu ở cookie hay local storage tùy theo thiết kế chương trình nhưng hãy thận trọng với XSS.

<?php
/**
 * index.php
 */
define( "APP_ID", "1234567890123456789" ); // FILL your Application ID here!!!
define( "APP_AUTH_URL", "https://zalo.example.com/auth.php" ); // URL to auth.php
define( "ZALO_PERMISSION_URL", "https://oauth.zaloapp.com/v4/oa/permission" ); // OA, Article, Shop, ZNS API
//define( "ZALO_PERMISSION_URL", "https://oauth.zaloapp.com/v4/permission" ); // Social API
session_start();
///// Utility functions /////
function base64url_encode($text) {
    $base64 = base64_encode($text);
    $base64 = trim($base64, "=");
    $base64url = strtr($base64, "+/", "-_");
    return $base64url;
}
function generate_state_param() {
    // a random 8 digit hex, for instance
    return bin2hex(openssl_random_pseudo_bytes(4));
}
function generate_pkce_codes() {
    $random = bin2hex(openssl_random_pseudo_bytes(32)); // a random 64-digit hex
    $code_verifier = base64url_encode(pack('H*', $random));
    $code_challenge = base64url_encode(pack('H*', hash('sha256', $code_verifier)));
    return array(
        "verifier" => $code_verifier,
        "challenge" => $code_challenge
    );
}
///// Authentication Process /////
if ( !isset($_SESSION["zalo_access_token"]) ) :
    $state = generate_state_param(); // for CSRF prevention
    // Generate the code verifier and code challenge
    $codes = generate_pkce_codes();
    // Store the request state to be checked in auth.php
    $_SESSION["zalo_auth_state"] = $state;
    // Store the code verifier to be used in auth.php
    $_SESSION["zalo_code_verifier"] = $codes["verifier"];
    $auth_uri = ZALO_PERMISSION_URL . "?" . http_build_query( array(
                     "app_id" => APP_ID,
                     "redirect_uri" => APP_AUTH_URL,
                     "code_challenge" => $codes["challenge"],
                     "state" => $state, // <- prevent CSRF
                 ) );
    header("Location: {$auth_uri}");
    exit;
endif;
// Store the INITIAL access token in a JavaScript constant.
echo "<script>const initialToken = " . json_encode( $_SESSION["zalo_access_token"] ) . ";</script>\r\n";
// Remove the access token in PHP session
unset( $_SESSION["zalo_access_token"] );
/***********************************
 *       START THE MAIN APP        *
 ***********************************/

Sau khi xác nhận quyền truy cập Official Account từ người dùng, Zalo sẽ redirect về auth.php, đồng thời truyền các tham số sau cho auth.php qua method GET.

Parameter Ý nghĩa
oa_id ID của Official Account đã cấp quyền cho Ứng dụng
code  Authorization code mà ta sẽ dùng để xác thực trong bước tiếp theo.
Code chỉ có hiệu lực trong vòng 10 phút.
code_challenge Code challenge được trả lại.
state Mã trạng thái được trả lại.

Bước 2 - Lấy Access Token

auth.php gửi Authentication code và Code verifier đến Access Token URL dưới đây để yêu cầu tạo access token qua method POST:

Loại API Access Token URL
Official Account API, Article API, Shop API, ZNS API https://oauth.zaloapp.com/v4/oa/access_token
Social API https://oauth.zaloapp.com/v4/access_token

Tham số trong HTTP header:

HTTP Header Ý nghĩa
Content-Type application/x-www-form-urlencoded để xác định dữ liệu trong POST request là kiểu query string.
secret_key Khóa bí mật của ứng dụng lấy từ trang cấu hình ứng dụng của Zalo.

Tham số trong POST parameter:

Parameter Ý nghĩa
app_id ID của ứng dụng lấy từ trang cấu hình ứng dụng của Zalo.
code  Authorization code mà ta đã lấy được ở bước trước đó.
grant_type Giá trị: authorization_code – tạo access token từ authorization code.
code_verifier Code verifier đã tạo trong index.php.

Zalo platform sẽ trả về dữ liệu kiểu JSON bao gồm 3 thông tin sau:

Parameter Ý nghĩa
access_token Mã truy cập API của Zalo platform (access token).
Hiệu lực trong vòng 1 giờ.
refresh_token Mã để cấp lại access token mới khi đáo hạn (sau 25 giờ = 90,000 giây).
Hiệu lực trong vòng 3 tháng.
Có thể dùng hoặc không tùy thiết kế ứng dụng.
expires_in Thời gian hiệu lực của access token = 90000.

Bạn hãy lưu lại mã access_token để truy cập đến API của Zalo ví dụ ở đây ta sẽ lưu vào một HTTP only cookie là zalo_access_token.

<?php
/**
 * auth.php
 */
define( "APP_ID", "1234567890123456789" ); // FILL your Application ID here!!!
define( "APP_SECRET", "012abcXYZ345defUVW78" ); // FILL your Application Secret Key here!!!
define( "ZALO_ACCESS_TOKEN_URL", "https://oauth.zaloapp.com/v4/oa/access_token" ); // OA, Article, Shop, ZNS API
//define( "ZALO_ACCESS_TOKEN_URL", "https://oauth.zaloapp.com/v4/access_token" ); // Social API
session_start();
// CSRF prevention
$is_valid = isset($_REQUEST["state"]) && isset($_SESSION["zalo_auth_state"])
        && $_SESSION["zalo_auth_state"] == $_REQUEST["state"];
if ( $is_valid ) :
    // Obtain the Access Token by performing a POST request to the Access Token URL
    $data = http_build_query( array(
                "app_id" => APP_ID,
                "code" => $_REQUEST["code"],
                "code_verifier" => $_SESSION["zalo_code_verifier"],
                "grant_type" => "authorization_code"
        ) );
    $curl = curl_init();
    curl_setopt_array($curl, array(
        CURLOPT_URL => ZALO_ACCESS_TOKEN_URL,
        CURLOPT_CUSTOMREQUEST => "POST",
        CURLOPT_HTTPHEADER => array(
                "Content-Type: application/x-www-form-urlencoded",
                "secret_key: " . APP_SECRET
            ),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_POSTFIELDS => $data,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_FAILONERROR => true,
    ) );
    $response = curl_exec($curl);
    curl_close($curl);
    $auth = json_decode( $response, true );
    // store the Access Token in a temporary PHP session variable
    $_SESSION["zalo_access_token"] = array(
        "access_token" => $auth["access_token"],
        "expires_in"   => $auth["expires_in"]
    );
    // store the Refresh Token in a secured HTTP only cookie
    $expr = time() + (3*30*24*60*60); // 3 months?
    setcookie( "zalo_refresh_token", $auth["refresh_token"],
            $expr, "/refresh",
            $_SERVER['HTTP_HOST'], true, true );
    // clean up the one-time-use state variables
    unset( $_SESSION["zalo_auth_state"] );
    unset( $_SESSION["zalo_code_verifier"] );
    // Go back to index.php
    header("Location: /index.php");
    exit;
endif;
// Otherwise response an error
http_response_code(400);
die('Bad Request');

Bước 3 - Tạo access token mới khi hết hạn - refesh token

Do access token có thời gian sống khoảng 25 giờ (90,000 giây) nên ứng dụng cần yêu cầu cấp phát một access token mới mà không phải thông qua nhiều bước như trên. Đây là lý do refresh token tồn tại.

Trong auth.php, refresh token đang được lưu trong một HTTP only cookie là zalo_refresh_token.

Khi một request bị hết hạn, Zalo sẽ trả về JSON có chứa mã lỗi (-216 / Access token không hợp lệ). Khi đó, từ phía client có thể request trang refresh.php dưới đây để lấy access token mới và lưu trữ lại.

<?php
/**
 * refresh.php
 */
define( "APP_ID", "1234567890123456789" ); // FILL your Application ID here!!!
define( "APP_SECRET", "012abcXYZ345defUVW78" ); // FILL your Application Secret Key here!!!
define( "ZALO_ACCESS_TOKEN_URL", "https://oauth.zaloapp.com/v4/oa/access_token" ); // OA, Article, Shop, ZNS API
//define( "ZALO_ACCESS_TOKEN_URL", "https://oauth.zaloapp.com/v4/access_token" ); // Social API
if ( isset($_COOKIE["zalo_refresh_token"]) ) :
    // Obtain the Access Token by performing a POST request to the Access Token URL
    $data = http_build_query( array(
                "app_id" => APP_ID,
                "refresh_token" => $_COOKIE["zalo_refresh_token"],
                "grant_type" => "refresh_token"
        ) );
    $curl = curl_init();
    curl_setopt_array($curl, array(
        CURLOPT_URL => ZALO_ACCESS_TOKEN_URL,
        CURLOPT_CUSTOMREQUEST => "POST",
        CURLOPT_HTTPHEADER => array(
                "Content-Type: application/x-www-form-urlencoded",
                "secret_key: " . APP_SECRET
            ),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_POSTFIELDS => $data,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_FAILONERROR => true,
    ) );
    $response = curl_exec($curl);
    curl_close($curl);
    $auth = json_decode( $response, true );
    // store the new Refresh Token in a secured HTTP only cookie
    $expr = time() + (3*30*24*60*60); // 3 months?
    setcookie( "zalo_refresh_token", $auth["refresh_token"], $expr, "/refresh",
            $_SERVER['HTTP_HOST'], true, true );
    
    $result = array(
        "error" => 0,
        "message" => "Success",
        "access_token" => $auth["access_token"],
        "expires_in" =>  $auth["expires_in"],
    );
else :
    $result = array(
        "error" => -1,
        "message" => "Refresh token not found",
    );
endif;
header( "Content-type: application/json; charset=utf-8" );
echo json_encode( $result );
?>

Nguồn icreativ.pro - Nguyễn Hồng Hải