Django Middleware

Verify Twingate Identity Firewall JWTs in a Django application.

This guide shows how to verify Twingate Identity Firewall JWTs in a Django application using PyJWT. For architecture details and the full JWT reference, see the Identity Firewall for Web Apps overview.

Prerequisites

  • Python 3.8+
  • A Django project

Configure the Gateway to Inject the JWT

The Gateway injects no headers by default. On the Web App Resource, add a request header so the Gateway sends the signed JWT on every request. The recommended convention is a Bearer token in the Authorization header:

Header KeyValue Template
AuthorizationBearer {{jwt}}

The header name and format are configurable at the Gateway/Resource level. This guide reads the JWT from Authorization using the Bearer scheme. See Request Headers for the full list of options and template variables.

Install Dependencies

pip install pyjwt[crypto]

The [crypto] extra installs the cryptography package required for signature verification.

Add the Middleware

Save this file as twingate_middleware.py in your Django app directory.

import logging
from datetime import timedelta
import jwt
from django.contrib.auth import get_user_model, login, logout
from jwt import PyJWKClient, PyJWTError
TWINGATE_JWKS_URL = "https://<your-tenant>.twingate.com/api/v1/jwk/ec"
AUDIENCE = "<your-tenant>"
class TwingateMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self._gat_verifier = TwingateVerifier(
allowed_issuers=["twingate"],
allowed_audiences=[AUDIENCE],
)
def __call__(self, request):
request.gat = None
auth_header = request.headers.get("Authorization", "")
if not auth_header:
# No Authorization header present, send to next middleware
return self.get_response(request)
parts = auth_header.split()
if len(parts) != 2 or parts[0].lower() != "bearer":
# Malformed header present, send to next middleware
return self.get_response(request)
try:
gat = self._gat_verifier.verify(parts[1])
except PyJWTError as ex:
# Token is not a valid Twingate Gateway Access Token
logging.warning("Invalid Gateway Access Token", exc_info=ex)
if gat is None:
# No valid token, let other authentication middleware handle the request.
return self.get_response(request)
request.gat = gat
gat_user = gat["user"]
# If the matching user is already logged in, there is nothing more to do.
if request.user.is_authenticated:
if request.user.get_username() == gat_user["username"]:
return self.get_response(request)
# A different user is logged in — replace the session.
logout(request)
User = get_user_model()
user, _ = User.objects.get_or_create(
username=gat_user["username"],
defaults={
"email": gat_user["email"],
"first_name": gat_user["first_name"],
"last_name": gat_user["last_name"],
},
)
login(request, user)
return self.get_response(request)
class TwingateVerifier:
def __init__(self, allowed_issuers, allowed_audiences):
self.allowed_issuers = allowed_issuers
self.allowed_audiences = allowed_audiences
self._jwks_client = PyJWKClient(
TWINGATE_JWKS_URL,
cache_jwk_set=True,
lifespan=int(timedelta(hours=24).total_seconds()),
timeout=5,
)
def verify(self, signed_jwt):
unverified = jwt.decode(signed_jwt, options={"verify_signature": False})
issuer = unverified.get("iss")
audience = unverified.get("aud")
if issuer not in self.allowed_issuers:
raise jwt.InvalidIssuerError(f"Issuer '{issuer}' not allowed")
if audience not in self.allowed_audiences:
raise jwt.InvalidAudienceError(f"Audience '{audience}' not allowed")
kid = jwt.get_unverified_header(signed_jwt).get("kid")
if not kid:
raise jwt.InvalidTokenError("Missing 'kid' in token header")
signing_key = self._jwks_client.get_signing_key(kid)
return jwt.decode(
signed_jwt,
signing_key.key,
algorithms=["ES256"],
audience=audience,
issuer=issuer,
)

Configure Django Settings

Register the middleware in your settings.py:

# settings.py
MIDDLEWARE = [
# ...
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"yourapp.twingate_middleware.TwingateMiddleware",
]

How It Works

Initialization

On startup, the middleware constructs a TwingateVerifier configured with the allowed issuer (twingate) and your Resource’s audience. The verifier lazily creates and caches a PyJWKClient per JWKS URL and keeps the fetched key set cached for 24 hours, so signing keys are not re-fetched on every request.

Request Handling

When no Authorization: Bearer header is present, or the token fails verification, the middleware passes the request through untouched. This lets other authentication middleware (and public or health-check routes) handle the request, and lets you run this middleware alongside your existing auth.

Token Verification

TwingateVerifier.verify() first decodes the token without checking the signature to read the iss and aud claims, and rejects any token whose issuer or audience is not allowed. It then reads the kid from the token header, fetches the matching signing key from the JWKS endpoint, and re-decodes the token with full signature verification using ES256, validating the issuer and audience again. Expiry (exp) is validated automatically, so expired tokens are rejected. On success it returns the decoded claims on any failure it raises a PyJWTError. The middleware then logs the error before falling through.

Session Login

After a token is verified, the middleware attaches the decoded claims to request.gat and signs the user into Django’s session, provisioning a Django user (via get_or_create) on first sight. If a different user is already signed in, it signs them out first. If the same user is already signed in, it skips straight through.

Use Identity in Views

Reading Identity

The middleware attaches the decoded token claims to request.gat. A view that returns the user info:

from django.http import JsonResponse
def me(request):
gat = getattr(request, "gat", None)
if gat is None:
return JsonResponse({"error": "Unauthorized"}, status=401)
user = gat["user"]
return JsonResponse({
"username": user["username"],
"groups": user["groups"],
})

Group-Based Authorization

A view that checks Group membership before granting access. Twingate Group names are in gat["user"]["groups"]:

def admin_dashboard(request):
gat = getattr(request, "gat", None)
if gat is None:
return JsonResponse({"error": "Unauthorized"}, status=401)
if "admin" not in gat["user"]["groups"]:
return JsonResponse({"error": "Forbidden"}, status=403)
return JsonResponse({"message": "Welcome, admin"})

Configuration

SettingDescription
TWINGATE_JWKS_URLConstant at the top of twingate_middleware.py. Set to your Twingate tenant’s JWKS endpoint: https://<your-tenant>.twingate.com/api/v1/jwk/ec
AUDIENCEConstant at the top of twingate_middleware.py. Set to your Twingate network name, which must match the token’s aud claim.

Next Steps

Last updated 3 hours ago