# Django

Serve every customer domain from one Django project. Replace the ALLOWED_HOSTS check with the Apexly signature and resolve the tenant from the host.

## How requests arrive

Apexly forwards each visitor's request to your origin with the original `Host` header, `X-Forwarded-Proto: https`, and `X-Apexly-Domain-Id`, `X-Apexly-App-Id`, `X-Apexly-Timestamp` and `X-Apexly-Signature`.

## Verify the signature

`X-Apexly-Signature` is the hex HMAC-SHA256 of `<timestamp>.<host>` keyed with your signing secret (`whsec_…`). This function is tested against the shared vectors on every build.

```python check="origin"
# tenants/apexly.py
import hashlib
import hmac
import re
import time


def verify_apexly_origin(host, timestamp, signature, secret, now=None) -> bool:
    """True when X-Apexly-Signature is valid for this Host (the edge signs f"{timestamp}.{host}")."""
    if not timestamp or not signature or not re.fullmatch(r"[0-9]{1,12}", timestamp):
        return False
    now = int(time.time()) if now is None else now
    if abs(now - int(timestamp)) > 300:
        return False
    name = re.sub(r":\d+$", "", host).lower()
    mac = hmac.new(secret.encode(), f"{timestamp}.{name}".encode(), hashlib.sha256)
    return hmac.compare_digest(mac.hexdigest().encode(), signature.encode())
```

## Allow your customers' hostnames

Django refuses any host missing from `ALLOWED_HOSTS` with a 400. Customer hostnames are not known in advance, so let the signature decide instead: allow every host, and put a middleware first that rejects unsigned requests for hosts you don't serve yourself, before anything reads `request.get_host()`.

```python
# settings.py
ALLOWED_HOSTS = ["*"]  # host checking is done by ApexlyHostMiddleware
OWN_HOSTS = {"app.hireflow.com"}
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
MIDDLEWARE = [
    "tenants.middleware.ApexlyHostMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # …
]
```

```python
# tenants/middleware.py
import os

from django.conf import settings
from django.http import HttpResponse

from .apexly import verify_apexly_origin
from .models import Tenant


class ApexlyHostMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        host = request.META.get("HTTP_HOST", "")
        request.tenant = None
        if host.split(":")[0] not in settings.OWN_HOSTS:
            ok = verify_apexly_origin(
                host,
                request.headers.get("X-Apexly-Timestamp"),
                request.headers.get("X-Apexly-Signature"),
                os.environ["APEXLY_SIGNING_SECRET"],
            )
            if not ok:
                return HttpResponse("Unknown host", status=421)
            request.tenant = Tenant.objects.filter(hostname=host.split(":")[0].lower()).first()
        return self.get_response(request)
```

Forms posted on a customer domain pass CSRF checks unchanged: the `Origin` matches the `Host` the visitor used, and `SECURE_PROXY_SSL_HEADER` makes the request secure.

## Receive webhooks

Verify `Apexly-Signature` against `request.body` with `verify_apexly_signature` from the [Webhooks](/docs/webhooks#verify-the-signature) page.

```python
# tenants/views.py
import json
import os

from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST


@csrf_exempt
@require_POST
def apexly_webhook(request):
    secret = os.environ["APEXLY_SIGNING_SECRET"]
    if not verify_apexly_signature(request.headers.get("Apexly-Signature"), request.body, secret):
        return HttpResponseBadRequest()
    event = json.loads(request.body)
    if event["type"] == "domain.active":
        Tenant.mark_live(event["data"]["domain"]["metadata"]["customer"])
    return HttpResponse(status=204)
```

## Try it

Run the [Quickstart](/docs/quickstart) with a test key, then use the [sandbox origin](/docs/guides/test-mode) to see the headers your app will receive.

- [Webhooks](/docs/webhooks)
- [Test mode](/docs/guides/test-mode)
- [Go guide](/docs/guides/go)
