Skip to main content

Django

The following could be applied to an existing Django application, but we have chosen to use the excellent tutorial application built by Mozilla as our example. If you aren't yet familiar with Django, we encourage you to follow the tutorial there.

The Phase Two Django example is the completed code of that tutorial, from MDN's GitHub repository, with Keycloak login added. We'll clone it to get started.

A full Django example is available.

Quick Start​

To get this project up and running locally on your computer:

  1. Set up the Python development environment. The example uses Python 3.14, the version in its .python-version file, and its pinned dependencies support Python 3.10 and newer.
  2. Clone the Phase Two examples repo and start its local Keycloak, which needs Docker with the Compose plugin. Its p2examples realm has a confidential client with the ID django and the user demo / demo:
    git clone https://github.com/p2-inc/examples.git
    cd examples
    docker compose -f keycloak/docker-compose.yml up -d --wait
    To use another Keycloak instead, see the settings step below.
  3. In frameworks/django, copy .env.example to .env, create and activate a virtual environment, and install the requirements. Then set up the database, run the tests, create a local account for the admin site and start the server on port 8000:
    cd frameworks/django
    cp .env.example .env
    python3 -m venv .venv
    source .venv/bin/activate
    pip install -r requirements.txt
    python manage.py migrate
    python manage.py test
    python manage.py createsuperuser
    python manage.py runserver 8000
    On Windows, activate the virtual environment with .venv\Scripts\activate. The tests don't need Keycloak. .env turns on DJANGO_DEBUG for local development and holds the Keycloak settings, which this page explains below.
  4. Open a browser to http://localhost:8000/admin/ to open the admin site.
  5. Create a few test objects of each type.
  6. Open a tab to http://localhost:8000 to see the main site, with your new objects.

Install and configure the Django OIDC library​

Now that we've installed and configured Keycloak, we need to set up Django to replace the native authentication method provided by the framework. The first task is to install a library that is compatible with Keycloak's OIDC implementation.

The mozilla-django-oidc library provides an easy way to integrate Keycloak (or any OpenID Connect-compliant identity provider) with your Django app. It abstracts many of the complexities of integrating authentication and authorization. Here's how you can set it up:

  1. Install the Package: The example's requirements.txt pins mozilla-django-oidc==5.0.2, so the quick start already installed it, along with python-dotenv, which reads .env. In your own project, install the mozilla-django-oidc package using pip:

    pip install mozilla-django-oidc
  2. Configure Django Settings: locallibrary/settings.py reads its configuration from the environment and from .env, and sets up mozilla-django-oidc. These are the parts that matter for Keycloak:

    import os
    from pathlib import Path

    from dotenv import load_dotenv

    BASE_DIR = Path(__file__).resolve().parent.parent

    load_dotenv(BASE_DIR / ".env")

    INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "mozilla_django_oidc",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "catalog.apps.CatalogConfig",
    ]

    AUTHENTICATION_BACKENDS = [
    "locallibrary.auth.KeycloakOIDCAuthenticationBackend",
    "django.contrib.auth.backends.ModelBackend",
    ]

    LOGIN_URL = "oidc_authentication_init"
    LOGIN_REDIRECT_URL = "/"
    LOGOUT_REDIRECT_URL = "/"

    OIDC_ISSUER = os.environ.get("OIDC_ISSUER", "http://localhost:8080/auth/realms/p2examples").rstrip("/")
    OIDC_OP_AUTHORIZATION_ENDPOINT = f"{OIDC_ISSUER}/protocol/openid-connect/auth"
    OIDC_OP_TOKEN_ENDPOINT = f"{OIDC_ISSUER}/protocol/openid-connect/token"
    OIDC_OP_USER_ENDPOINT = f"{OIDC_ISSUER}/protocol/openid-connect/userinfo"
    OIDC_OP_JWKS_ENDPOINT = f"{OIDC_ISSUER}/protocol/openid-connect/certs"
    OIDC_OP_LOGOUT_ENDPOINT = f"{OIDC_ISSUER}/protocol/openid-connect/logout"
    OIDC_OP_LOGOUT_URL_METHOD = "locallibrary.auth.keycloak_logout_url"
    OIDC_RP_CLIENT_ID = os.environ.get("OIDC_RP_CLIENT_ID", "django")
    OIDC_RP_CLIENT_SECRET = os.environ.get("OIDC_RP_CLIENT_SECRET", "")
    OIDC_RP_SIGN_ALGO = "RS256"
    OIDC_RP_SCOPES = "openid email profile"
    OIDC_USE_PKCE = True
    OIDC_STORE_ID_TOKEN = True
    • mozilla_django_oidc is in INSTALLED_APPS, after django.contrib.auth.
    • AUTHENTICATION_BACKENDS puts the Keycloak backend from locallibrary/auth.py first. Use Username rather than Email explains it.
    • LOGIN_URL sends pages that need a login to mozilla-django-oidc's oidc_authentication_init view, which redirects to Keycloak.
    • The Keycloak endpoints are derived from OIDC_ISSUER, with Keycloak's paths. mozilla-django-oidc checks the ID token's RS256 signature against the keys at OIDC_OP_JWKS_ENDPOINT.
    • OIDC_RP_SCOPES asks for the email and profile scopes, which carry the user's email, name and username.
    • OIDC_USE_PKCE adds PKCE to the login, on top of the client secret.
    • OIDC_STORE_ID_TOKEN keeps the ID token in the session, where the function named in OIDC_OP_LOGOUT_URL_METHOD finds it to log out of Keycloak. Logging out explains both.

    The Keycloak values come from .env. .env.example has the values for the local Keycloak:

    DJANGO_DEBUG=True
    OIDC_ISSUER=http://localhost:8080/auth/realms/p2examples
    OIDC_RP_CLIENT_ID=django
    OIDC_RP_CLIENT_SECRET=django-local-dev-secret

    To use another Keycloak, create an OpenID Connect client in your realm with client authentication on, the standard flow enabled, http://localhost:8000/* as a valid redirect URI and + as a valid post logout redirect URI. Keep Keycloak's default profile and email client scopes on it, which provide the username, name and email claims. Then put your realm's issuer URL, such as https://<your-keycloak-host>/auth/realms/<your-realm>, in OIDC_ISSUER, the client ID in OIDC_RP_CLIENT_ID and the client secret from the client's Credentials tab in OIDC_RP_CLIENT_SECRET. Restart the server after you change .env. Variables that are already set in the environment take precedence over .env, which is ignored by git.

  3. Add URLs: locallibrary/urls.py includes the authentication URLs provided by mozilla-django-oidc:

    from django.contrib import admin
    from django.urls import include, path
    from django.views.generic import RedirectView

    urlpatterns = [
    path("", RedirectView.as_view(url="/catalog/", permanent=True)),
    path("admin/", admin.site.urls),
    path("catalog/", include("catalog.urls")),
    path("oidc/", include("mozilla_django_oidc.urls")),
    ]

    They are /oidc/authenticate/ (named oidc_authentication_init), which redirects to Keycloak, /oidc/callback/, where Keycloak sends the user back after the login, and /oidc/logout/ (named oidc_logout).

  4. Log in: Open http://localhost:8000 and click Login. Log in as demo / demo on the local Keycloak, or as a user of your realm. Back on the site, the sidebar shows your username and email.

Using it in your app​

Protect your views​

mozilla-django-oidc has no decorators of its own. Protect views with Django's login_required decorator and LoginRequiredMixin, as the example does. Because LOGIN_URL is oidc_authentication_init, they send anonymous users to /oidc/authenticate/, which redirects to Keycloak. The My Borrowed page in catalog/views.py uses the mixin:

from django.contrib.auth.mixins import LoginRequiredMixin


class LoanedBooksByUserListView(LoginRequiredMixin, generic.ListView):
"""Generic class-based view listing books on loan to current user."""
model = BookInstance
template_name = 'catalog/bookinstance_list_borrowed_user.html'
paginate_by = 10

def get_queryset(self):
return (
BookInstance.objects.filter(borrower=self.request.user)
.filter(status__exact='o')
.order_by('due_back')
)

Function-based views use the decorator, as renew_book_librarian in catalog/views.py does:

from django.contrib.auth.decorators import login_required, permission_required


@login_required
@permission_required('catalog.can_mark_returned', raise_exception=True)
def renew_book_librarian(request, pk):
"""View function for renewing a specific BookInstance by librarian."""
book_instance = get_object_or_404(BookInstance, pk=pk)

PermissionRequiredMixin, which the librarian's class-based views use, also sends anonymous users to the login.

Accessing user information​

mozilla-django-oidc logs in a regular Django user, so there is no request.oidc_user: use request.user in views, as get_queryset does above, and user in templates. The sidebar in catalog/templates/base_generic.html shows the username, the email and the logout button, or the Login link:

{% if user.is_authenticated %}
<li>User: {{ user.get_username }}</li>
<li>Email: {{ user.email }}</li>
<li><a href="{% url 'my-borrowed' %}">My Borrowed</a></li>
<form action="{% url 'oidc_logout' %}" method="post">
{% csrf_token %}
<input type="submit" value="logout">
</form>
{% else %}
<li><a href="{% url 'oidc_authentication_init' %}">Login</a></li>
{% endif %}

By default, mozilla-django-oidc looks up a Django user matching the email field to the email address returned in the user info data from Keycloak.

If a user logs into your site and doesn’t already have an account, by default, mozilla-django-oidc will create a new Django user account. It will create the User instance filling in the username (hash of the email address) and email fields.

Use Username rather than Email​

The example matches Django users to Keycloak users by username instead. Keycloak sends the username in the preferred_username claim, which is set up by default. KeycloakOIDCAuthenticationBackend in locallibrary/auth.py overrides the OIDCAuthenticationBackend class in mozilla_django_oidc.auth:

from mozilla_django_oidc.auth import OIDCAuthenticationBackend


class KeycloakOIDCAuthenticationBackend(OIDCAuthenticationBackend):
def verify_claims(self, claims):
return bool(claims.get("preferred_username"))

def filter_users_by_claims(self, claims):
return self.UserModel.objects.filter(username=claims["preferred_username"])

def create_user(self, claims):
user = self.UserModel.objects.create_user(claims["preferred_username"])
return self.update_user(user, claims)

def update_user(self, user, claims):
user.email = claims.get("email", "")
user.first_name = claims.get("given_name", "")
user.last_name = claims.get("family_name", "")
user.save()
return user
  • verify_claims rejects a login without a preferred_username claim.
  • filter_users_by_claims looks up the Django user with that username, so a Keycloak user logs in to the Django account with the same username, including one created with createsuperuser.
  • create_user creates the user on their first login, without a Django password.
  • update_user copies the email, first name and last name from Keycloak on every login.

AUTHENTICATION_BACKENDS in locallibrary/settings.py, shown above, lists this backend first. Django's ModelBackend stays after it, so the admin site still accepts the local accounts that createsuperuser creates, and the tests can log in with client.login.

Logging out​

There is no @oidc_logout decorator either. The oidc_logout view of mozilla-django-oidc, at /oidc/logout/, logs the user out, and it only accepts POST requests. The logout button in base_generic.html, shown above, is a form that posts to it.

Before it ends the Django session, the view calls the function that OIDC_OP_LOGOUT_URL_METHOD names, keycloak_logout_url in locallibrary/auth.py, and redirects to the URL it returns:

from urllib.parse import urlencode

from django.conf import settings
from django.shortcuts import resolve_url


def keycloak_logout_url(request):
redirect_url = resolve_url(settings.LOGOUT_REDIRECT_URL)
id_token = request.session.get("oidc_id_token")
if not id_token:
return redirect_url
query = urlencode(
{
"client_id": settings.OIDC_RP_CLIENT_ID,
"post_logout_redirect_uri": request.build_absolute_uri(redirect_url),
"id_token_hint": id_token,
}
)
return f"{settings.OIDC_OP_LOGOUT_ENDPOINT}?{query}"

That URL is Keycloak's end-session endpoint, with the ID token that OIDC_STORE_ID_TOKEN keeps in the session as id_token_hint. Keycloak ends its session without asking for confirmation, and redirects back to LOGOUT_REDIRECT_URL, which the client's valid post logout redirect URIs allow. A user who logged in without Keycloak, for example through the admin login form, only has their Django session ended.

Add support for Django Rest Framework​

Django REST framework (DRF) is a flexible toolkit built on top of Django, specifically designed for building RESTful APIs. The example doesn't use it, but mozilla-django-oidc supports it.

If you want DRF to authenticate users based on an OAuth access token provided in the Authorization header, you can use the DRF-specific authentication class which ships with the package.

Add this to your settings:

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'mozilla_django_oidc.contrib.drf.OIDCAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
}

Note that this only takes care of authenticating against an access token, and provides no options to create or renew tokens.

If you’ve created a custom Django OIDCAuthenticationBackend and added that to your AUTHENTICATION_BACKENDS, the DRF class should be smart enough to figure that out. Alternatively, you can manually set the OIDC backend to use:

OIDC_DRF_AUTH_BACKEND = 'locallibrary.auth.KeycloakOIDCAuthenticationBackend'