# Verifying requests

> How your origin verifies the edge's signed X-Custom-Domain-Assertion header and selects the workspace from it, with the Python SDK, a Node.js example and the exact algorithm for any language.

Source: https://customdomainapi.com/docs/verify-requests/

Every request the edge forwards to your origin carries:

```
X-Custom-Domain-Assertion: v1.<key id>.<base64url payload>.<base64url HMAC-SHA256>
```

It says which application, domain and workspace the edge routed the request
for. **Select the workspace from the verified assertion, never from the
`Host` header**: a hostname alone doesn't prove which workspace may be
served.

You need two values, both on the **Origin** page of your application: your
**application id**, and your **assertion key** (its id, starting with
`app_`, and its secret).

## Python (FastAPI, Starlette, any ASGI app)

```python
import os
from custom_domain import CustomDomainMiddleware

app.add_middleware(
    CustomDomainMiddleware,
    keys={"app_…": os.environ["CUSTOM_DOMAIN_ASSERTION_SECRET"]},
    application_id=os.environ["CUSTOM_DOMAIN_APPLICATION_ID"],
    workspace_lookup=load_workspace,   # reference -> workspace, or None
    on_missing="reject",               # "passthrough" if this app also serves its own domain
)

@app.get("/")
def home(request):
    assertion = request.state.custom_domain
    workspace = load_workspace(assertion.reference)
```

Any other Python framework:

```python
from custom_domain import WorkspaceResolver, AssertionInvalid

resolver = WorkspaceResolver(keys={"app_…": SECRET}, application_id=APPLICATION_ID)
try:
    assertion = resolver.resolve(request.headers, request.host)
except AssertionInvalid as exc:
    return forbidden(exc.code)  # missing, expired, bad_signature, wrong_application, ...
workspace = load_workspace(assertion.reference)
```

## Node.js

```js
import crypto from "node:crypto";

// keys: { "app_…": "<secret>" }; keep the previous key here during a rotation.
export function verifyAssertion(token, keys, { applicationId, host, skew = 30 }) {
  const parts = (token || "").split(".");
  if (parts.length !== 4 || parts[0] !== "v1") throw new Error("malformed");
  const [, keyId, payload, signature] = parts;
  const secret = keys[keyId];
  if (!secret) throw new Error("unknown_key");
  const expected = crypto.createHmac("sha256", secret).update(`v1.${keyId}.${payload}`).digest();
  const given = Buffer.from(signature, "base64url");
  if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) {
    throw new Error("bad_signature");
  }
  const data = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
  const now = Math.floor(Date.now() / 1000);
  if (data.iat > now + skew) throw new Error("not_yet_valid");
  if (data.exp + skew < now) throw new Error("expired");
  if (data.app !== applicationId) throw new Error("wrong_application");
  if (data.host !== host.toLowerCase().split(":")[0]) throw new Error("wrong_hostname");
  return { reference: data.ref, domainId: data.dom, hostname: data.host, requestId: data.rid };
}
```

## The algorithm, for any language

1. Read `X-Custom-Domain-Assertion`. Reject the request if it is missing.
2. Split it on `.`; require exactly four parts, the first being `v1`.
3. Look up the secret by the key id (the second part). Reject unknown ids.
4. Compute HMAC-SHA256 with that secret over `v1.<key id>.<payload>` and
   compare it, in constant time, with the base64url-decoded fourth part.
5. Decode the payload (base64url, then JSON). Reject it if `exp` plus 30
   seconds is in the past, or `iat` is more than 30 seconds in the future.
6. Require `app` to equal your application id, and `host` to equal the
   request's hostname.
7. Only then use `ref` to select the workspace.

| Payload field | Meaning |
|---|---|
| `app` | Your application id. |
| `dom` | The domain id. |
| `ref` | The workspace `reference` you registered the domain with. |
| `host` | The canonical hostname. |
| `iat`, `exp` | Issued and expires, Unix seconds (60 seconds apart). |
| `rid` | The edge's request id. |

## The workspace endpoint

Before a domain goes live, the service calls your origin through the edge:

```
GET /.well-known/custom-domain-workspace
```

Verify the assertion as above, and answer `200` with the workspace you
selected:

```json
{"reference": "ws_8f3a1c", "application": "<your application id>"}
```

Answer `404` if the workspace doesn't exist. The Python middleware does this
for you. A `workspace_mismatch` here means your origin isn't selecting the
workspace from the assertion.

## Rotating the key

On the Origin page, **Rotate the key** issues a new one that starts signing
24 hours later. Add the new key id and secret to your keyring next to the
current one; after the switch, remove the old one.

## Keep the origin behind the edge

An assertion is valid for 60 seconds and bound to one hostname and
workspace. Even so, don't let the custom-domain path of your origin be
reached directly: allow only requests with a valid assertion there, or
restrict it to the edge at the network level.
