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 nicknamegiven_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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_amzn_oidc_auth-1.0.0.tar.gz (48.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_amzn_oidc_auth-1.0.0-py3-none-any.whl (13.9 kB view details)

Uploaded Python 3

File details

Details for the file django_amzn_oidc_auth-1.0.0.tar.gz.

File metadata

  • Download URL: django_amzn_oidc_auth-1.0.0.tar.gz
  • Upload date:
  • Size: 48.1 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for django_amzn_oidc_auth-1.0.0.tar.gz
Algorithm Hash digest
SHA256 3d00ab45a243ba3ea21e2c74814083c0ccb94a83fc3c0f3d6d0b88a211880fac
MD5 eb0f96e4d46b115647d50da49987dd3a
BLAKE2b-256 b5892d6446dce593839b358dcdb2d60d070e1fea1c78c5317d053ec723c40c76

See more details on using hashes here.

File details

Details for the file django_amzn_oidc_auth-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: django_amzn_oidc_auth-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 13.9 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for django_amzn_oidc_auth-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 18d3da4ca804fa5b64509038dccb6eb7deda79604fb405533c5f9570a8a27615
MD5 f7e00f52e1a573a37298f94860addc7e
BLAKE2b-256 04fa620ac57b97276a562aa6c4aebeafefcd0af55ce3a32ddfa544b1f870eb73

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page