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, so 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. Book a demo or read about our SLA guarantees.


