Using Cerbos with Keycloak for Identity/AuthN

Published by Omu Inetimi on May 16, 2024
Using Cerbos with Keycloak for Identity/AuthN

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:

  • A working Django web application.
  • A Keycloak server.
  • Docker installed in your development environment.

Setting Up Keycloak

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.

Create a client

  1. In your project realm, go to the “clients” section and click Create.
  2. In the general settings, you’ll see a few fields, the clientID field being required. 
  3. Choose a client ID that represents your Django project. This ID is what Keycloak will use to authenticate your Django application.
  4. In the capability config, turn on client authentication - this ensures that client credentials are required for authentication.
  5. And finally in login settings, configure the Root URL to point to your Django application - usually http://localhost:8000.
  6. Specify “Valid Redirect URIs” to include paths Keycloak will use for authentication callbacks, typically your Django login and post-login redirect URIs.

Define User Roles

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.

Tying KeyCloak with Django

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

  • Install django all-auth via pip

$ pip install django-allauth

  • Then go ahead and start a Django project.
  • After setting up a Django project, we will configure it to work with Keycloak in the settings.py file. Add allauth and its related apps to INSTALLED_APPS:
INSTALLED_APPS = [
   # ...
   'allauth',
   'allauth.account',
   'allauth.socialaccount',
   'allauth.socialaccount.providers.openid_connect',
   # ...
]
  • Set authentication backends:
AUTHENTICATION_BACKENDS = [
   # ...
   'allauth.account.auth_backends.AuthenticationBackend',
   # ...
]
  • Update the 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.

  • Add all-auth to middleware:

'allauth.account.middleware.AccountMiddleware',

  • Finally, update your urls.py file by including django-allauth.
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.

  • You can now start the Django development server to test if everything is working.

$ 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.

Integrating Cerbos for Authorization

Set up a Cerbos policy repository

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".

Set up the Cerbos PDP

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

Set up the Cerbos client

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.

Extracting roles from Keycloak 

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 []

Adding Cerbos to your views

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.

Conclusion

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.

GUIDE
INTEGRATION

Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team