Django middleware and auth backend for OIDC authentication via AWS ALB
Project description
django-amzn-oidc-auth
Django middleware and authentication backend for apps deployed behind an AWS Application Load Balancer (ALB) with OIDC authentication enabled.
How it works
When an ALB is configured with OIDC, it handles the full OAuth2 flow and injects a signed JWT into every upstream request via the x-amzn-oidc-data header. This package:
- Validates the JWT signature using the ALB's region-specific public key (fetched from AWS and cached)
- Maps the decoded claims to a Django
User(creating one on first login if configured) - Establishes a normal Django session — groups, permissions,
@login_required, andrequest.userall work as usual
Installation
pip install django-amzn-oidc-auth
or
uv add django-amzn-oidc-auth
Setup
Add to INSTALLED_APPS, MIDDLEWARE, and AUTHENTICATION_BACKENDS:
INSTALLED_APPS = [
...
"django_amzn_oidc_auth",
]
MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django_amzn_oidc_auth.middleware.AmznOidcMiddleware",
...
]
AUTHENTICATION_BACKENDS = [
"django_amzn_oidc_auth.backends.AmznOidcAuthBackend",
"django.contrib.auth.backends.ModelBackend",
]
Settings
| Setting | Default | Description |
|---|---|---|
AWS_REGION |
required | Region used to fetch ALB public keys |
AMZN_OIDC_BYPASS_VALIDATION |
False |
Skip JWT signature check — dev only |
AMZN_OIDC_AUTO_CREATE_USERS |
True |
Create Django users on first login |
AMZN_OIDC_EXEMPT_PATHS |
[] |
Paths that skip OIDC auth (e.g. health checks) |
AMZN_OIDC_USERNAME_CLAIM |
sub |
OIDC claim to use as the Django username. Defaults to sub. Set this if your IdP uses a different stable identifier (e.g. "preferred_username"). Changing this on an existing deployment will break logins for users whose accounts were created under the old claim value. |
AMZN_OIDC_FIRST_NAME_CLAIM |
None |
OIDC claim to use as first_name. When unset, falls back to nickname → given_name → first word of name. When set, only that claim is used — no fallback. |
AMZN_OIDC_LAST_NAME_CLAIM |
None |
OIDC claim to use as last_name. When unset, falls back to family_name → second word of name. When set, only that claim is used — no fallback. |
AMZN_OIDC_AUDIENCE |
None |
Expected value of the JWT aud claim. When set, tokens whose aud does not match are rejected — recommended when multiple applications share the same ALB to prevent cross-application token replay. When unset, audience validation is skipped. |
Usage in views
Once the middleware is active, every authenticated request has a populated request.user (a standard Django User instance) and request.oidc_claims (the raw decoded JWT payload).
Function-based views
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
@login_required
def profile(request):
return JsonResponse({
"username": request.user.username,
"email": request.user.email,
})
def debug_claims(request):
# Raw OIDC payload — useful during development
return JsonResponse(request.oidc_claims)
Class-based views
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views import View
from django.http import JsonResponse
class ProfileView(LoginRequiredMixin, View):
def get(self, request):
return JsonResponse({"username": request.user.username})
LoginRequiredMixin and @login_required both work because the middleware establishes a normal Django session — the auth decorators don't know or care that authentication came from an ALB header.
Checking permissions and groups
Standard Django permission checks work unchanged:
@login_required
def admin_only(request):
if not request.user.has_perm("myapp.change_widget"):
return HttpResponseForbidden()
...
Health check / unauthenticated paths
Add paths that should bypass OIDC to AMZN_OIDC_EXEMPT_PATHS. The middleware passes these through without checking the x-amzn-oidc-data header, so load-balancer health checks and similar endpoints keep working even before a session exists:
AMZN_OIDC_EXEMPT_PATHS = ["/healthcheck/", "/readyz/"]
Accessing raw OIDC claims
request.oidc_claims contains the full decoded JWT payload from the ALB. This is useful when your OIDC provider includes custom claims (e.g. roles, tenant ID) beyond what Django's User model stores.
Authorising based on a custom claim
from django.http import HttpResponseForbidden
def admin_dashboard(request):
roles = request.oidc_claims.get("custom:roles", [])
if "admin" not in roles:
return HttpResponseForbidden()
...
Multi-tenant routing
def my_view(request):
tenant = request.oidc_claims.get("custom:tenant_id")
queryset = Widget.objects.filter(tenant=tenant)
...
Using a claim as the Django username
If your IdP uses preferred_username (or another claim) as the stable account identifier instead of sub, configure it via AMZN_OIDC_USERNAME_CLAIM:
# settings.py
AMZN_OIDC_USERNAME_CLAIM = "preferred_username"
Note: Only set this on a fresh deployment, or when you are prepared to migrate existing
Userrows. Changing the claim on an existing deployment means Django will look up users by the new claim value and won't find accounts that were created under the old one.
Enriching the user model from claims
If you need to store extra claim data on first login (e.g. a department or employee ID), subclass the backend:
from django_amzn_oidc_auth.backends import AmznOidcAuthBackend
class MyBackend(AmznOidcAuthBackend):
def _sync_user_fields(self, user, claims):
super()._sync_user_fields(user, claims)
# profile is a OneToOneField added by your app
user.profile.department = claims.get("custom:department", "")
user.profile.save()
Register MyBackend in place of (or in addition to) the default in AUTHENTICATION_BACKENDS.
Local development
In production the ALB injects the x-amzn-oidc-data header automatically. Locally there is no ALB, so set AMZN_OIDC_BYPASS_VALIDATION = True to accept unsigned tokens and inject the header yourself.
# settings.py (local only)
AMZN_OIDC_BYPASS_VALIDATION = True
Generate a token
python -c "
import jwt, time
print(jwt.encode({'sub': 'dev-user', 'email': 'dev@example.com',
'iss': 'https://example.com', 'exp': int(time.time()) + 3600},
'not-a-real-secret-dev-only-ignored', algorithm='HS256'))
"
curl
Pass the token directly as a request header:
TOKEN=$(python -c "
import jwt, time
print(jwt.encode({'sub': 'dev-user', 'email': 'dev@example.com',
'iss': 'https://example.com', 'exp': int(time.time()) + 3600},
'not-a-real-secret-dev-only-ignored', algorithm='HS256'))
")
curl -H "x-amzn-oidc-data: $TOKEN" http://localhost:8000/
Browser
Browsers don't let pages set arbitrary request headers directly. The easiest workaround is a browser extension that injects custom headers, such as ModHeader (Chrome/Firefox). Add a request header named x-amzn-oidc-data with the token as the value.
Alternatively, use a reverse proxy that injects the header for you. Both options below add the header to every request automatically, so you can browse normally without touching the extension on each page.
ngrok (if you already have it installed):
TOKEN=$(python -c "
import jwt, time
print(jwt.encode({'sub': 'dev-user', 'email': 'dev@example.com',
'iss': 'https://example.com', 'exp': int(time.time()) + 3600},
'not-a-real-secret-dev-only-ignored', algorithm='HS256'))
")
ngrok http 8000 --request-header-add "x-amzn-oidc-data:$TOKEN"
ngrok prints a public URL (e.g. https://abc123.ngrok.io) — open that in your browser.
mitmproxy (local only, no public URL):
TOKEN=$(python -c "
import jwt, time
print(jwt.encode({'sub': 'dev-user', 'email': 'dev@example.com',
'iss': 'https://example.com', 'exp': int(time.time()) + 3600},
'not-a-real-secret-dev-only-ignored', algorithm='HS256'))
")
mitmdump --mode reverse:http://localhost:8000 --listen-port 8080 \
--modify-headers "/~q/x-amzn-oidc-data/$TOKEN"
Then browse to http://localhost:8080.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_amzn_oidc_auth-1.0.0rc2.tar.gz.
File metadata
- Download URL: django_amzn_oidc_auth-1.0.0rc2.tar.gz
- Upload date:
- Size: 37.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61d1a00c102bc4038f6c188a50db4bc1ce0e58a936e8003cdabd69341fb725ca
|
|
| MD5 |
6213b7c5fd27b5b610acbe1fac39cfa4
|
|
| BLAKE2b-256 |
a394f47965f69b48199fb65d179f3265773e1c53f49afaffb4b838fc36b50237
|
File details
Details for the file django_amzn_oidc_auth-1.0.0rc2-py3-none-any.whl.
File metadata
- Download URL: django_amzn_oidc_auth-1.0.0rc2-py3-none-any.whl
- Upload date:
- Size: 8.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe6d48528f4d56a03b6f241155181e1ab3b0790e008a15e6723ab98f84d18ec0
|
|
| MD5 |
571b9353c370283f888011da9f022a33
|
|
| BLAKE2b-256 |
4bcfc9d55b71989f20bf6dc105c0cbe3b50c763188df64d7da29337cdedb2b9c
|