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 Key | Value Template |
|---|---|
Authorization | Bearer {{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 loggingfrom datetime import timedelta
import jwtfrom django.contrib.auth import get_user_model, login, logoutfrom 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.pyMIDDLEWARE = [ # ... "django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "yourapp.twingate_middleware.TwingateMiddleware",]TwingateMiddleware must run after SessionMiddleware and AuthenticationMiddleware, because it reads request.user and calls Django’s login()/logout(), both of which depend on the session and auth middleware.
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
| Setting | Description |
|---|---|
TWINGATE_JWKS_URL | Constant at the top of twingate_middleware.py. Set to your Twingate tenant’s JWKS endpoint: https://<your-tenant>.twingate.com/api/v1/jwk/ec |
AUDIENCE | Constant at the top of twingate_middleware.py. Set to your Twingate network name, which must match the token’s aud claim. |
Next Steps
- JWT Payload Reference for the full token structure
- Request Headers for configuring which headers the Gateway injects
- Express.js, Next.js, and Next.js + Auth.js guides for other frameworks
Last updated 3 hours ago