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:
| Scenario | Purpose | Algorithm |
|---|---|---|
| OAuth installation / authorization callback | Validate query parameter integrity | HMAC-SHA256(sorted and concatenated query, client_secret) |
| Webhook delivery | Validate payload integrity | HMAC-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
- Remove the
hmacfield from the query parameters. - Sort the remaining parameters in ascending order by key.
- Concatenate them as
key=value, separated by&(do not URL-encode). - Generate an HMAC-SHA256 digest of the concatenated string using
client_secretas the key, outputting a lowercase hex string. - Perform a time-safe comparison with the
hmacvalue 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
- Node.js
- Python
- Go
- Ruby
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));
}
import hmac as hmac_lib
import hashlib
from urllib.parse import parse_qs
def verify_hmac(query_string: str, client_secret: str) -> bool:
params = {k: v[0] for k, v in parse_qs(query_string).items()}
received_hmac = params.pop('hmac', None)
if not received_hmac:
return False
message = '&'.join(f'{k}={params[k]}' for k in sorted(params.keys()))
generated = hmac_lib.new(
client_secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256,
).hexdigest()
return hmac_lib.compare_digest(generated, received_hmac)
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/url"
"sort"
"strings"
)
func VerifyHMAC(query url.Values, clientSecret string) bool {
received := query.Get("hmac")
if received == "" {
return false
}
query.Del("hmac")
keys := make([]string, 0, len(query))
for k := range query {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, len(keys))
for i, k := range keys {
parts[i] = k + "=" + query.Get(k)
}
message := strings.Join(parts, "&")
mac := hmac.New(sha256.New, []byte(clientSecret))
mac.Write([]byte(message))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(received))
}
def verified_hmac?(hmac)
sha256 = OpenSSL::Digest::SHA256.new
query_string = "code=1vtke5ljOOL2jPds6gM0TNCeYZDitYB&shop=simon.myshoplaza.com"
calculated_hmac = OpenSSL::HMAC.hexdigest(sha256, CLIENT_SECRET, query_string)
ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac)
end
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
hmacin 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
- Take the raw body byte stream of the request (do not parse it as JSON and then re-serialize it).
- Compute HMAC-SHA256 on the raw body using
client_secretas the key. - Base64-encode the digest.
- Perform a time-safe comparison with the value of the
X-Shoplazza-Hmac-Sha256request header.
Reference Implementation
- Ruby (Sinatra)
- Node.js
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
import crypto from 'crypto';
// 必须拿到原始 body(如用 express.raw()),不能用解析后的 JSON 重新序列化
function verifyWebhook(rawBody, hmacHeader) {
const calculated = crypto
.createHmac('sha256', process.env.CLIENT_SECRET)
.update(rawBody)
.digest('base64');
return crypto.timingSafeEqual(Buffer.from(calculated), Buffer.from(hmacHeader));
}
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.
Related
- OAuth Authentication — Where Scenario 1 fits into the OAuth flow
- Webhook Overview — The context of Scenario 2 (Webhook subscription and lifecycle)
- Build an App with Node.js and Express — A complete Node.js + Express hands-on guide