POST

In this article we’ll cover lessons learned implementing a Keycloak authentication solution into Django Rest Framework (DRF) using the mozilla-django-oidc library. Note that this article assumes some familiarity with Django.

Requirements

These can be summarized as follows:

  1. Replace the current authentication solution with a Keycloak-based solution so that users can be authenticated and enable single sign-on between applications using different authentication providers.
  2. The solution should cover both Django (session authentication) and DRF (token authentication).
  3. It should be able to handle a dedicated Keycloak client and expandable to allow roles-based authorisation.
  4. OpenID connect preferred
  5. Any libraries used should be currently supported and widely used

Design Phase

There are many Keycloak-related Django libraries available. A small PoC was developed to look at different options and decide on the most appropriate solution. Some of the options looked at included:

  1. django-rest-framework-keycloak following broadly the approach detailed here.
  2. django-keycloak-auth using the approach defined in the repository
  3. django-oidc using the approach detailed here

Each of these caused issues with the particular implementation/configuration we were trying to introduce and would have required additional customisation but may be suitable in other cases. We ultimately settled on mozilla-django-oidc - a well supported, lightweight option which could be implemented in a simple Django application more or less according to the instructions in the documentation.

The PoC was a simple app developed according to the standard Django tutorial and then updated to add the oidc library following their instructions. After some trial and error, it was running successfully, but required a couple of additional tweaks:

In settings.py:

  1. As indicated, for openID, RS256 had to be set explicitly as the signing algorithm (OIDC_RP_SIGN_ALGO).
  2. The JWKS endpoint (OIDC_OP_JWKS_ENDPOINT) has to be set up ()
  3. The sign key (OIDC_RP_IDP_SIGN_KEY) has to be present, but be left empty so it uses the JWKS endpoint.

With these modifications it was possible to set up dedicated OIDC client realm (see OIDC Clients) in keycloak and demonstrate that basic authentication was possible. Furthermore, using the groups mapper for the realm, I was able to add a groups claim, add a user group and then add a user to the group. When the user logons on via a session, or via a JWT (JSON Web Token) the claims are returned allowing authorisation groups to be set up in keycloak - see here - and interrogated by the Django app.

Development/Implementation

Implementation to production is being phased with part one - session authentication (based on the middleware) - described here. During development some additional customisations became necessary. Fortunately, the additional configuration instructions provided enough hints to cover these use cases:

UC1: Use Username rather than Email

mozilla-django-oidc defaults to setting up Django users using the email address as the user name whereas to enable communication with an associated system, the preferred_username from keycloak was required. Fortunately this is set up by default in Keycloak as a claim. The claim can interrogated by overriding the OIDCAuthenticationBackend class in mozilla_django_oidc.auth and referring to this in AUTHENTICATION_BACKENDS as below:

In a new file auth.py, add:

# Classes to override default OIDCAuthenticationBackend (Keycloak authentication)
from mozilla_django_oidc.auth import OIDCAuthenticationBackend

class KeycloakOIDCAuthenticationBackend(OIDCAuthenticationBackend):

    def create_user(self, claims):
        """ Overrides Authentication Backend so that Django users are 
            created with the keycloak preferred_username.
            If nothing found matching the email, then try the username.
        """
        user = super(KeycloakOIDCAuthenticationBackend, self).create_user(claims)
        user.first_name = claims.get('given_name', '')
        user.last_name = claims.get('family_name', '')
        user.email = claims.get('email')
        user.username = claims.get('preferred_username')
        user.save()
        return user

    def filter_users_by_claims(self, claims):
        """ Return all users matching the specified email.
            If nothing found matching the email, then try the username
        """
        email = claims.get('email')
        preferred_username = claims.get('preferred_username')

        if not email:
            return self.UserModel.objects.none()
        users = self.UserModel.objects.filter(email__iexact=email)

        if len(users) < 1:
            if not preferred_username:
                return self.UserModel.objects.none()
            users = self.UserModel.objects.filter(username__iexact=preferred_username)
        return users

    def update_user(self, user, claims):
        user.first_name = claims.get('given_name', '')
        user.last_name = claims.get('family_name', '')
        user.email = claims.get('email')
        user.username = claims.get('preferred_username')
        user.save()
        return user

In settings.py, overide the new library you have just added in AUTHENTICATION_BACKENDS :

    # mozilla_django_oidc - Keycloak authentication
    "fragalysis.auth.KeycloakOIDCAuthenticationBackend",

UC2: Allow the option to fully log out from Keycloak.

The logout view in mozilla-django-oidc defaults to ending the Django session. We also wanted to be able to optionally end the keycloak session by use of an environment parameter in our docker configuration. This can be achieved by setting, and referring to, the OIDC_OP_LOGOUT_URL_METHOD parameter and overriding the logout view as follows:

Create a file to contain the logout view:

# Classes/Methods to override default OIDC Views (Keycloak authentication)
from mozilla_django_oidc.views import OIDCLogoutView
from django.conf import settings

def keycloak_logout(request):
    """ Ths method is used to retrieve logout endpoint to also end the 
        keycloak session as well as the Django session.
    """
    logout_endpoint = settings.OIDC_OP_LOGOUT_ENDPOINT
    return logout_endpoint + "?redirect_uri=" + \
           request.build_absolute_uri(settings.LOGOUT_REDIRECT_URL)

class LogoutView(OIDCLogoutView):
    """ Extend standard logout view to include get method (called from URL)
    """
    def get(self, request):
        return self.post(request)

Set the OIDC_OP_LOGOUT_ENDPOINT in settings.py so this can be used as an environment variable:

# Override method to also log user out from Keycloak as well as Django.
# If desired, this should be set to "<path.to.view>.keycloak_logout"
OIDC_OP_LOGOUT_URL_METHOD = os.environ.get("OIDC_OP_LOGOUT_URL_METHOD")

Also refer to your new LogoutView in your URLs file, with a line like:

# Modify in list of urls for your own implementation
   url("accounts/logout/", <path.to.view>.LogoutView.as_view(), name="keycloak_logout"),
latest posts
by year
by category