# HMAC signature verification

> Verify HMAC-SHA256 signatures on Shoplazza OAuth callbacks and webhook payloads, with the algorithm steps, reference implementations, and pitfalls to avoid.

# 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 |

:::note
**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](/docs/app/getting-started/create-public-app/develop-app).
:::

## 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

**Node.js**

```javascript
import crypto from 'crypto';

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

  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 verification failed' });
  }

  next();
}

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

**Python**

```python
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)
```

**Go**

```go
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))
}
```

**Ruby**

```ruby
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:

```go
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

**Ruby (Sinatra)**

```ruby
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
```

**Node.js**

```javascript
import crypto from 'crypto';

// Use the raw body (for example via express.raw()); do not re-serialize the parsed 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));
}
```

:::note
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](/docs/app/building-blocks/authentication/oauth-flow) — Where Scenario 1 fits into the OAuth flow
- [Webhook Overview](/docs/app/building-blocks/webhooks/overview) — The context of Scenario 2 (Webhook subscription and lifecycle)
- [Build an App with Node.js and Express](/docs/app/getting-started/create-public-app/develop-app) — A complete Node.js + Express hands-on guide
