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:
-
Add a
Client scope
forgroups
:-
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
- Name:
-
Click
Save
-
Mappers -> click
Add predefined mapper
-
Search for
groups
-
Tick
groups
and clickAdd
-
-
Add the
Client scope
to theClient
:- Clients -> select client (e.g.
my-client-id
) - Client scopes -> Add client scope
- Tick
groups
- Click
Add
and selectDefault
- Clients -> select client (e.g.
-
Add the
Roles
you wish to use:- Realm roles -> click
Create role
- Name:
my-role
- Realm roles -> click
-
Add the
Groups
you wish to use:- Groups -> click
Create group
- Name:
my-group
- Role mapping ->
Assign role
- Tick
my-role
- Click
Assign
- Groups -> click
-
Assign the Group to a User:
- Users -> click on the user
- Groups -> click
Join Group
- Tick the group
- 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.