phpgodigital-signaturetiktok

Using TikTok's Partner API and PHP, how do you create the signature to pass with API calls?


Tiktok's API "signature" piece is a nightmare, and I can find absolutely no information from anywhere about it, other than their lame documentation linked below, where the example is written in "golang" - whatever the heck THAT is lol

I managed to put the encryption code together in two lines of PHP (see below), but the confusing part to me is Step 5 of these signing instructions where it talks about passing the body:

"If the request header content_type is not multipart/form-data, append body to the end"

Body? What body?? From the cUrl request I haven't even made yet??? I am so lost on this step, but its necessary for GET requests apparently, and probably why my signature is always coming back as invalid from the API.

https://partner.tiktokshop.com/docv2/page/64f199709495ef0281851fd0

PHP code where $input is the concatenated string as per documentation, sans the illusive 'body' part from step 5.

    $hmac = hash_hmac('sha256', $input, $secretKey, true);
    $urlSafeEncoded = urlencode(rtrim(strtr(base64_encode($hmac), '+/', '-_'), '='));

Hopefully someone out here has experience with this chaos and can understand what they mean by body.

FOLLOW UP: using a Postman example in API docs, I can get the call to work. They provide snippets of Javascript and Golang code to create the signature in Postman. I'd like to post their code, and my php code here for comparison in case anyone can see what I may be missing in my php code, please and thank you!

Javascript Signature Code (works!)

// Replace secret with your own  
var secret = "c4bb0b4d6ad7407cb53e6bc038a3074b15fc53b0"  
  
function objKeySort(obj) {  
    var newKey = Object.keys(obj).sort()  
    var newObj = {}  
    for (var i = 0; i < newKey.length; i++) {  
        newObj[newKey[i]] = obj[newKey[i]]  
    }  
    return newObj  
}  
  
function getEnvVar(k) {  
    var v = pm.variables.get(k)  
    if (v != null) {  
        return v  
    }  
    v = pm.environment.get(k)  
    if (v != null) {  
        return v  
    }  
    v = pm.globals.get(k)  
    if (v != null) {  
        return v  
    }  
    return null  
}  
  
var ts = Date.parse(new Date()) / 1000  
pm.variables.set("timestamp", ts)  
  
calSign = function(secret) {  
    var ts = getEnvVar("timestamp")  
    var queryParam = pm.request.url.query.members  
    var param = {}  
    for (var item in queryParam) {  
        if (queryParam[item].key == "timestamp") {  
            v = ts  
        } else {  
            var v = queryParam[item].value  
            if (v == null || v == "{{" + queryParam[item].key + "}}") {  
                v = getEnvVar(queryParam[item].key)  
            }  
        }  
        param[queryParam[item].key] = v  
    }  
  
    delete param["sign"];  
    delete param["access_token"]  
    var sortedObj = objKeySort(param)  
    var signstring = secret + pm.request.url.getPath()  
    for (var key in sortedObj) {  
        signstring = signstring + key + sortedObj[key]  
    }  
    signstring = signstring + secret  
    sign = CryptoJS.HmacSHA256(signstring, secret).toString()  
    return sign  
}  
  
var sign = calSign(secret)  
pm.variables.set("sign", sign)

Golang Signature Code (works!)

import (  
   "crypto/hmac"  
   "crypto/sha256"  
   "encoding/hex"  
   "sort"  
)  
/**  
** path: API path, for example /api/orders  
** queries: Extract all query param EXCEPT 'sign','access_token',query param,not body  
** secret: App secter  
**/  
func generateSHA256(path string, queries map[string]string, secret string) string{  
     
   //Reorder the params based on alphabetical order.  
   keys := make([]string, len(queries))  
   idx := 0  
   for k, _ := range queries{  
      keys[idx] = k  
      idx++  
   }  
   sort.Slice(keys, func(i, j int) bool {  
      return keys[i] < keys[j]  
   })  
     
   //Concat all the param in the format of {key}{value} and append the request path to the beginning  
   input := path  
   for _, key := range keys{  
      input = input + key + queries[key]  
   }  
     
   //Wrap string generated in up with app_secret.  
   input = secret + input + secret  
  
   //Encode the digest byte stream in hexadecimal and use sha256 to generate sign with salt(secret)  
   h := hmac.New(sha256.New, []byte(secret))  
     
   // error log  
   if _, err := h.Write([]byte(input)); err != nil{  
      // todo: log error  
      return ""  
   }  
  
   return hex.EncodeToString(h.Sum(nil))  
}

PHP Code (does not work!)


    public function encrypt_sign($vars)
    {
      $qstring = '';
      ksort($vars['qvars']);
      if (count($vars['qvars']) > 0) {
        foreach ($vars['qvars'] as $k=>$v) {
          if ($k != 'sign') {
            $qstring .= $k.$v;
          }
        }
      }

      //add body of request here!?!  what body, its a get request!
      //$qstring = $qstring.{request body};

      $qstring = $this->tiktok_app_secret . $qstring . $this->tiktok_app_secret;

      $sign = $this->encryptForUrl($qstring, $this->tiktok_app_secret);
      
      return $sign;
    }

    public function encryptForUrl($input, $secretKey)
    {
        $hmac = hash_hmac('sha256', $input, $secretKey, true);

        $urlSafeEncoded = urlencode(rtrim(strtr(base64_encode($hmac), '+/', '-_'), '='));

        return $urlSafeEncoded;
    }

Solution

  • In the original question only missing thing I saw was the "path". For example: /authorization/202309/shops

    Just in case if someone still looking for this, here is working example:

    function signature($path, $params, $app_secret, $requet_method = 'GET', $requet_type = "normal", $body = '')
    {
        $arr_params = [];
        if(!is_array($params))
        {
            parse_str($params, $arr_params);
        }
        ksort($arr_params);
    
        $input = '';
        foreach($arr_params as $key => $value)
        {
            $input .= $key . $value;
        }
    
        if ($requet_method !='GET' && $requet_type != 'multipart/form-data') {
            $input .= $body;
        }
        $input = $path . $input;
    
        $input = $app_secret . $input . $app_secret;
        
        return bin2hex(hash_hmac('sha256', $input, $app_secret, true));
    }