Skip to main content

HMAC Signature Verification

Shoplazza uses HMAC-SHA256 signatures in several scenarios to allow your app to confirm that requests are indeed from Shoplazza and have not been tampered with by a man-in-the-middle:

ScenarioPurposeAlgorithm
OAuth installation / authorization callbackValidate query parameter integrityHMAC-SHA256(sorted and concatenated query, client_secret)
Webhook deliveryValidate payload integrityHMAC-SHA256(raw body, client_secret) → base64

This page serves as: A unified Chinese reference for the HMAC algorithm.

To follow steps to integrate signature verification into your code: Go to Build an App with Node.js and Express.

Common Elements

  • Algorithm: HMAC-SHA256
  • Key: Client Secret (generated in the Partner Center, only visible server-side)
  • Encoding: Output as lowercase hex digest
  • Comparison: Use time-safe comparison (e.g., Node's crypto.timingSafeEqual) to avoid timing attacks

Scenario 1: OAuth Installation / Authorization Callback Signature

Algorithm Steps

  1. Remove the hmac field from the query parameters.
  2. Sort the remaining parameters in ascending order by key.
  3. Concatenate them as key=value, separated by & (do not URL-encode).
  4. Generate an HMAC-SHA256 digest of the concatenated string using client_secret as the key, outputting a lowercase hex string.
  5. Perform a time-safe comparison with the hmac value in the request.

Example Input

Request:

GET /auth/install?hmac=c4caf9b0...&install_from=app_store&shop=xxx.myshoplaza.com&store_id=1339409

String to sign after removing hmac and sorting:

install_from=app_store&shop=xxx.myshoplaza.com&store_id=1339409

Reference Implementation

import crypto from 'crypto';

function hmacValidator(req, res, next) {
const { hmac } = req.query;
if (!hmac) {
return res.status(400).json({ message: '缺少 hmac 参数' });
}

const map = { ...req.query };
delete map.hmac;
const sortedKeys = Object.keys(map).sort();
const message = sortedKeys.map(key => `${key}=${map[key]}`).join('&');

const generatedHash = crypto
.createHmac('sha256', process.env.CLIENT_SECRET)
.update(message)
.digest('hex');

if (!secureCompare(generatedHash, hmac)) {
return res.status(400).json({ message: 'HMAC 校验失败' });
}

next();
}

function secureCompare(a, b) {
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}

You can also use the Shoplazza OAuth SDK (Go) to quickly verify the shop and hmac:

import (
co "github.com/shoplazza-os/oauth-sdk-go"
"github.com/shoplazza-os/oauth-sdk-go/shoplazza"
)

oauth := &co.Config{
ClientID: "s1Ip1WxpoEAHtPPzGiP2rK2Az-P07Nie7V97hRKigl4",
ClientSecret: "{YOUR_CLIENT_SECRET}",
Endpoint: shoplazza.Endpoint,
RedirectURI: "https://3830-43-230-206-233.ngrok.io/oauth_sdk/redirect_uri/",
Scopes: []string{"read_shop"},
}
oauth.ValidShop("xxx.myshoplaza.com") // verify shop parameter

var requestUrl = "http://example.com/some/redirect_uri?code={authorization_code}&shop={store_name}.myshoplaza.com&hmac={hmac}"
query := strings.Split(requestUrl, "?")
params, _ := url.ParseQuery(query[1])
oauth.SignatureValid(params) // verify hmac

Common Pitfalls

  • ❌ Re-URL-encoding after sorting – produces a different hash.
  • ❌ Using == for string comparison – introduces timing attack risk.
  • ❌ Leaving hmac in the parameters when signing – creates self-reference.
  • ❌ Using SHA-256 instead of HMAC-SHA256 (missing the key).

Scenario 2: Webhook Payload Signature

Webhook verification differs from Scenario 1: HMAC-SHA256 is computed on the raw request body, and the output is base64-encoded (Scenario 1 uses the sorted query string and outputs hex).

Every webhook request includes a base64-encoded X-Shoplazza-Hmac-Sha256 header, generated from the request body using your app's Client Secret. To verify, compute the HMAC digest as described below, compare it with the header value using a time-safe comparison. A match confirms the request is from Shoplazza. Best practice: perform verification before your app responds to the webhook.

Algorithm Steps

  1. Take the raw body byte stream of the request (do not parse it as JSON and then re-serialize it).
  2. Compute HMAC-SHA256 on the raw body using client_secret as the key.
  3. Base64-encode the digest.
  4. Perform a time-safe comparison with the value of the X-Shoplazza-Hmac-Sha256 request header.

Reference Implementation

require 'rubygems'
require 'base64'
require 'openssl'
require 'sinatra'

# Shoplazza's Client Secret
SECRET = 'my_secret'

helpers do
# Compute HMAC of request body with shared secret and compare with HMAC in header
def verify_webhook(data, hmac_header)
calculated_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', SECRET, data))
ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
end
end

# Respond to HTTP POST requests
post '/' do
request.body.rewind
data = request.body.read
verified = verify_webhook(data, env["X-Shoplazza-Hmac-Sha256"])
puts "Webhook verified: #{verified}"
end

Difference from Scenario 1: Scenario 1 signs the sorted query string and outputs hex; Scenario 2 signs the raw body and outputs base64, with the request header X-Shoplazza-Hmac-Sha256.