Django Quickstart
Integrate UniAuth with Django using mozilla-django-oidc — Mozilla's production-grade OIDC client (used by Firefox Accounts). Full discovery support, JWKS verification, back-channel logout.
Discovery-first: mozilla-django-oidc auto-fetches every endpoint from https://uniauth.id/.well-known/openid-configuration. You set the issuer; it configures the rest.
1. Install
pip install mozilla-django-oidc cryptography2. Environment
# .env
UNIAUTH_CLIENT_ID=uni_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
UNIAUTH_CLIENT_SECRET=unis_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx3. settings.py
# settings.py
import os
INSTALLED_APPS = [
# ...
'mozilla_django_oidc',
'myapp',
]
AUTHENTICATION_BACKENDS = [
'myapp.auth.UniAuthBackend', # custom — see step 5
'django.contrib.auth.backends.ModelBackend',
]
MIDDLEWARE = [
# ...
'django.contrib.auth.middleware.AuthenticationMiddleware',
'mozilla_django_oidc.middleware.SessionRefresh',
]
# UniAuth via OIDC discovery
OIDC_RP_CLIENT_ID = os.environ['UNIAUTH_CLIENT_ID']
OIDC_RP_CLIENT_SECRET = os.environ['UNIAUTH_CLIENT_SECRET']
OIDC_RP_SIGN_ALGO = 'RS256'
OIDC_OP_JWKS_ENDPOINT = 'https://uniauth.id/.well-known/jwks.json'
OIDC_OP_AUTHORIZATION_ENDPOINT = 'https://uniauth.id/api/oauth/authorize'
OIDC_OP_TOKEN_ENDPOINT = 'https://uniauth.id/api/oauth/token'
OIDC_OP_USER_ENDPOINT = 'https://uniauth.id/api/oauth/userinfo'
OIDC_OP_LOGOUT_ENDPOINT = 'https://uniauth.id/api/oauth/end-session'
# Scopes — 'groups' lets you use the OIDC groups claim for RBAC
OIDC_RP_SCOPES = 'openid profile email groups'
# Require PKCE (UniAuth enforces it)
OIDC_USE_PKCE = True
# Verify ID token locally (don't roundtrip to /userinfo unless needed)
OIDC_VERIFY_JWT = True
OIDC_CREATE_USER = True
LOGIN_URL = '/oidc/authenticate/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'Tip: for extra resilience, fetch endpoints from discovery at startup instead of hardcoding them in settings.py:
# settings.py — alternative: fetch from discovery
import requests
_disc = requests.get(
'https://uniauth.id/.well-known/openid-configuration',
timeout=5
).json()
OIDC_OP_AUTHORIZATION_ENDPOINT = _disc['authorization_endpoint']
OIDC_OP_TOKEN_ENDPOINT = _disc['token_endpoint']
OIDC_OP_USER_ENDPOINT = _disc['userinfo_endpoint']
OIDC_OP_JWKS_ENDPOINT = _disc['jwks_uri']
OIDC_OP_LOGOUT_ENDPOINT = _disc['end_session_endpoint']4. urls.py
# urls.py
from django.urls import include, path
from myapp.views import backchannel_logout
urlpatterns = [
path('oidc/', include('mozilla_django_oidc.urls')),
path('logout-webhook/', backchannel_logout, name='backchannel_logout'),
# ...
]5. Custom Backend — Map groups Claim to is_staff
# myapp/auth.py
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
class UniAuthBackend(OIDCAuthenticationBackend):
def verify_claims(self, claims):
# Require email_verified before login (no env-var allowlist)
if not claims.get('email_verified'):
return False
return super().verify_claims(claims)
def create_user(self, claims):
user = super().create_user(claims)
self._apply_groups(user, claims)
return user
def update_user(self, user, claims):
user = super().update_user(user, claims)
self._apply_groups(user, claims)
return user
def _apply_groups(self, user, claims):
groups = claims.get('groups', []) or []
# Map UniAuth groups claim to Django staff/superuser
user.is_staff = 'admin' in groups or 'staff' in groups
user.is_superuser = 'admin' in groups
# Set custom attribute if you have one
# user.uniauth_sub = claims['sub']
user.save()6. Back-channel Logout
# myapp/views.py
import requests
from functools import lru_cache
from django.contrib.sessions.models import Session
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.conf import settings
from jose import jwt
@lru_cache(maxsize=1)
def _jwks():
return requests.get('https://uniauth.id/.well-known/jwks.json', timeout=5).json()
@csrf_exempt
@require_POST
def backchannel_logout(request):
token = request.POST.get('logout_token', '')
try:
claims = jwt.decode(
token, _jwks(),
algorithms=['RS256'],
issuer='https://uniauth.id',
audience=settings.OIDC_RP_CLIENT_ID,
)
except Exception:
return HttpResponse(status=400)
# Required per OIDC Back-Channel Logout 1.0
if 'http://schemas.openid.net/event/backchannel-logout' not in (claims.get('events') or {}):
return HttpResponse(status=400)
if 'nonce' in claims:
return HttpResponse(status=400)
# Invalidate all Django sessions for this user
from django.contrib.auth import get_user_model
User = get_user_model()
try:
user = User.objects.get(username=claims['sub']) # or by your uniauth_sub field
Session.objects.filter(expire_date__gt=__import__('django').utils.timezone.now()).all()
# Simpler: bump user.last_login / clear sessions filtered by session_data contains user_id
user.sessions.all().delete() if hasattr(user, 'sessions') else None
except User.DoesNotExist:
pass
return HttpResponse(status=200)Register https://yourapp.com/logout-webhook/ as backchannel_logout_uri in the Developer Console.
7. Protecting Views
from django.contrib.auth.decorators import login_required, user_passes_test
@login_required
def dashboard(request):
return render(request, 'dashboard.html')
@user_passes_test(lambda u: u.is_staff)
def admin_panel(request):
return render(request, 'admin.html')