Django REST Framework Authentication with Keycloak
Last updated: March 2026
Django REST Framework (DRF) is the go-to toolkit for building APIs in Django, and Keycloak is one of the most capable open-source identity providers available. Together, they give you a production-ready API with single sign-on, multi-factor authentication, role-based access control, and identity federation — without paying for a proprietary IAM service.
This guide covers integrating DRF with Keycloak using mozilla-django-oidc, the most widely-used and actively maintained OIDC library for Django. You’ll build a custom authentication backend, DRF permission classes, and protected API views with role-based access.
Prerequisites
- Python 3.10+
- Django 4.2+ or 5.x
- Django REST Framework 3.15+
- A running Keycloak instance (version 22+). Use our Docker Compose Generator for local development or try managed Keycloak hosting.
Step 1: Configure the Keycloak Client
In the Keycloak Admin Console:
- Go to Clients > Create client
- Set Client ID to
django-api - Set Client type to
OpenID Connect - Enable Client authentication (confidential client)
- Set Valid redirect URIs to
http://localhost:8000/oidc/callback/ - Set Valid post logout redirect URIs to
http://localhost:8000/ - Set Web origins to
http://localhost:8000
After saving, copy the Client secret from the Credentials tab.
Create Roles and Mappers
- Under your client’s Roles tab, create
staffandadminroles - Assign roles to test users via Users > Role mappings > Client roles
Make sure client roles are included in the access token. Go to Client scopes > roles > Mappers > client roles and confirm that Add to access token is enabled.
For a deeper look at Keycloak’s authorization model, read our guide on fine-grained authorization in Keycloak.
Step 2: Install Dependencies
pip install django djangorestframework mozilla-django-oidc PyJWT[crypto] python-dotenv
Or add to your requirements.txt:
Django>=5.1
djangorestframework>=3.15.0
mozilla-django-oidc[drf]>=5.0.0
PyJWT[crypto]>=2.9.0
python-dotenv>=1.0.0
The mozilla-django-oidc[drf] extra installs the DRF authentication class. PyJWT with the [crypto] extra provides RS256 support for JWT signature verification.
Step 3: Django Settings Configuration
Add the following to your settings.py:
# settings.py
import os
from dotenv import load_dotenv
load_dotenv()
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Third-party
"rest_framework",
"mozilla_django_oidc",
"corsheaders",
# Your apps
"api",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
# OIDC session refresh middleware
"mozilla_django_oidc.middleware.SessionRefresh",
]
# --- Authentication Backends ---
AUTHENTICATION_BACKENDS = [
"api.auth.KeycloakOIDCAuthenticationBackend",
"django.contrib.auth.backends.ModelBackend",
]
# --- Keycloak OIDC Configuration ---
KEYCLOAK_URL = os.environ.get("KEYCLOAK_URL", "http://localhost:8080")
KEYCLOAK_REALM = os.environ.get("KEYCLOAK_REALM", "your-realm")
KEYCLOAK_BASE = f"{KEYCLOAK_URL}/realms/{KEYCLOAK_REALM}"
OIDC_RP_CLIENT_ID = os.environ.get("KEYCLOAK_CLIENT_ID", "django-api")
OIDC_RP_CLIENT_SECRET = os.environ.get("KEYCLOAK_CLIENT_SECRET", "")
OIDC_RP_SIGN_ALGO = "RS256"
OIDC_RP_SCOPES = "openid email profile"
# OIDC endpoints (auto-derived from Keycloak base URL)
OIDC_OP_AUTHORIZATION_ENDPOINT = f"{KEYCLOAK_BASE}/protocol/openid-connect/auth"
OIDC_OP_TOKEN_ENDPOINT = f"{KEYCLOAK_BASE}/protocol/openid-connect/token"
OIDC_OP_USER_ENDPOINT = f"{KEYCLOAK_BASE}/protocol/openid-connect/userinfo"
OIDC_OP_JWKS_ENDPOINT = f"{KEYCLOAK_BASE}/protocol/openid-connect/certs"
OIDC_OP_LOGOUT_ENDPOINT = f"{KEYCLOAK_BASE}/protocol/openid-connect/logout"
# Create users in Django when they first authenticate
OIDC_CREATE_USER = True
# Use Keycloak's preferred_username for Django username
OIDC_USERNAME_ALGO = None # We handle this in the custom backend
# --- Django REST Framework ---
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"api.auth.KeycloakJWTAuthentication",
"mozilla_django_oidc.contrib.drf.OIDCAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
# --- CORS ---
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://localhost:5173",
]
CORS_ALLOW_CREDENTIALS = True
# --- Login/Logout URLs ---
LOGIN_URL = "/oidc/authenticate/"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
Create a .env file:
KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_REALM=your-realm
KEYCLOAK_CLIENT_ID=django-api
KEYCLOAK_CLIENT_SECRET=your-client-secret
DJANGO_SECRET_KEY=your-django-secret-key
The key configuration points:
OIDC_RP_SIGN_ALGO = "RS256": Keycloak uses RS256 by default. This tells mozilla-django-oidc to validate JWT signatures using public keys from the JWKS endpoint.OIDC_CREATE_USER = True: When a user authenticates via Keycloak for the first time, a Django User object is created automatically.- Three authentication classes: We stack JWT Bearer token auth (for API clients), OIDC session auth (for browser-based flows), and session auth (for the admin interface).
Step 4: Custom Authentication Backend
Create a custom backend that maps Keycloak user attributes and roles to Django users:
# api/auth.py
import jwt
from jwt import PyJWKClient
from django.conf import settings
from django.contrib.auth import get_user_model
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
User = get_user_model()
class KeycloakOIDCAuthenticationBackend(OIDCAuthenticationBackend):
"""
Custom OIDC backend that maps Keycloak claims to Django user fields.
"""
def create_user(self, claims):
"""Create a Django user from Keycloak claims."""
user = super().create_user(claims)
self.update_user_from_claims(user, claims)
return user
def update_user(self, user, claims):
"""Update an existing Django user from Keycloak claims."""
self.update_user_from_claims(user, claims)
return user
def update_user_from_claims(self, user, claims):
"""Map Keycloak claims to Django user fields."""
user.first_name = claims.get("given_name", "")
user.last_name = claims.get("family_name", "")
user.email = claims.get("email", "")
# Map Keycloak roles to Django staff/superuser flags
realm_roles = claims.get("realm_access", {}).get("roles", [])
client_roles = (
claims.get("resource_access", {})
.get(settings.OIDC_RP_CLIENT_ID, {})
.get("roles", [])
)
all_roles = set(realm_roles + client_roles)
user.is_staff = "staff" in all_roles or "admin" in all_roles
user.is_superuser = "admin" in all_roles
user.save()
def filter_users_by_claims(self, claims):
"""Find existing users by Keycloak's preferred_username."""
username = claims.get("preferred_username")
if username:
return User.objects.filter(username=username)
return self.UserModel.objects.none()
@staticmethod
def generate_username(email):
"""Use email prefix as username fallback."""
return email.split("@")[0] if email else ""
class KeycloakJWTAuthentication(BaseAuthentication):
"""
DRF authentication class that validates Keycloak JWT access tokens.
Use this for API-to-API communication where the client sends a
Bearer token directly, without going through the OIDC login flow.
"""
def __init__(self):
self._jwks_client = None
@property
def jwks_client(self):
if self._jwks_client is None:
self._jwks_client = PyJWKClient(
settings.OIDC_OP_JWKS_ENDPOINT,
cache_keys=True,
lifespan=3600,
)
return self._jwks_client
def authenticate(self, request):
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
if not auth_header.startswith("Bearer "):
return None # Let other auth classes handle it
token = auth_header[7:] # Strip "Bearer "
try:
signing_key = self.jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="account",
issuer=f"{settings.KEYCLOAK_URL}/realms/{settings.KEYCLOAK_REALM}",
options={
"verify_exp": True,
"verify_aud": True,
"verify_iss": True,
},
)
except jwt.ExpiredSignatureError:
raise AuthenticationFailed("Token has expired")
except jwt.InvalidTokenError as e:
raise AuthenticationFailed(f"Invalid token: {str(e)}")
# Find or create the Django user
user = self._get_or_create_user(payload)
user.keycloak_claims = payload # Attach claims to user object
return (user, payload)
def _get_or_create_user(self, payload):
"""Find or create a Django user from JWT claims."""
username = payload.get("preferred_username", payload["sub"])
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
if not settings.OIDC_CREATE_USER:
raise AuthenticationFailed("User does not exist")
user = User.objects.create(
username=username,
email=payload.get("email", ""),
first_name=payload.get("given_name", ""),
last_name=payload.get("family_name", ""),
)
# Update roles
realm_roles = payload.get("realm_access", {}).get("roles", [])
client_roles = (
payload.get("resource_access", {})
.get(settings.OIDC_RP_CLIENT_ID, {})
.get("roles", [])
)
all_roles = set(realm_roles + client_roles)
user.is_staff = "staff" in all_roles or "admin" in all_roles
user.is_superuser = "admin" in all_roles
user.save(update_fields=["is_staff", "is_superuser"])
return user
This module contains two classes:
-
KeycloakOIDCAuthenticationBackendextends mozilla-django-oidc’s backend for browser-based OIDC login flows. It maps Keycloak’sgiven_name,family_name, and role claims to Django’sUsermodel, includingis_staffandis_superuserflags. -
KeycloakJWTAuthenticationis a DRF authentication class for API clients sending Bearer tokens directly. It validates the JWT signature using Keycloak’s JWKS endpoint, then finds or creates a matching Django user.
Step 5: Custom DRF Permission Classes
Build permission classes that check Keycloak roles:
# api/permissions.py
from rest_framework.permissions import BasePermission
class HasKeycloakRole(BasePermission):
"""
Permission class that checks for specific Keycloak roles
in the JWT claims attached to the request user.
"""
required_roles = []
def has_permission(self, request, view):
claims = getattr(request.user, "keycloak_claims", None)
if claims is None:
# Fall back to checking auth tuple
if request.auth and isinstance(request.auth, dict):
claims = request.auth
else:
return False
realm_roles = claims.get("realm_access", {}).get("roles", [])
client_id = getattr(
__import__("django.conf", fromlist=["settings"]).settings,
"OIDC_RP_CLIENT_ID",
"",
)
client_roles = (
claims.get("resource_access", {})
.get(client_id, {})
.get("roles", [])
)
all_roles = set(realm_roles + client_roles)
return all(role in all_roles for role in self.required_roles)
class IsKeycloakStaff(HasKeycloakRole):
"""Requires the 'staff' role from Keycloak."""
required_roles = ["staff"]
class IsKeycloakAdmin(HasKeycloakRole):
"""Requires the 'admin' role from Keycloak."""
required_roles = ["admin"]
def keycloak_role_required(*roles):
"""
Factory function to create permission classes for specific roles.
Usage:
permission_classes = [keycloak_role_required("editor", "publisher")]
"""
class DynamicRolePermission(HasKeycloakRole):
required_roles = list(roles)
DynamicRolePermission.__name__ = f"Requires_{'_'.join(roles)}"
return DynamicRolePermission
The keycloak_role_required factory function lets you create role-specific permission classes inline, which keeps your views clean and readable.
Step 6: Build API Views
Create serializers and views that use the auth layer:
# api/serializers.py
from rest_framework import serializers
class UserProfileSerializer(serializers.Serializer):
sub = serializers.CharField()
email = serializers.EmailField(allow_blank=True)
username = serializers.CharField()
first_name = serializers.CharField(allow_blank=True)
last_name = serializers.CharField(allow_blank=True)
realm_roles = serializers.ListField(child=serializers.CharField())
client_roles = serializers.ListField(child=serializers.CharField())
is_staff = serializers.BooleanField()
is_superuser = serializers.BooleanField()
class ItemSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
owner = serializers.CharField()
created_at = serializers.DateTimeField()
# api/views.py
from datetime import datetime, timezone
from django.conf import settings
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from .permissions import IsKeycloakAdmin, IsKeycloakStaff, keycloak_role_required
from .serializers import UserProfileSerializer, ItemSerializer
class HealthView(APIView):
"""Public health check endpoint."""
permission_classes = [AllowAny]
def get(self, request):
return Response({"status": "healthy"})
class UserProfileView(APIView):
"""Returns the authenticated user's profile with Keycloak roles."""
permission_classes = [IsAuthenticated]
def get(self, request):
claims = getattr(request.user, "keycloak_claims", {})
if isinstance(request.auth, dict):
claims = request.auth
realm_roles = claims.get("realm_access", {}).get("roles", [])
client_roles = (
claims.get("resource_access", {})
.get(settings.OIDC_RP_CLIENT_ID, {})
.get("roles", [])
)
data = {
"sub": claims.get("sub", ""),
"email": request.user.email,
"username": request.user.username,
"first_name": request.user.first_name,
"last_name": request.user.last_name,
"realm_roles": realm_roles,
"client_roles": client_roles,
"is_staff": request.user.is_staff,
"is_superuser": request.user.is_superuser,
}
serializer = UserProfileSerializer(data)
return Response(serializer.data)
class ItemListView(APIView):
"""
List items. Requires 'staff' role.
"""
permission_classes = [IsAuthenticated, IsKeycloakStaff]
def get(self, request):
items = [
{
"id": 1,
"name": "Widget",
"owner": request.user.username,
"created_at": datetime.now(timezone.utc),
},
{
"id": 2,
"name": "Gadget",
"owner": request.user.username,
"created_at": datetime.now(timezone.utc),
},
]
serializer = ItemSerializer(items, many=True)
return Response(serializer.data)
class AdminUserListView(APIView):
"""
Admin endpoint to list Django users.
Requires the 'admin' Keycloak role.
"""
permission_classes = [IsAuthenticated, IsKeycloakAdmin]
def get(self, request):
from django.contrib.auth import get_user_model
User = get_user_model()
users = User.objects.values(
"id", "username", "email", "is_staff", "is_active",
"date_joined",
)[:100]
return Response({"users": list(users)})
# Function-based view with dynamic role check
@api_view(["GET"])
@permission_classes([IsAuthenticated, keycloak_role_required("staff")])
def protected_report(request):
"""Generate a report. Requires 'staff' role."""
return Response({
"report": "Monthly authentication summary",
"generated_by": request.user.username,
"generated_at": datetime.now(timezone.utc),
})
Step 7: URL Configuration
Wire up the URLs:
# api/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("health/", views.HealthView.as_view(), name="health"),
path("me/", views.UserProfileView.as_view(), name="user-profile"),
path("items/", views.ItemListView.as_view(), name="item-list"),
path("admin/users/", views.AdminUserListView.as_view(), name="admin-users"),
path("reports/", views.protected_report, name="protected-report"),
]
# project/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("oidc/", include("mozilla_django_oidc.urls")),
path("api/", include("api.urls")),
]
The oidc/ URL prefix handles the OIDC authentication callbacks — login initiation, callback processing, and logout.
Step 8: Run and Test
python manage.py migrate
python manage.py runserver
Get a Token
TOKEN=$(curl -s -X POST
http://localhost:8080/realms/your-realm/protocol/openid-connect/token
-d "client_id=django-api"
-d "client_secret=your-client-secret"
-d "grant_type=password"
-d "username=testuser"
-d "password=testpassword"
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
Test Endpoints
# Public health check
curl http://localhost:8000/api/health/
# User profile (any authenticated user)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/me/
# Items (requires 'staff' role)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/items/
# Admin users (requires 'admin' role)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/admin/users/
Inspect the token contents with our JWT Token Analyzer to verify roles and claims are present.
Step 9: Django Admin Integration
One of the benefits of using mozilla-django-oidc is that Keycloak users can access the Django admin interface. When a user with the admin role authenticates through Keycloak, our backend sets is_staff=True and is_superuser=True, granting full admin access.
To enable OIDC login on the admin page, add a login link:
# templates/admin/login.html
{% extends "admin/login.html" %}
{% block content %}
{{ block.super }}
<div style="text-align: center; margin-top: 20px;">
<a href="{% url 'oidc_authentication_init' %}">
Login with Keycloak
</a>
</div>
{% endblock %}
This gives your team a single identity across both the API and the Django admin panel, all managed by Keycloak.
Step 10: Keycloak Logout with Session Termination
Handle logout properly by terminating the Keycloak session:
# api/views.py (add to existing file)
from django.contrib.auth import logout
from django.shortcuts import redirect
from django.conf import settings
def keycloak_logout(request):
"""Log out of both Django and Keycloak."""
logout(request)
# Build Keycloak logout URL
logout_url = (
f"{settings.OIDC_OP_LOGOUT_ENDPOINT}"
f"?client_id={settings.OIDC_RP_CLIENT_ID}"
f"&post_logout_redirect_uri={settings.LOGOUT_REDIRECT_URL}"
)
return redirect(logout_url)
This ensures the user’s session is terminated across both your Django application and the Keycloak identity provider.
Security Considerations
-
Rotate the Django
SECRET_KEYand Keycloak client secret on a regular schedule. Never commit secrets to version control. -
Enable audit logging in Keycloak. Track authentication events for compliance and security monitoring. Our guide on Keycloak auditing best practices covers the details.
-
Use short-lived access tokens. Configure 5-15 minute access token lifetimes in Keycloak. mozilla-django-oidc’s
SessionRefreshmiddleware handles token refresh automatically for browser sessions. -
Enforce HTTPS in production. Set
SECURE_SSL_REDIRECT = True,SESSION_COOKIE_SECURE = True, andCSRF_COOKIE_SECURE = Truein your Django settings. -
Consider MFA for admin-level users. Keycloak supports conditional MFA policies that require a second factor only for users with specific roles.
-
Limit CORS origins to your actual frontend domains. The example above uses
localhostfor development — replace these with your production URLs.
What’s Next
With your DRF application secured, here are some natural extensions:
- Add SCIM provisioning for automated user lifecycle management. Test endpoints with our SCIM Endpoint Tester.
- Connect a React or Next.js frontend — see our Next.js + Keycloak guide.
- Implement identity brokering for social login or enterprise SSO.
- Explore Keycloak Organizations for multi-tenant SaaS applications.
- Review the mozilla-django-oidc documentation and the Keycloak Server Administration Guide for advanced configuration.
Skip the Keycloak ops burden. Skycloak manages your Keycloak deployment with automated backups, zero-downtime upgrades, and enterprise-grade security. See pricing or read about our SLA guarantees.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.