> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frostauth.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# Security Tips

> What Frost Auth enforces for you, what you still have to do yourself, and a worked signature check.

> [!WARNING]
>
> Frost Auth protects your licensing and authentication. **Protecting your program itself is on you.** Software running on a machine somebody else owns can always be modified by that person. Our job is to make cracking more expensive than buying, not to pretend it's impossible. We are a licensing service, not an obfuscator.

### What's already enforced for you

These are decided **server-side**, on every request so your integration is not just a local `if` check that someone can patch out:

* **The server decides.** Every activation and every heartbeat re-applies the same gates — kill switch, license state, blacklist, platform, version, and build hash from one shared module. Activation and heartbeat can never disagree about whether a license is valid.
* **Atomic device limits.** Concurrent activations cannot overshoot a license's device cap. Fifty simultaneous requests against a one-seat license grant exactly one.
* **Revocation on the next beat.** A license is re-read from the database on every heartbeat rather than trusted from a token, so a revoked key stops working within one beat.
* **Vague refusals.** An unknown key, a key belonging to another product, and a wrong password are all indistinguishable from the outside. Attackers cannot enumerate valid keys from the error responses.

In other words: your app only proceeds if the **server** says so and the server re-checks every time.

### What you should do as a developer

1. **Do not remove the checks.** Keep the refusal handling in place: if `/activate` or `/validate` returns anything other than success, stop. Don't turn a failed validation into a warning.
2. **Use HTTPS safely.** All traffic is HTTPS already; if you modify the request layer yourself, add certificate pinning so a man-in-the-middle can't substitute a fake "valid" reply.
3. **Spread out your checks.** Don't only validate at startup. Use the heartbeat call `POST /api/v1/validate` on a timer every `heartbeatSeconds` and add your own integrity checks at multiple points in your code, not just one.
4. **Do not hardcode sensitive data.** Never embed license keys or your webhook signing secret in source or shipped configs. Keep secrets in environment variables.
5. **Obfuscate your code, and turn on Build Hash checks.** Even basic obfuscation or packing slows an attacker down; anti-debug / anti-inject is recommended where possible. Then register your binary's SHA-256 under Settings → Build Hashes and enable **Check Hash**. A patched build then fails with `INVALID_HASH` even if it holds a valid key.
6. **Update your program often.** The longer a build sits unchanged, the more time an attacker has to study it. Ship updates, re-register the new build hash, and bump your version so old cracked builds fall below your `minVersion` gate and stop activating.

These don't make your program uncrackable nothing is. They make cracking it cost more than buying it.

> [!NOTE]
>
> If you're verifying licenses from **your own server** (not from an application running on the user's machine), you don't need client-side hardening. Servers can't be patched by end users, so network traffic between your server and Frost Auth doesn't require anti-tamper protection.

### Webhook signature verification — worked example

Frost Auth signs every webhook delivery so your server can prove it came from us. The `X-Frost-Signature` header arrives as `t=<unix>,v1=<hmac>`. You compute an HMAC-SHA256 of `${t}.${rawBody}` using your webhook signing secret and compare.

Your signing secret is generated automatically the first time you save a webhook URL under **Settings → General → Webhook Deliveries**. It looks like this:

```bash theme={null}
whsec_••••••••••••••••••••••••
```

Say Frost Auth delivers this payload to your endpoint:

```json theme={null}
{
  "id": "cmskeyhvo0006yk77pz214vc0",
  "event": "activation.succeeded",
  "created": "2026-08-08T12:40:00.000Z",
  "data": {
    "licenseId": "4cab2fa9-923f-42c4-9465-58b63fe68515",
    "os": "Windows",
    "appVersion": "1.4.2",
    "user": "alex.dev"
  }
}
```

And the request headers include:

```text theme={null}
X-Frost-Signature: t=1786312800,v1=0aa0ab61b54e695cbd883c8fb41db716260032e17dcfb432d888d1f1223da4b3
X-Frost-Delivery: cmskeyhvo0006yk77pz214vc0
```

The signed material is the timestamp concatenated with the raw body, separated by a dot:

```text theme={null}
1786312800.{"id":"cmskeyhvo0006yk77pz214vc0","event":"activation.succeeded","created":"2026-08-08T12:40:00.000Z","data":{"licenseId":"4cab2fa9-923f-42c4-9465-58b63fe68515","os":"Windows","appVersion":"1.4.2","user":"alex.dev"}}
```

HMAC-SHA256 that string with your secret `whsec_4a59f8cab30447e28d56870004a6`, and you get:

```text theme={null}
0aa0ab61b54e695cbd883c8fb41db716260032e17dcfb432d888d1f1223da4b3
```

That matches the `v1` value in the header. In your application, compare these using a constant-time function and reject the delivery if they don't match:

```typescript theme={null}
import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhook(rawBody: string, header: string, secret: string): boolean {
  const ts = /t=(\d+)/.exec(header)?.[1];
  const sig = /v1=([a-f0-9]+)/.exec(header)?.[1];
  if (!ts || !sig) return false;

  // Reject deliveries older than 5 minutes to kill replay windows
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(sig);
  return a.length === b.length && timingSafeEqual(a, b);
}
```

> [!WARNING]
>
> Hash the **raw request body** before your framework parses it. Most frameworks parse JSON before your handler runs, and re-stringifying can reorder keys or change spacing, either one changes the hash and every delivery will look forged. Answer `401` when verification fails.

De-duplicate on `X-Frost-Delivery`. Delivery is at least once, never exactly once. If your server accepts a payload and crashes before responding, you will receive it again. Store each delivery ID you've processed and skip duplicates.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.