Introduction

User management is a fundamental aspect of web application development. While a local login is the simplest method to grant users access to resources, it comes with certain limitations. If users want to access multiple services using the same credentials, a centralized authentication system is necessary.

For a diverse user base (spanning different contexts such as industry specialists, academia, and various organizations or locations) a Single Sign-On (SSO) solution is essential. Well-known identity providers (IdPs) like GitHub1 in the tech world, ORCID2 for the scientific community, and the lesser-known DNF-AAI3 in German academia enable users to reuse their credentials across services.

In this article, we will focus on integrating Django with Keycloak4, an open-source Identity and Access Management (IAM) solution that simplifies the process of adding authentication to applications with minimal effort.

The diagram below illustrates how Django and Keycloak interact when implementing Single Sign-On.

  • The End-User attempts to access resources from the Service Provider (SP), which requires authentication.
  • The Identity Provider (IdP) manages user identities and handles authentication (e.g., via username and password). It issues authentication tokens for the End-User, which are then provided to the Service Provider.
  • The Service Provider (SP) offers resources to registered End-Users. Instead of handling the login directly, the SP redirects the authentication process to an IdP. The SP then consumes and verifies the authentication tokens issued by the IdP.

Although Keycloak is an Identity Provider itself, it can also integrate5 with other Identity Providers, offering users various login options.

Keycloak

Python Integration

The python-keycloak package6 provides access to the Keycloak API and is compatible with the latest Keycloak versions. To establish a connection, credentials can be obtained by configuring the admin-cli client in Keycloak.

Clients / admin-cli / Capability config

In the Credentials tab, the Client Secret can be retrieved. Client permissions are managed under Service Account Roles, where roles must be assigned based on the required API actions.

The Python setup is straight forward:

1
pip install python-keycloak
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from keycloak import KeycloakAdmin, KeycloakOpenIDConnection

def create_admin(server_url: str, realm_name: str, client_id: str, client_secret: str) -> KeycloakAdmin:
    keycloak_connection = KeycloakOpenIDConnection(
        server_url=server_url,
        realm_name=realm_name,
        client_id=client_id,
        client_secret_key=client_secret,
        verify=True,
    )
    return KeycloakAdmin(connection=keycloak_connection)


server_url = "http://127.0.0.1:8080"
realm_name = "django"
client_id = "admin-cli"
client_secret_key = "changeme"

kc_admin = create_admin(server_url, realm_name, client_id, client_secret_key)

# Example
users = kc_admin.get_users()

An indication that a permission is missing is the following error message:

1
2
3
4
KeycloakGetError: 403: {
  "error":"HTTP 403 Forbidden",
  "error_description":"For more on this error consult the server log at the debug level."
}

Django Integration

The django-allauth package7 provides various authentication solutions for Django. By configuring Keycloak as an OpenID Connect provider, user data associated with Keycloak are stored in a dedicated model (SocialAccount8), which is linked to Django’s user model.

With this setup, it becomes possible to retrieve the Keycloak user ID from the SocialAccount object associated with a Django user.

1
2
3
4
5
6
7
from django.contrib.auth import get_user_model

User = get_user_model()

def get_social_account_id(user: User) -> str:
    social_account = SocialAccount.objects.get(user_id=user.id)
    return social_account.uid  # Keycloak User Id

The obtained user id can be used with python-keycloak:

1
2
3
def find_user_by_id(admin: KeycloakAdmin, user_id: str) -> dict:
    data = admin.get_user(user_id)
    return data

Use Cases

After integrating Keycloak as a login provider for Django and setting up a Python API for Keycloak, both can work together seamlessly. The following use cases demonstrate solutions to common issues.

Update Django User

Update the data in the SocialAccount model whenever a user logs in.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver

@receiver(user_logged_in)
def update_user_on_login(sender, user, request, **kwarg):
    social_account_id = get_social_account_id(user)
    data = kc_admin.get_user(social_account_id)
    user.first_name = data['firstName']
    user.last_name = data['lastName']
    user.email = data['email']
    user.save()

Update Keycloak User

Update a user’s data in Keycloak for a specific Django user. This example demonstrates how to update the user’s email address.

1
2
3
4
5
6
7
8
social_account_id = get_social_account_id(user)
kc_admin = create_admin(server_url, realm_name, client_id, client_secret_key)
kc_user = kc_admin.get_user(social_account_id)

kc_user.update({
  'email': 'new_email@example.com'
})
kc_admin.update_user(social_account_id, payload=kc_user)

This can can be implemented in user.save() or as a post-save signal.

Delete/Disable Keycloak User

Delete or disable a Keycloak user.

1
2
3
4
5
def disable_user_by_id(admin: KeycloakAdmin, user_id: str):
    admin.disable_user(user_id)

def delete_user_by_id(admin: KeycloakAdmin, user_id: str):
    admin.delete_user(user_id)

Reset Keycloak Password

Reset the user password by sending an email

The user will receive an email with a temporary link to reset their password.

1
2
social_account_id = get_social_account_id(user)
kc_admin.send_update_account(user_id=social_account_id,  payload=['UPDATE_PASSWORD'])

Reset the user password by triggering a login action

The user has to change their password with the next login.

1
2
3
4
5
user_data = find_user_by_id(admin, user_id)
required_actions = user_data["requiredActions"]
if "UPDATE_PASSWORD" not in required_actions:
    required_actions.append("UPDATE_PASSWORD")
admin.update_user(user_id, {"requiredActions": required_actions})

Logout Keycloak User with Django Logout

Logging out from Django does not automatically log the user out of Keycloak.

A common misconception is that logging out from Django also ends the session in Keycloak. To achieve this behavior, a logout signal must be sent to Keycloak.

1
2
3
4
5
6
from django.contrib.auth.signals import user_logged_out

@receiver(user_logged_out)
def terminate_session(sender, request, user, **kwargs):
    social_account_id = get_social_account_id(user)
    keycloak_admin.user_logout(social_account_id)

Otherwise, the Keycloak session will remain active, and subsequent logins will not prompt for credentials. Users will automatically continue their previous session without being redirected to Keycloak or asked to log in again.

Conclusion

Enhancing Django’s login functionality can improve user convenience, but it also introduces additional complexity to the system’s architecture. Django offers various login options, ranging from simple methods like email-based login (django-unique-user-email9) to more complex systems (django-allauth7).

Any changes to the login process should be thoughtfully evaluated to ensure they comply with standard web practices and are customized to meet the needs of the user base.

Supplements

Example Project

The GitHub project ublabs-django-keycloak-example provides a Django project derived from the official django-allauth example. The example is extended to support a self-hosted Keycloak as a openid-connect provider.

A description of the most important configuration steps are included as well as a demonstration video of the login process.

Keycloak User Login Flow

sequenceDiagram autonumber User->>Django: Login Django-->>User: Redirect to Keycloak Login User->>Keycloak: Send authentication request to Keycloak Keycloak->>Keycloak: Authenticate Keycloak-->>Django: Redirect with authorization code Django->>Keycloak: Send token request to Keycloak Keycloak->>Django: Send access/identity token Django->>User: Display Page

The basic OIDC Login flow is described by Keycloak10 as follows:

  • A user connects to an application using a browser. […].
  • The application redirects the browser to Keycloak for authentication.
  • The application passes a callback URL as a query parameter in the browser redirect. Keycloak uses the parameter upon successful authentication.
  • Keycloak authenticates the user and creates a one-time, short-lived, temporary code.
  • Keycloak redirects to the application using the callback URL and adds the temporary code as a query parameter in the callback URL.
  • The application extracts the temporary code and makes a background REST invocation to Keycloak to exchange the code for an identity and access and refresh token. […].

  1. Authorizing OAuth apps. URL: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps (Retrieved 2024-08-19) ↩︎

  2. Sign In Using ORCID Credentials. URL: https://info.orcid.org/documentation/integration-guide/sign-in-using-orcid-credentials/ (Retrieved 2024-08-19) ↩︎

  3. Authentication and authorization infrastructure for research and education communities in Germany. URL: https://www.aai.dfn.de/index.en.html (Retrieved 2024-08-19) ↩︎

  4. OIDC auth flows - Keycloak Server Administration Guide (Version 25.0.2). URL: https://www.keycloak.org/docs/latest/server_admin/index.html#con-oidc-auth-flows_server_administration_guide (Retrieved 2024-07-25) ↩︎

  5. Integrating identity providers - Keycloak Server Administration Guide (Version 25.0.2). URL: https://www.keycloak.org/docs/latest/server_admin/#_identity_broker (Retrieved 2024-08-19) ↩︎

  6. python-keycloak. URL: https://github.com/marcospereirampj/python-keycloak (Retrieved 2024-08-01) ↩︎

  7. django-allauth. URL: https://allauth.org/ (Retrieved 2024-08-01) ↩︎ ↩︎

  8. SocialAccount Model. URL: https://github.com/pennersr/django-allauth/blob/main/allauth/socialaccount/models.py#L92 (Retrieved 2024-10-16) ↩︎

  9. django-unique-user-email. URL: https://github.com/carltongibson/django-unique-user-email (Retrieved 2024-08-01) ↩︎

  10. Keycloak Server Administration Guide (Version 25.0.2). URL: https://www.keycloak.org/docs/latest/server_admin/index.html#_oidc-auth-flows-authorization (Retrieved 2024-07-25) ↩︎