Skip to main content

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:

  1. Validates the JWT signature using the ALB's region-specific public key (fetched from AWS and cached)
  2. Confirms the token was signed by your load balancer, by checking the header's signer field against AMZN_OIDC_ALB_ARN
  3. Maps the decoded claims to a Django User (creating one on first login if configured)
  4. Establishes a normal Django session — groups, permissions, @login_required, and request.user all 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

# settings.py
AWS_REGION = "us-east-1"
AMZN_OIDC_ALB_ARN = (
    "arn:aws:elasticloadbalancing:us-east-1:123456789012:"
    "loadbalancer/app/my-alb/0123456789abcdef"
)

Find the ARN with aws elbv2 describe-load-balancers --query 'LoadBalancers[].LoadBalancerArn'. It is not a secret.

Setting Default Description
AWS_REGION required Region used to fetch ALB public keys
AMZN_OIDC_ALB_ARN required ARN of the load balancer allowed to sign tokens, matched against the JWT header's signer field. Accepts a list of ARNs for blue/green or multi-region deployments. Startup fails if unset.
AMZN_OIDC_ISSUER None Expected value of the iss claim. Recommended: pins tokens to your identity provider as well as your load balancer.
AMZN_OIDC_BYPASS_VALIDATION False Accept unsigned tokens — dev only. Must be an actual bool, and is refused unless DEBUG is True.
AMZN_OIDC_REVALIDATE_SECONDS 300 How long an established session stands in for a fresh authentication before the backend is consulted again. Lower values propagate deactivation and authorization changes faster at the cost of a periodic DB write; 0 revalidates every request.
AMZN_OIDC_LINK_EXISTING_USERS False Allow OIDC login to adopt a pre-existing account that has a usable password. Off by default so an IdP-supplied claim cannot name a local account (e.g. a createsuperuser admin) and log in as it.
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_REQUIRE_VERIFIED_EMAIL True Only store user.email when the token says the address is verified (email_verified true, as a bool or the string "true"). An unverified address is left unstored and any address already on the account is kept. Set to False if your identity provider does not send email_verified.
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 User rows. 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)
DEBUG = True
AMZN_OIDC_BYPASS_VALIDATION = True

Two guardrails apply, because this flag disables authentication entirely:

  • It must be a real bool. A string is rejected with ImproperlyConfigured, because settings read from the environment arrive as strings and every non-empty string is truthy in Python — AMZN_OIDC_BYPASS_VALIDATION = "false" would otherwise turn off signature verification. If you wire it to an env var, convert it explicitly: os.environ.get("AMZN_OIDC_BYPASS_VALIDATION") == "true".
  • It is refused unless DEBUG is True, so it cannot be left on in a production deployment.

Expiry is still enforced in bypass mode: an expired dev token reads as expired, and a token with no exp is rejected.

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.

Metadata

Release files for django-amzn-oidc-auth 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-amzn-oidc-auth 1.0.0
File Size Uploaded
django_amzn_oidc_auth-1.0.0.tar.gz 48.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-amzn-oidc-auth 1.0.0
File Interpreter ABI Platform
django_amzn_oidc_auth-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 62.0 kB

Release files / django_amzn_oidc_auth-1.0.0.tar.gz

Download URL django_amzn_oidc_auth-1.0.0.tar.gz
Size 48.1 kB
Tags Source
SHA-256 checksum
How to use checksums
3d00ab45a243ba3ea21e2c74814083c0ccb94a83fc3c0f3d6d0b88a211880fac
BLAKE2b-256 checksum
How to use checksums
b5892d6446dce593839b358dcdb2d60d070e1fea1c78c5317d053ec723c40c76
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","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}

Release files / django_amzn_oidc_auth-1.0.0-py3-none-any.whl

Download URL django_amzn_oidc_auth-1.0.0-py3-none-any.whl
Size 13.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
18d3da4ca804fa5b64509038dccb6eb7deda79604fb405533c5f9570a8a27615
BLAKE2b-256 checksum
How to use checksums
04fa620ac57b97276a562aa6c4aebeafefcd0af55ce3a32ddfa544b1f870eb73
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","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}

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page