Skip to main content

Using Keycloak authentication with Django

Keycloak is an open source Identity and Access Management (IAM) system. Keycloak allows you to add authentication to applications and secure services easily as it handles storing and authenticating users. Keycloak provides user federation, strong authentication, user management, fine-grained authorization, and more. In particular, it supports multi-factor authentication (MFA), and WebAuthn using mobile apps and U2F Tokens such as Yubikeys. It supports the OpenID Connect standard, which builds on the OAuth 2.0 protocol.

To deploy Keycloak on Dokku, we use dokku-keycloak. This uses the latest Quarkus build of Keycloak by using their docker image. Other published Dokku or Heroku plugins used older builds.

To authenticate using OpenID Connect, we use the mozilla-django-oidc library. Integration was mostly straightforward following their Quick Start guide. Once the changes in the Django application was complete, I found that creating a new OpenID Connect client and setting the following attributes was sufficient:

  • Client ID (e.g. my-client-id)
  • Root URL (e.g. http://localhost:8000)
  • Home URL (e.g. http://localhost:8000)
  • Valid redirect URIs (e.g. http://localhost:8000/*)
  • Valid post logout redirect URIs (e.g. + to use valid redirect URIs)
  • Client authentication (On)

These URL and URI settings need to match your environment, and a different client is required for each environment.

You then update the Django settings.py with the Client ID and Client secret obtained from the Credentials tab for the Client in Keycloak. If the Credentials tab is missing because you accepted the defaults when creating the client, you need to turn on Client authentication.

# app/settings.py
OIDC_RP_CLIENT_ID="my-client-id",
OIDC_RP_CLIENT_SECRET="client-secret",

These settings are normally provided to the application through environment variables (see 12-factor apps) to allow client credentials to be easily changed for each environment.

Exploring the claims passed to the app

In integrating Keycloak and Django, I wanted to make use of groups for permissions within Cyber Springboard. Groups, along with other user information, is passed as claims with the authentication token.

To explore the claims provided you can use the following custom OIDCAuthenticationBackend. This will print the claims to the console, using Python’s pprint module. You can of course set entry to the verify_claims(...) function as a breakpoint, and explore in your debugger.

# app/auth.py
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from pprint import pprint


class MyAuthenticationBackend(OIDCAuthenticationBackend):
    def verify_claims(self, claims):
        pprint(claims)
        return super().verify_claims(claims)
# app/settings.py
...
AUTHENTICATION_BACKENDS = (
    'app.auth.MyAuthenticationBackend',
)
...

You should see a dictionary of claims similar to that shown below printed in the console.

{'email': 'user@example.com'
 'email_verified': True,
 'family_name': 'User',
 'given_name': 'My',
 'name': 'My User',
 'preferred_username': 'user@example.com',
 'sub': '12345678-1234-1234-1234-1234567890ab',
}

Configuring groups and adding them to the token’s claims

To ensure that groups configured in Keycloak are present in the claims presented in the token, you need to do the following:

  1. Add a Client scope for groups:

    1. Client scopes -> Create client scope

      • Name: groups
      • Description: Open ID groups for a user
      • Type: Default
      • Protocol: OpenID Connect
      • Display on consent screen: Off
      • Include in token scope: On
    2. Click Save

    3. Mappers -> click Add predefined mapper

    4. Search for groups

    5. Tick groups and click Add

  2. Add the Client scope to the Client:

    1. Clients -> select client (e.g. my-client-id)
    2. Client scopes -> Add client scope
    3. Tick groups
    4. Click Add and select Default
  3. Add the Roles you wish to use:

    1. Realm roles -> click Create role
    2. Name: my-role
  4. Add the Groups you wish to use:

    1. Groups -> click Create group
    2. Name: my-group
    3. Role mapping -> Assign role
    4. Tick my-role
    5. Click Assign
  5. Assign the Group to a User:

    1. Users -> click on the user
    2. Groups -> click Join Group
    3. Tick the group
    4. Click Join

When logging into the application now, the user’s claims should include the ‘groups’ element, as follows:

{'email': 'user@example.com'
 'email_verified': True,
 'family_name': 'User',
 'given_name': 'My',
 'groups': ['default-roles-my-client-id',
            'my-group',
            'offline_access',
            'uma_authorization'],
 'name': 'My User',
 'preferred_username': 'user@example.com',
 'sub': '12345678-1234-1234-1234-1234567890ab',
}

Using group membership to control access to Django’s admin interface

Django’s built-in admin interface doesn’t know how to authenticate against Keycloak. Attempts to log-in with the current setup will fail.

We could allow a normal Django superuser or staff account created outside of Keycloak to authenticate by adding a second backend to AUTHENTICATION_BACKENDS as follows:

# app/settings.py
...
AUTHENTICATION_BACKENDS = (
    'app.auth.MyAuthenticationBackend',
    'django.contrib.auth.backends.ModelBackend',
)
...

However, it is possible to make use of groups to allow our Keycloak authenticated users to use Django’s built-in admin functionality. Let’s say wish to use the groups staff and superuser to set the User’s is_staff and is_superuser attributes. We can achieve this by updating our custom OIDCAuthenticationBackend as follows:

# app/auth.py
from mozilla_django_oidc.auth import OIDCAuthenticationBackend


def update_user_from_claims(user, claims):
    user.first_name = claims.get("given_name", '')
    user.last_name = claims.get("family_name", '')
    user.is_staff = "staff" in claims.get("groups", [])
    user.is_superuser = "superuser" in claims.get("groups", [])
    user.save()
    return user


class MyAuthenticationBackend(OIDCAuthenticationBackend):
    def create_user(self, claims):
        user = super().create_user(claims)
        user = update_user_from_claims(user, claims)
        return user

    def update_user(self, user, claims):
        user = super().update_user(user, claims)
        user = update_user_from_claims(user, claims)
        return user

To ensure the application’s permissions match the user’s current group membership, we need to set these attributes both when the user is created by create_user(...) and also whenever the user logs and update_user(...) is called.