Ensuring Server‑to‑Server Integrity with HMAC Signatures in Django/DRF

When two servers talk to each other, how can you be sure that the request you received is really the one you sent, and that it hasn’t been altered in transit? This post walks through how to use HMAC‑based authentication in a Django/DRF environment to guarantee the integrity and trustworthiness of inter‑service requests.

In particular, we’ll cover:

  • What the technique is meant to solve
  • Which attacks it can prevent
  • Why it’s unsuitable for client‑server communication (its limitations)
  • How to implement it in a real Django/DRF project using hmac_mixin.py and class‑based views (CBVs)

What is HMAC‑Signature Authentication?



HMAC (Hash‑based Message Authentication Code) is a way to create a signature from a shared secret key and a message.

  1. Two servers (A and B) share the same secret key.
  2. Server A wants to POST to server B: * It concatenates the request body and a timestamp, then signs that string with HMAC. * The resulting signature is sent in an HTTP header.
  3. Server B recomputes the signature using the same key and compares it: * If the signatures match, the request is authentic and untampered. * If they differ, the request was altered or forged.

Thus HMAC provides integrity (no tampering) and authentication (only the holder of the secret key can produce a valid signature).

Prerequisite: the secret key must be shared securely only between servers.


What Problems Does HMAC Solve?

1. Preventing Body Tampering

If an attacker intercepts traffic, they could change the body to transfer funds to a different account or modify parameters. The receiver recomputes the HMAC; any change—even a single character—breaks the signature, so the request is rejected.

2. Blocking Server Spoofing

An attacker can’t forge a valid signature without the secret key. If the key is unknown, the receiver will detect the mismatch and reject the request, ensuring that only legitimate servers can communicate.

3. Defending Against Replay Attacks

By including a timestamp (or a nonce) in the signed payload, the receiver can reject requests that are too old or that reuse a nonce. This prevents an attacker from simply replaying a previously captured request.

Note: HMAC is an additional layer on top of HTTPS/TLS; it does not replace transport‑layer security.


Why HMAC Is Not for Client‑Server Communication



  1. Secret Key Exposure – Client code (mobile apps, SPAs, desktop apps) inevitably contains the key in the binary or JavaScript. An attacker can reverse‑engineer the app and extract the key, allowing them to forge requests.
  2. No Encryption – HMAC only verifies integrity and authenticity; it does not encrypt the body. Sensitive data must still be protected by TLS.

Therefore, HMAC is best suited for server‑to‑server or backend‑to‑backend communication where the secret key can be kept confidential.


When to Use HMAC‑Based Server‑to‑Server Auth? (Scenarios)

1. Django App → Separate DRF Auth Server

A common pattern is to have a dedicated authentication/registration service built with DRF. The main Django app posts login or token‑issuance requests to this service, signing each request with HMAC.

2. Django App → AI Inference Server (DRF/FastAPI)

The Django app handles the web front‑end while heavy AI inference runs on a separate server. The app posts inference requests to the AI server, signing them with HMAC to ensure only legitimate requests consume GPU resources.


Practical Pattern in Django/DRF: hmac_mixin.py + CBV

A clean project structure looks like this:

  1. Place hmac_mixin.py at the root of the project (or app).
  2. Extract the common HMAC signing logic and POST helper into a mixin.
  3. In any CBV that needs to send signed requests, simply inherit from the mixin.

1. hmac_mixin.py – Common Signing & POST

# hmac_mixin.py
import hmac
import hashlib
import json
import time

import requests
from django.conf import settings


class HMACRequestMixin:
    HMAC_SECRET_KEY = settings.HMAC_SECRET_KEY.encode("utf-8")
    HMAC_HEADER_NAME = "X-HMAC-Signature"
    HMAC_TIMESTAMP_HEADER = "X-HMAC-Timestamp"
    HMAC_ALGORITHM = hashlib.sha256

    def build_hmac_signature(self, body: bytes, timestamp: str) -> str:
        message = timestamp.encode("utf-8") + b"." + body
        return hmac.new(self.HMAC_SECRET_KEY, message, self.HMAC_ALGORITHM).hexdigest()

    def post_with_hmac(self, url: str, payload: dict, timeout: int = 5):
        body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
        timestamp = str(int(time.time()))
        signature = self.build_hmac_signature(body, timestamp)

        headers = {
            "Content-Type": "application/json",
            self.HMAC_HEADER_NAME: signature,
            self.HMAC_TIMESTAMP_HEADER: timestamp,
        }

        response = requests.post(url, data=body, headers=headers, timeout=timeout)
        response.raise_for_status()
        return response

2. Sending View Example

# views.py
from django.conf import settings
from django.http import JsonResponse
from django.views import View

from .hmac_mixin import HMACRequestMixin


class AIInferenceRequestView(HMACRequestMixin, View):
    def post(self, request, *args, **kwargs):
        data = json.loads(request.body.decode("utf-8"))
        payload = {
            "text": data.get("text", ""),
            "user_id": request.user.id if request.user.is_authenticated else None,
        }

        url = settings.AI_SERVER_URL

        try:
            ai_response = self.post_with_hmac(url, payload)
        except requests.RequestException as e:
            return JsonResponse({"detail": "AI server error", "error": str(e)}, status=502)

        return JsonResponse(ai_response.json(), status=ai_response.status_code)

This pattern keeps view code concise and reusable.


Receiving Side: Custom DRF Authentication

On the DRF server, create a custom authentication class that verifies the HMAC signature.

# authentication.py
import hmac
import hashlib
import time

from django.conf import settings
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed


class HMACSignatureAuthentication(BaseAuthentication):
    SECRET_KEY = settings.HMAC_SECRET_KEY.encode("utf-8")
    HMAC_HEADER_NAME = "X-HMAC-Signature"
    HMAC_TIMESTAMP_HEADER = "X-HMAC-Timestamp"
    ALGORITHM = hashlib.sha256
    MAX_SKEW_SECONDS = 60

    def _build_signature(self, body: bytes, timestamp: str) -> str:
        message = timestamp.encode("utf-8") + b"." + body
        return hmac.new(self.SECRET_KEY, message, self.ALGORITHM).hexdigest()

    def authenticate(self, request):
        signature = request.headers.get(self.HMAC_HEADER_NAME)
        timestamp = request.headers.get(self.HMAC_TIMESTAMP_HEADER)

        if not signature or not timestamp:
            raise AuthenticationFailed("Missing HMAC headers")

        try:
            ts = int(timestamp)
        except ValueError:
            raise AuthenticationFailed("Invalid timestamp")

        now = int(time.time())
        if abs(now - ts) > self.MAX_SKEW_SECONDS:
            raise AuthenticationFailed("Request timestamp too old")

        expected_signature = self._build_signature(request.body, timestamp)

        if not hmac.compare_digest(signature, expected_signature):
            raise AuthenticationFailed("Invalid HMAC signature")

        return (None, None)

Applying to a DRF View

# views.py (DRF side)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny

from .authentication import HMACSignatureAuthentication


class AIInferenceView(APIView):
    authentication_classes = [HMACSignatureAuthentication]
    permission_classes = [AllowAny]

    def post(self, request, *args, **kwargs):
        input_text = request.data.get("text", "")
        result = {"answer": f"AI result for: {input_text}"}
        return Response(result)

With this setup:

  • The sender (Django) uses HMACRequestMixin to sign requests.
  • The receiver (DRF) uses HMACSignatureAuthentication to validate them.

Summary

  • HMAC signatures give you integrity and authentication for server‑to‑server calls using a shared secret key.
  • They protect against body tampering, server spoofing, and replay attacks (when combined with timestamps or nonces).
  • They are not suitable for client‑server scenarios because the key can’t be hidden in client code.
  • In Django/DRF projects, a mixin (hmac_mixin.py) and a custom DRF authentication class provide a clean, reusable pattern for secure inter‑service communication.

image