Request Signing
How the Cake Mobile SDK signs its requests to the eKYC and FaceAuthen backends: HMAC-SHA256 over a timestamp and the request body, using the per-transaction JWT token as the secret key. This is a different scheme from the backend's ATV <ACCESS_ID>:<SIGNATURE> header. On FaceAuthen tenants with the device signature check enabled, the SDK also signs with a registered device key — see Device Signature.
Why It Exists
| What | Description |
|---|---|
| Authentication | Verifies the request originates from an authorized client. |
| Integrity | Ensures the request body was not tampered with in transit. |
| Replay prevention | The timestamp bounds how long a captured request stays usable. |
Signing Inputs
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
timestamp | string | Yes | — | RFC3339 timestamp, e.g. 2019-04-21T18:00:15+07:00. Sent in the X-TV-Timestamp header. |
requestBody | string | Yes | — | The raw request body. Works for JSON and for multipart form data. |
jwtToken | string | Yes | — | The token from Create Transaction, used as the HMAC secret key. |
WARNING
The JWT is the signing key, so it is a credential, not just an identifier. Never log it, never persist it beyond the transaction, and never send it to a third party.
Algorithm
StringToSign = timestamp + "\n" + hex_encode(SHA256(requestBody))
Signature = Base64(HmacSha256(JWTToken, Utf8EncodingOf(StringToSign)))- Hash the raw request body with SHA256 and hex-encode it.
- Join the timestamp and that hash with a newline.
- UTF-8 encode the result.
- Apply HMAC-SHA256 using the JWT token as the key.
- Base64 encode the digest.
JavaScript
function signRequest(requestBody, jwtToken) {
// Generate RFC3339 timestamp
const date = new Date();
const timestamp = date.toISOString();
// Generate hashed body
const hashedBody = CryptoJS.SHA256(requestBody).toString(CryptoJS.enc.Hex);
// Create string to sign
const stringToSign = `${timestamp}\n${hashedBody}`;
// Generate HMAC-SHA256 signature
const hmac = CryptoJS.HmacSHA256(stringToSign, jwtToken);
// Convert to hex and then Base64 encode
const hexHash = hmac.toString(CryptoJS.enc.Hex);
const signature = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Hex.parse(hexHash));
return {
timestamp: timestamp,
signature: signature
};
}Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"time"
)
func signRequest(requestBody, jwtToken string) (string, string, error) {
// Generate RFC3339 timestamp
timestamp := time.Now().Format(time.RFC3339)
// Generate hashed body
hashFunc := sha256.New()
if _, err := hashFunc.Write([]byte(requestBody)); err != nil {
return "", "", err
}
hashedBody := hex.EncodeToString(hashFunc.Sum(nil))
// Create string to sign
stringToSign := timestamp + "\n" + hashedBody
// Generate HMAC-SHA256 signature
h := hmac.New(sha256.New, []byte(jwtToken))
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
return timestamp, signature, nil
}Request Headers
| Header | Required | Description |
|---|---|---|
X-TV-Timestamp | Yes | The RFC3339 timestamp used in StringToSign. |
X-Signature | Yes | The Base64-encoded signature. |
Authorization | Yes | Bearer {jwt_token}. |
POST /api/face-auth HTTP/1.1
Host: <HOSTNAME>
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
X-TV-Timestamp: <TIMESTAMP>
X-Signature: <SIGNATURE>
{
"cus_user_id": "user_00000001",
"ref_id": "ref_00000001"
}Server-Side Verification
The backend verifies each request by:
- Extracting
X-TV-TimestampandX-Signaturefrom the request. - Recreating the expected signature with the same algorithm.
- Comparing with a constant-time comparison, to avoid leaking information through timing.
- Proceeding only if the signatures match.
Device Signature
On a tenant with the device signature check enabled, the Mobile SDK also signs each request it sends with the transaction token, using the device key registered through Init Signing Credential and Confirm Signing Credential. It adds three headers:
| Header | Required | Description |
|---|---|---|
Device-Id | With the signature headers | The device_id the credential was registered for. Must exactly equal the credential's stored device_id (case-sensitive). |
X-Device-Key-Id | See below | The signing key ID — the key_id from Init and Confirm. The credential must be ACTIVE. |
X-Device-Signature | See below | Compact JWS (typ: cake-request-proof+jwt, alg: ES256) produced by the SDK over the environment audience, the method, the URL path as documented (e.g. /v1/face_auth, no gateway prefix), Content-Type, body hash, token hash, issue time and a unique jti. |
POST /v1/face_auth HTTP/1.1
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
Device-Id: <DEVICE_ID>
X-Device-Key-Id: <DEVICE_KEY_ID>
X-Device-Signature: <DEVICE_SIGNATURE>The headers are required when the Device-Id sent has an ACTIVE signing credential, or when the SDK version is at or above the version from which signing is enforced. They apply to every SDK request with the transaction token: Verify Face Authen (POST /v1/face_auth), Upload Image (POST /v1/images, signed over the raw multipart body) and POST /v1/get_config.
- The request is rejected with
access_denied_exception(401100) when the signature is missing, invalid, stale or replayed, or when the credential named byX-Device-Key-Idis not ACTIVE or was registered for a differentDevice-Id. - Once a minimum SDK version is set, a request from an older SDK, or without an
Sdk-Versionheader, is rejected withsdk_version_unsupported(2009005). - A body over the size limit is rejected with
invalid_parameter_exception(4001000) when a signature is required.
WARNING
The device signature protects the request in transit; it does not make the app's view of a result trustworthy. Confirm every authentication outcome from your backend with Verify Transaction Authentication Key.
Next Steps
- Verify Face Authen — the main SDK call that carries this signature
- Create Transaction — where the JWT signing key comes from
- Init Signing Credential — register the device key the SDK signs with