Django REST Framework Authentication with Keycloak

Guilliano Molaire Guilliano Molaire Updated March 24, 2026 11 min read

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

Step 1: Configure the Keycloak Client

In the Keycloak Admin Console:

  1. Go to Clients > Create client
  2. Set Client ID to django-api
  3. Set Client type to OpenID Connect
  4. Enable Client authentication (confidential client)
  5. Set Valid redirect URIs to http://localhost:8000/oidc/callback/
  6. Set Valid post logout redirect URIs to http://localhost:8000/
  7. Set Web origins to http://localhost:8000

After saving, copy the Client secret from the Credentials tab.

Create Roles and Mappers

  1. Under your client’s Roles tab, create staff and admin roles
  2. 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:

  1. KeycloakOIDCAuthenticationBackend extends mozilla-django-oidc’s backend for browser-based OIDC login flows. It maps Keycloak’s given_name, family_name, and role claims to Django’s User model, including is_staff and is_superuser flags.

  2. KeycloakJWTAuthentication is 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

  1. Rotate the Django SECRET_KEY and Keycloak client secret on a regular schedule. Never commit secrets to version control.

  2. Enable audit logging in Keycloak. Track authentication events for compliance and security monitoring. Our guide on Keycloak auditing best practices covers the details.

  3. Use short-lived access tokens. Configure 5-15 minute access token lifetimes in Keycloak. mozilla-django-oidc’s SessionRefresh middleware handles token refresh automatically for browser sessions.

  4. Enforce HTTPS in production. Set SECURE_SSL_REDIRECT = True, SESSION_COOKIE_SECURE = True, and CSRF_COOKIE_SECURE = True in your Django settings.

  5. Consider MFA for admin-level users. Keycloak supports conditional MFA policies that require a second factor only for users with specific roles.

  6. Limit CORS origins to your actual frontend domains. The example above uses localhost for development — replace these with your production URLs.

What’s Next

With your DRF application secured, here are some natural extensions:


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.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman