# Authentication Guide

# Request URL

// Depends on specific AI capability
https://rest-api.xfyun.cn/v2/***

# Signature and Authentication

API calls must sign HTTP requests. The server identifies and validates users through signatures. Users apply to the server for a credential, which is a key/secret key pair. The client concatenates Method, Accept, Date, other Header fields, and URL in a specified manner, then signs the request using a hash algorithm (such as HMAC-SHA256) and the user's secret. Finally, place the key, algorithm used, header fields involved in signing, and the calculated signature into the "Authorization" header field.

# Request Header Example:

Content-Type: application/json
Accept: application/json;version=1.0
Date: Tue, 26 Jun 2018 12:27:03 UTC
Host: "your host"
Digest: SHA-256=xxxxxxxxxxxxxxxxxxxxxxxx
Authorization: hmac api_key="your_key", algorithm="hmac-sha256", headers="host date request-line", signature="base64_digest"

# Authentication Method

The client needs to use the hmac-sha256 algorithm to calculate a digest of relevant request parameters to generate a signature, building the Authorization header. The server parses the Authorization header and calculates the signature in the same way, comparing whether the signatures match. If they match, authentication passes.

# Header Detailed Description

Signature Parameter Description
Date Request date, UTC timezone. Format example: Tue, 26 Jun 2018 12:27:03 UTC
Host Request host, this header is required for signature calculation
Authorization Authentication parameter, specific construction method as follows
Digest Body digest, calculation method is "SHA256="+sha256(${body})

# Signature Generation Formula

api_key="${api_key}", algorithm="hmac-sha256", headers="host date request-line digest", signature="${signature}"

Explanation:

  • api_key: API key applied from the open platform
  • algorithm: Signature calculation algorithm, supports hmac-sha256
  • headers: Parameters required for signature, must include at least host, date, and request-line
  • signature: Calculated signature

# Authentication Detailed Process

# 1. Build Signature Origin String (signature_origin_str)

The format of signature_origin_str is:

host: ${host}
date: ${date}
${method} ${path} HTTP/1.1
digest: ${digest}
  • host: Request host header, e.g., iat-api.xfyun.cn. Must be placed in the request header, and the host used in signature calculation must be completely consistent with the header in the HTTP message.
  • date: Current timestamp, format is Wed, 08 Jun 2022 08:12:15 UTC. Must be placed in the request header.
  • method: Request method. Supports GET, POST, DELETE, PATCH, PUT. Must be consistent with the current HTTP request method.
  • path: Request path. Does not include the query string part ('?' and following). For example, when URL is '/v2/iat?a=b&c=d', only take '/v2/iat' as the path value.
  • digest: Request body digest part, calculation method is "SHA256="+sha256(${body})

Note: There is a space after ':', '\n' is a newline character. HTTP/1.1 is the HTTP protocol version. If the client uses HTTP/1.0 protocol, the value should be changed to HTTP/1.0.

# 2. Calculate Signature

Calculation method: Use hmac-sha256 algorithm to calculate the signature of signature_origin_str, and perform base64 encoding.

signature = base64(hmac-sha256(${signature_origin_str}, ${api_secret}))

Where api_secret is the secret part of the key pair obtained from the platform side, and signature_origin_str is the parameters concatenated in the previous step.

# 3. Concatenate Authorization Header

authorization_raw = api_key="${api_key}", algorithm="hmac-sha256", headers="host date request-line digest", signature="${signature}"

# Signature Generation Example

Suppose there is the following request:

  • Request URL: http://iat-api.xfyun.cn/v2/iat
  • Request method: POST
  • User's api_key = 5ccdf2b4d1b5cdf81846697bf8bcd05d
  • api_secret = B00TFRS9KDCfTrdX5JQwhVSXaFoHLy34
  • Current time: Wed, 08 Jun 2022 09:00:06 UTC
  • Request body content: hello world

Step 1: Then signature_origin_str should be concatenated as:

host: iat-api.xfyun.cn
date: Wed, 08 Jun 2022 09:00:06 UTC
POST /v2/iat HTTP/1.1
digest: SHA256=uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=

Step 2: The signature calculated using signature_origin_str:

rRU2FA174RdsqpdxGzrLmJ6C1CPk5GgfP7bUQToxQIw=

Step 3: Then the authorization header is:

api_key="5ccdf2b4d1b5cdf81846697bf8bcd05d", algorithm="hmac-sha256", headers="host date request-line", signature="rRU2FA174RdsqpdxGzrLmJ6C1CPk5GgfP7bUQToxQIw="

Step 4: The final request headers are:

Authorization: api_key="5ccdf2b4d1b5cdf81846697bf8bcd05d", algorithm="hmac-sha256", headers="host date request-line", signature="VhEap7PkvX7ujjx8DjBtkRZFwQDIEOc62EM+M9N+pf8="
Host: iat-api.xfyun.cn
Date: Wed, 08 Jun 2022 08:12:15 UTC
Digest: SHA256=uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=

Users can use this example to verify whether their signature algorithm implementation is correct.

# Key Function Description

Function Name Description
hmac-sha256 A standard signature algorithm. Provide a key and data to be signed, and it can calculate the signature digest. The calculation result is raw bytes and should not be encoded.
base64 A standard method to encode bytes into visible strings. Note to use standard base64, not base64_url.
sha256 sha256 signature algorithm

# Authentication Failure Response Example

HTTP/1.1 403 Forbidden
Date: Thu, 06 Dec 2018 07:55:16 GMT
Content-Length: 116
Content-Type: text/plain; charset=utf-8

{
    "message": "HMAC signature does not match"
}

# Possible Causes of Authentication Failure Analysis

Users can determine where the authentication problem lies through the httpCode returned by the server and the message field in the response body.

HTTP Code Message Cause
401 Unauthorized User did not pass the Authorization header
401 HMAC signature cannot be verified, fail to retrieve credential Server cannot query the api_key. Check if the apikey is correct.
401 HMAC signature cannot be verified, enforce header 'host' not used for HMAC Authentication Server failed to parse 'Authorization' header. Check if the format of 'Authorization' header meets document requirements.
403 HMAC signature cannot be verified, a valid date or x-date header is required for HMAC Authentication Date header format does not match document requirements or time deviation from server exceeds 300s. Check if the client machine timestamp is synchronized with the internet, or if the timezone is correct.
403 not found Cannot find the request address. Check if the request address is correct.
401 HMAC signature does not match Server calculated signature does not match client calculated signature value. There may be multiple causes. Try the following solutions:

Troubleshooting Steps:

  1. Check if api_secret is correct
  2. Check if signature_origin_str format is concatenated correctly. The correct format is described in the document above. You can print parameters for comparison.
  3. Check if the hmac-sha256 calculated signature length is 44. If it is 88, the string used for base64 calculation has already been hex-encoded. You need to use raw unencoded byte stream for base64 encoding.
  4. Check if nginx proxy is used. Nginx defaults to using HTTP 1.0 for proxy requests, which causes the server to get HTTP version HTTP/1.0, resulting in signature calculation mismatch. Need to set nginx to use HTTP/1.1 protocol to request the server.
  5. If none of the above methods solve the issue, try using packet capture tools to analyze the packet. Some frameworks may send HTTP messages that differ slightly from the code, such as host, path, and HTTP version number may not match expectations. Focus on checking whether these parameters in the packet match the corresponding parameters used in code for concatenating signature_origin_str.

# Pseudocode

func assembleRequestHeader(requestUrl, method, body, apikey, apisecret) {
    url = urlparse(requestUrl)
    host = url.host
    path = url.path
    date = now().format('Tue, 26 Jun 2018 12:27:03 UTC')
    request-line = "$method $path HTTP/1.1"
    signature_headers = "host date request-line"
    signature_strs = "host: $host\ndate: $date\n$request-line"
    digest = ""
    
    if body != nil:
        signature_headers = "host date request-line digest"
        digest = "SHA-256=" + base64(sha256(body))
        signature_strs = "host: $host\ndate: $date\n$request-line\ndigest: $digest"
    else:
        signature_strs = "host: $host\ndate: $date\n$request-line"

    signature = base64(hmac-sha256(signature_strs, apisecret))

    authorization = 'api_key="$apikey", algorithm="hmac-sha256", headers="$signature_headers", signature="$signature"'

    return {
        "Host": host,
        "Date": date,
        "Digest": digest,
        "Authorization": authorization
    }
}

# Sample Code for Signature Generation

# Golang

package iflyauth

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "fmt"
    "net/url"
    "time"
)

// Build authentication headers
// @requestUrl: like http://api.xfyun.cn
// @method: GET, POST, etc....
// @body: request body
func NewAuthHeaders(requestUrl, method string, apiKey, apiSecret string, body []byte) map[string]string {
    bodySign := ""
    if body == nil {
        bodySign = sha256Base64([]byte(nil))
    } else {
        bodySign = sha256Base64(body)
    }

    bodySign = "SHA256=" + bodySign

    u, err := url.Parse(requestUrl)
    if err != nil {
        panic("parse url error" + err.Error())
    }
    host := u.Host
    date := time.Now().UTC().Format(time.RFC1123)
    if u.Path == "" {
        u.Path = "/"
    }
    requestLine := method + " " + u.Path + " HTTP/1.1"
    signUrl := fmt.Sprintf("host: %s\ndate: %s\n%s\ndigest: %s", host, date, requestLine, bodySign)

    signature := hmacSha256Base64([]byte(apiSecret), []byte(signUrl))

    authorization := fmt.Sprintf(`api_key="%s", algorithm="hmac-sha256", headers="host date request-line digest", signature="%s"`, apiKey, signature)
    return map[string]string{
        "host":          host,
        "date":          date,
        "authorization": authorization,
        "digest":        bodySign,
    }
}

func sha256Base64(b []byte) string {
    h := sha256.New()
    h.Write(b)
    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

func hmacSha256Base64(secret []byte, data []byte) string {
    h := hmac.New(sha256.New, secret)
    h.Write(data)
    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

# Java

/**
 * Calculate header parameters needed for signature (HTTP interface)
 * @param requestUrl like 'http://rest-api.xfyun.cn/v2/iat'
 * @param apiKey
 * @param apiSecret
 * @method request method POST/GET/PATCH/DELETE etc....
 * @param body HTTP request body
 * @return header map, contains all headers should be set when accessing api
 */
public static Map<String, String> assembleRequestHeader(String requestUrl, String apiKey, String apiSecret, String method, byte[] body) {
    URL url = null;
    try {
        url = new URL(requestUrl);
        // Get date
        SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
        format.setTimeZone(TimeZone.getTimeZone("UTC"));
        String date = format.format(new Date());
        // Calculate body digest (SHA256)
        MessageDigest instance = MessageDigest.getInstance("SHA-256");
        instance.update(body);
        String digest = "SHA256=" + Base64.getEncoder().encodeToString(instance.digest());
        // date = "Thu, 19 Dec 2024 07:47:57 GMT";
        String host = url.getHost();
        int port = url.getPort(); // port > 0 means url contains port
        if (port > 0) {
            host = host + ":" + port;
        }
        String path = url.getPath();
        if ("".equals(path) || path == null) {
            path = "/";
        }
        // Build parameters needed for signature calculation
        StringBuilder builder = new StringBuilder()
            .append("host: ").append(host).append("\n")
            .append("date: ").append(date).append("\n")
            .append(method).append(" ").append(path).append(" HTTP/1.1").append("\n")
            .append("digest: ").append(digest);
        Charset charset = Charset.forName("UTF-8");

        System.out.println(builder.toString());
        // Use hmac-sha256 to calculate signature
        Mac mac = Mac.getInstance("hmacsha256");
        // System.out.println(builder.toString());
        SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(charset), "hmacsha256");
        mac.init(spec);
        byte[] hexDigits = mac.doFinal(builder.toString().getBytes(charset));
        String sha = Base64.getEncoder().encodeToString(hexDigits);
        // Build header
        String authorization = String.format("hmac-auth api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line digest", sha);
        Map<String, String> header = new HashMap<String, String>();
        header.put("authorization", authorization);
        header.put("host", host);
        header.put("date", date);
        header.put("digest", digest);
        System.out.println("header " + header.toString());
        return header;
    } catch (Exception e) {
        throw new RuntimeException("assemble requestHeader error:" + e.getMessage());
    }
}

# JavaScript

function assembleRequestHeader(host, path, method, apiKey, apiSecret, body) {
    var date = new Date().toGMTString()
    var algorithm = 'hmac-sha256'
    var headers = 'host date request-line digest'
    var digest = "SHA256=" + CryptoJS.enc.Base64.stringify(CryptoJS.SHA256(body))
    var signatureOrigin = `host: ${host}\ndate: ${date}\n${method} ${path} HTTP/1.1\ndigest: ${digest}`
    var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret)
    var signature = CryptoJS.enc.Base64.stringify(signatureSha)
    var authorization = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`

    return {
        // 'Host': host,
        'X-Date': date, // In JS, may not be able to set Date header, use X-Date
        'Authorization': authorization,
        'Digest': digest,
    }
}

var headers = assembleRequestHeader('rest-api-gz.xfyun.cn', '/v2/tts', 'POST', 'xxxxxxxxxxxxxxx', 'xxxxxxxxxxxxxxx', '')

# Python 3

from datetime import datetime
from wsgiref.handlers import format_date_time
from time import mktime
import hashlib
import base64
import hmac
from urllib.parse import urlparse
import sys

# Calculate sha256 and encode to base64
def sha256base64(data):
    sha256 = hashlib.sha256()
    sha256.update(data)
    digest = base64.b64encode(sha256.digest()).decode(encoding='utf-8')
    return digest

# Build auth request url
def assemble_auth_header(requset_url, method="GET", api_key="", api_secret="", body=""):
    u = urlparse(requset_url)
    host = u.hostname
    path = u.path
    now = datetime.now()
    date = format_date_time(mktime(now.timetuple()))
    digest = "SHA256=" + sha256base64(body.encode())
    # date = "Thu, 12 Dec 2019 01:57:27 GMT"
    signature_origin = "host: {}\ndate: {}\n{} {} HTTP/1.1\ndigest: {}".format(host, date, method, path, digest)
    # print(signature_origin)
    signature_sha = hmac.new(api_secret.encode('utf-8'), signature_origin.encode('utf-8'),
                             digestmod=hashlib.sha256).digest()
    signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8')
    authorization = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % (
        api_key, "hmac-sha256", "host date request-line digest", signature_sha)
    # print(authorization_origin)
    headers = {
        "host": host,
        "date": date,
        "authorization": authorization,
        "digest": digest,
    }
    return headers

requrl = "http://rest-api.xfyun.cn/v2/itr"

import requests
import json
import time

body = {
    "common": {},
    "business": {},
    "data": {}
}
now = time.time()
bds = json.dumps(body)

headers = assemble_auth_header(requrl, method="POST", api_key="xxxxxxxxx",
                            api_secret="xxxxxxxxxxxxx", body=bds)
resp = requests.post(requrl, headers=headers, data=bds)

print(resp.status_code, resp.text)

# PHP

class http_test {
    function tocurl($url, $header, $content) {
        $ch = curl_init();
        if (substr($url, 0, 5) == 'https') {
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Skip certificate check
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);  // Check if SSL encryption algorithm exists in certificate
            curl_setopt($ch, CURLOPT_SSLVERSION, 1);
        }
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_URL, $url);
        if (is_array($header)) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
        }
        curl_setopt($ch, CURLOPT_POST, true);
        if (!empty($content)) {
            if (is_array($content)) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($content));
            } else if (is_string($content)) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
            }
        }
        $response = curl_exec($ch);
        $error = curl_error($ch);
        // var_dump($error);
        if ($error) {
            die($error);
        }
        $header = curl_getinfo($ch);

        curl_close($ch);
        $data = array('header' => $header, 'body' => $response);
        return $data;
    }

    function xfyun() {
        $app_id = "XXXXX";
        $api_sec = "XXXXXXXXX";
        $api_key = "XXXXXXXXX";
        $resource = "resource/xx";
        $url = "xx";
        $host = "xx.xxx.cn";  # Request host
        $path = "/v2/xxx";    # Request path
        
        // Body assembly
        $body = json_encode($this->getBody($app_id, $resource));

        // Assemble HTTP request headers
        // $date = gmstrftime("%a, %d %b %Y %H:%M:%S %Z", time());
        $date = gmdate('D, d M Y H:i:s') . ' GMT';

        $digestBase64 = "SHA-256=" . base64_encode(hash("sha256", $body, true));

        $builder = sprintf("host: %s\ndate: %s\nPOST %s HTTP/1.1\ndigest: %s", $host, $date, $path, $digestBase64);

        $sha = base64_encode(hash_hmac("sha256", $builder, $api_sec, true));

        $authorization = sprintf("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", $api_key, "hmac-sha256", "host date request-line digest", $sha);

        $header = [
            "Authorization: " . $authorization,
            'Content-Type: application/json',
            'Accept: application/json;version=1.0',
            'Host: rest-api.xfyun.cn',  // Replace with actual domain host
            'Date: ' . $date,
            'Digest: ' . $digestBase64
        ];

        $response = $this->tocurl($url, $header, $body);

        var_dump($response['body']);
    }
}