In this tutorial, we'll use Keycloak for authentication and then implement Cerbos for fine-grained access control, all within a Django web application, though the same principle will work for any application. As a result we will ensure that only authenticated users can access certain parts of the application, and their actions are authorized with precision.
Prerequisites
Before beginning the integration, you will need:
Let's start by installing KeyCloak using Docker. Type the following into your terminal to start Keycloak
docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:24.0.1 start-dev
After running the command, open http://localhost:8080 in your browser. You should see the Keycloak admin page. Login with the credentials you used in the Docker command (it should be admin
for username and password).
Once logged in, we can now create a realm from the admin dashboard.
You’ll notice you're in the master realm; let's create a custom realm for our project. click on “Add realm” to create a new realm. Name the realm to match your Django project.
clientID
field being required. http://localhost:8000
.Navigate to the “roles” tab still within the clients section and click “create role.” Here you can define the necessary roles relevant to your project structure, but for the purpose of this article, we’ll keep it simple and define a few roles - admin, user and manager. Keycloak will assign these roles to users and manage them.
You can now create users by moving to the users section and adding new users. For each user, you can set up credentials and assign them roles under the “Role Mappings” tab assigning them the roles you created.
Now let's connect our KeyCloak instance to our Django application.
We’ll be using django-allauth which is a Django package that simplifies the entire process of authentication and then we can register Keycloak within it as an option for authN
$ pip install django-allauth
INSTALLED_APPS
:INSTALLED_APPS = [
# ...
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.openid_connect',
# ...
]
AUTHENTICATION_BACKENDS = [
# ...
'allauth.account.auth_backends.AuthenticationBackend',
# ...
]
SOCIALACCOUNT_PROVIDERS
setting in settings.py:SOCIALACCOUNT_PROVIDERS = {
"openid_connect": {
"APPS": [
{
"provider_id": "keycloak",
"name": "Keycloak",
"client_id": "<insert-keycloak-client-id>",
"secret": "<insert-keycloak-client-secret>",
"settings": {
"server_url": "http://keycloak:8080/realms/<your-realm>/.well-known/openid-configuration",
},
}
]
}
}
Replace <insert-keycloak-client-id>
and <insert-keycloak-client-secret>
with your actual Keycloak client ID and secret (your client ID was the name you put as your KeyCloak ID, and the secret can be found within the credentials tab in the clients section)
Ensure the server_url
points to your KeyCloak server's OpenID Connect configuration endpoint, usually structured like this: http://keycloak:8080/realms/<your-realm>/.well-known/openid-configuration
Replace <your-realm>
with your realm name.
'allauth.account.middleware.AccountMiddleware',
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('allauth.urls')),
# Include other app URLs as needed
]
This ensures django-allauth handles all authentication routing.
$ python manage.py runserver
Navigate to http://localhost:8000/accounts/login/ in your browser to start the KeyCloak login flow. You’ll see the Django auth page, and under “other” means of login, click on “Login with Keycloak”. You will be redirected to your Keycloak server, where you can authenticate using your credentials. Upon successful login, you will be redirected to http://127.0.0.1:8000/accounts/profile/ by default. This confirms that your login was successful.
After following these steps, you have successfully created the authentication for your Django application. Keycloak will allow you to keep track of who is allowed to access your application and what roles they have. Now we can tie this in with Cerbos to manage what these defined roles are allowed to do/access within your Django application.
We’ll start by creating the Cerbos policy repository. This is where all policies will be defined for Cerbos to reference and then make authorization decisions.
Within your Django project's root directory, create a folder named ‘cerbos’, and within the cerbos folder, create a subfolder named ‘policies’.
Now, within the policies folder, we’ll create a YAML file to define the rules for authorization checks.
Still following our admin, user and manager role structure, let's create a YAML file that defines what these roles are allowed to do with ‘document’ resources. These roles are authorized as follows:
So our documents.yaml
file would look like this:
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default
resource: document # Applies to 'document' resources.
rules:
# Admins can view, edit, and delete any document.
- actions: ['view', 'edit', 'delete']
effect: EFFECT_ALLOW
roles: ['admin']
# Users can view and edit their own documents.
- actions: ['view', 'edit']
effect: EFFECT_ALLOW
roles: ['user']
condition: # Condition ensures the user is the document's author.
match:
expr: request.resource.attr.author == request.principal.id
# managers can view, edit and approve documents.
- actions: ['view', 'edit',’approve’]
effect: EFFECT_ALLOW
roles: ['manager']
This policy allows admins to view, edit and delete documents. Users are only allowed to view and edit documents they authored (where the ‘author’ attribute matches the user id). Managers are allowed to view, edit and approve documents.
Let's also write a principal policy to help manage permissions in a more granular manner, allowing for configurations based on the user.
There can be multiple managers across various departments so we want to restrict approval permissions to only a single manager within their corresponding departments, for example - marketing department.
For this principal policy the manager of the marketing department is identified with the principal ID, “john_doe
”.
We also want managers to only approve documents that have the status “pending_approval
”.
Create a principal.yaml
file and add:
apiVersion: api.cerbos.dev/v1
principalPolicy:
version: default
principal: "john_doe"
rules:
- resource: document
actions:
- action: "approve"
effect: EFFECT_ALLOW
condition:
match:
all:
Of:
- expr: request.resource.attr.department == 'Marketing'
- expr: request.resource.attr.status == 'pending_approval'
This policy ensures that only one user from the department (the manager), john_doe
, can approve documents, and only if those documents are from the marketing department and marked as "pending_approval
".
The Cerbos policy decision point(PDP) is a standalone service responsible for evaluating requests against the defined authorization policies.
Still, within the cerbos directory, create a file named conf.yaml
to specify the HTTP port Cerbos will listen for and the location of your policy repository:
server:
httpListenAddr: ":3592"
storage:
driver: "disk"
disk:
directory: /policies
We’ll use Docker to start the Cerbos PDP in a container and run it locally:
docker run --rm --name cerbos -t \
-v $(pwd)/Cerbos/policies:/policies \
-v $(pwd)/Cerbos:/config \
-p 3592:3592 \
ghcr.io/cerbos/cerbos:latest server --config=/config/conf.yaml
We need to be able to interact with the Cerbos PDP from our Django application. Let's do that by installing the Cerbos Python client and configuring the client instance:
$ pip install cerbos
Within our root directory, we create a file called cerbos_client.py containing the utility function we will use to communicate with the Cerbos PDP. In this file we set the Cerbos host to point to the instance we are running locally in Docker:
from cerbos.sdk.client import CerbosClient
cerbos_host = "localhost:3592"
cerbos_client = CerbosClient(host=cerbos_host)
You can now create views within your Django apps that will perform authorization checks using the Cerbos client before executing sensitive operations on resources. We’ll be integrating the Cerbos client within our views to check permissions before allowing access to specific functionalities.
We need to be able to retrieve our defined roles from Keycloak within our Django application before passing them to Cerbos for authorization checks. In a new file, we call utils.py
Here’s a way to go about that:
from jose import jwt
from django.conf import settings
def extract_roles_from_jwt(request):
"""Extract roles from JWT token."""
auth_header = request.headers.get('Authorization')
if not auth_header:
return []
token = auth_header.split()[1]
try:
# Decode JWT token. Use Keycloak's public key here, replacing 'your-public-key'
decoded_token = jwt.decode(token, 'your-public-key', algorithms=['RS256'])
# Extract roles. Adjust this based on how roles are stored in your JWT
roles = decoded_token.get('realm_access', {}).get('roles', [])
return roles
except jwt.JWTError:
return []
Let's say we wanted to create a view function for the actions defined in our policies, ensuring the user is authenticated via Keycloak and then using Cerbos to enforce the permissions tied to the user's role. It would look something along these lines:
from django.http import HttpResponseForbidden, HttpResponse
from .utils import extract_roles_from_jwt
from .cerbos_client import cerbos_client # Ensure this is your configured Cerbos client instance
from cerbos.sdk.grpc.client import CerbosClient
from cerbos.engine.v1 import engine_pb2
from google.protobuf.struct_pb2 import Value
def manage_document(request, document_id, action):
# Extract user roles from JWT
user_roles = extract_roles_from_jwt(request)
user = request.user
# Setup the principal object for Cerbos
principal = engine_pb2.Principal(
id=user.username,
roles=user_roles,
attr={}
)
# Setup the resource object for Cerbos
resource = engine_pb2.Resource(
id=document_id,
kind="document",
attr={
"author": Value(string_value=user.username),
"status": Value(string_value="pending_approval"), # For 'approve' action condition
"department": Value(string_value="Marketing")
},
)
# Authorization check with Cerbos for the requested action
if not cerbos_client.is_allowed(action, principal, resource):
return HttpResponseForbidden(f"Not authorized to {action} this document.")
# Handling each action explicitly
if action == "view":
# Logic for viewing the document could involve retrieving it from a database
document_content = "Dummy content of the document."
return HttpResponse(f"Viewing Document: {document_content}", status=200)
elif action == "edit":
# Logic for editing might involve showing an edit form or directly saving an edit
return HttpResponse("Edit Document: Submit your edits.", status=200)
elif action == "delete":
# Logic for deleting might involve removing the document from the database
return HttpResponse("Document deleted successfully.", status=200)
elif action == "approve":
# Logic for approving might involve changing the document's status and notifying the author
return HttpResponse("Document approved successfully.", status=200)
else:
return HttpResponseForbidden("Invalid action requested.")
In the views.py
file of our Django application, we have a function called manage_document that handles various actions like viewing, editing, deleting, and approving documents. The function starts by extracting user roles from the JWT provided by Keycloak to determine the user's permissions. It sets up a principal object representing the user and their attributes, and a resource object for the document being accessed, which includes details like the document's owner and status.
Using Cerbos, the function performs an authorization check to see if the user is allowed to carry out the requested action on the document. If they're not authorized, it immediately returns a forbidden response. Depending on the action, the function either displays the document, provides an editing interface, deletes the document, or updates its status as part of the approval process. It then sends a confirmation response for the action performed, ensuring that operations on documents are securely managed based on the user's roles.
By setting up Keycloak, we established a secure authentication system, which, coupled with Cerbos' policy-driven authorization, provides a robust framework for managing user access and permissions. This integration ensures that only authenticated users can perform specific actions, enhancing the security and usability of Django applications. With these tools, developers can confidently build applications that protect sensitive operations and data, tailoring access to the needs and roles of different users.
Thanks for reading! If you enjoyed this blog, be sure to explore more integrations available in the ecosystem on our website. Don't forget to star(⭐️) cerbos open-source on GitHub – it really helps us. Join our Slack Community for any questions. Sign up for Cerbos Hub for free today.
Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team