Keycloak Integration

7inOne provides comprehensive Keycloak integration for identity management, user provisioning, and group synchronization. This allows organizations to use Keycloak as their single source of truth for user authentication while maintaining seamless integration with Plone’s permission system.

Architecture Overview

The integration consists of three main components:

  1. KeycloakAdminClient - REST API client for Keycloak Admin operations

  2. KeycloakPlugin (PAS) - Pluggable Authentication Service plugin for user enumeration and properties

  3. Group Sync - One-way synchronization of Keycloak groups to native Plone groups

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│    Keycloak     │────▶│  KeycloakPlugin  │────▶│   Plone PAS     │
│  (Auth Server)  │     │   (PAS Plugin)   │     │  (acl_users)    │
└─────────────────┘     └──────────────────┘     └─────────────────┘
        │                        │
        │                        ▼
        │               ┌──────────────────┐
        └──────────────▶│   Group Sync     │
                        │  (Plone Groups)  │
                        └──────────────────┘

Configuration

Registry Settings

Configure Keycloak integration via the Plone registry. These settings are required for the integration to function.

Keycloak Registry Records

Record Name

Description

wcs.backend.keycloak.server_url

Base URL of the Keycloak server (e.g., https://keycloak.example.com)

wcs.backend.keycloak.realm

The Keycloak realm to manage users in

wcs.backend.keycloak.admin_client_id

Client ID with admin permissions for user management (service account)

wcs.backend.keycloak.admin_client_secret

Client secret for the admin service account

wcs.backend.keycloak.sync_groups

Enable/disable Keycloak group synchronization (default: False)

Example configuration via GenericSetup (registry.xml):

<record name="wcs.backend.keycloak.server_url">
    <value>https://keycloak.example.com</value>
</record>
<record name="wcs.backend.keycloak.realm">
    <value>my-realm</value>
</record>
<record name="wcs.backend.keycloak.admin_client_id">
    <value>plone-admin</value>
</record>
<record name="wcs.backend.keycloak.admin_client_secret">
    <value>your-client-secret</value>
</record>
<record name="wcs.backend.keycloak.sync_groups">
    <value>True</value>
</record>

Keycloak Client Setup

Create a dedicated client in Keycloak for Plone integration:

  1. Create Client

    • Go to: Keycloak Admin Console → Clients → Create

    • Client ID: plone-admin (or your preferred name)

    • Client Protocol: openid-connect

  2. Configure Settings

    • Client authentication: Enabled (this enables the Credentials tab)

    • Service accounts roles: Enabled

    • Authorization: Optional

  3. Assign Service Account Roles

    Go to: Client → Service Account Roles → Assign role

    Required roles from realm-management:

    • manage-users - Create, update, delete users

    • view-users - List and search users

    • query-users - Query user information

    • manage-realm - Required for group operations (optional)

  4. Get Client Secret

    Go to: Client → Credentials → Client secret

KeycloakPlugin (PAS Plugin)

The KeycloakPlugin is a PAS (Pluggable Authentication Service) plugin that provides:

  • IUserAdderPlugin - Create users in Keycloak when registered via Plone

  • IUserEnumerationPlugin - Enumerate users from Keycloak with local caching

  • IPropertiesPlugin - Provide user properties (email, fullname) from Keycloak

Installation

The plugin is installed via the ZMI (Zope Management Interface):

  1. Navigate to: /acl_users/manage_main

  2. Select “Keycloak Plugin” from the dropdown

  3. Click “Add”

  4. Configure plugin properties

Plugin Properties

KeycloakPlugin Properties

Property

Default

Description

send_password_reset

True

Send password reset email (UPDATE_PASSWORD action)

send_verify_email

True

Send email verification (VERIFY_EMAIL action)

require_totp

False

Require 2FA/TOTP setup (CONFIGURE_TOTP action)

email_link_lifespan

86400

Email link validity in seconds (default: 24 hours)

redirect_uri

""

Redirect URI after completing Keycloak actions

redirect_client_id

""

Client ID for redirect (required if redirect URI is set)

User Enumeration

The plugin optimizes user enumeration with a persistent local cache:

  1. Exact lookups (by ID or login) first check the local _user_storage (OOBTree)

  2. Cache miss triggers a Keycloak API call

  3. Results are stored persistently for future lookups

  4. Supports search by: username, email, fullname

# Example: Enumerate users programmatically
from Products.CMFCore.utils import getToolByName

acl_users = getToolByName(portal, 'acl_users')

# Exact match by username
users = acl_users.searchUsers(id='john.doe', exact_match=True)

# Search by email
users = acl_users.searchUsers(email='[email protected]')

# General search
users = acl_users.searchUsers(fullname='John')

User Creation

When a user is created through Plone’s registration:

  1. User is created in Keycloak via Admin REST API

  2. Configured required actions are set (password reset, email verification, 2FA)

  3. Execute-actions email is sent to the user

  4. User data is cached in local storage

# User registration triggers KeycloakPlugin.doAddUser()
from plone import api

# This creates the user in Keycloak
api.user.create(
    username='newuser',
    email='[email protected]',
    properties={
        'fullname': 'New User',
    }
)

Group Synchronization

Groups are synced one-way from Keycloak to native Plone groups. Keycloak is the single source of truth for group membership.

Sync Behavior

  • Group Prefix: Synced groups are prefixed with keycloak_ to distinguish them from native Plone groups

  • Automatic Sync: Groups are synced when a user logs in (if sync_groups is enabled)

  • Manual Sync: Use the @@sync-keycloak-groups view for full synchronization

  • Cleanup: Deleted users are removed from local storage during sync

Sync Operations

The sync process performs:

  1. Group Sync (sync_all_groups)

    • Creates new Plone groups for Keycloak groups

    • Updates group titles if changed

    • Deletes Plone groups that no longer exist in Keycloak

  2. Membership Sync (sync_all_memberships)

    • Adds users to groups based on Keycloak membership

    • Removes users from groups they’re no longer members of

  3. User Cleanup (cleanup_deleted_users)

    • Removes deleted users from the plugin’s local storage

Manual Sync View

Trigger a full sync via browser or cron job:

# Browser
https://your-site.com/@@sync-keycloak-groups

# Cron (with authentication)
curl -u admin:password https://your-site.com/@@sync-keycloak-groups

Response (JSON):

{
    "success": true,
    "message": "Sync complete: 5 groups created, 2 updated, 1 deleted. 15 users added to groups, 3 removed.",
    "stats": {
        "groups_created": 5,
        "groups_updated": 2,
        "groups_deleted": 1,
        "users_added": 15,
        "users_removed": 3,
        "users_cleaned": 0,
        "errors": 0
    }
}

Login Event Handler

When sync_groups is enabled, the on_user_logged_in event handler:

  1. Syncs all groups from Keycloak (ensures groups exist)

  2. Syncs the logged-in user’s group memberships

# Configured in configure.zcml
<subscriber
    for="Products.PluggableAuthService.interfaces.events.IUserLoggedInEvent"
    handler=".group_sync.on_user_logged_in"
/>

KeycloakAdminClient API

The KeycloakAdminClient provides a Python interface to the Keycloak Admin REST API.

Initialization

from wcs.backend.login.keycloak_client import KeycloakAdminClient, get_keycloak_client

# Option 1: Use factory function (reads from registry)
client = get_keycloak_client()

# Option 2: Direct initialization
client = KeycloakAdminClient(
    server_url='https://keycloak.example.com',
    realm='my-realm',
    client_id='plone-admin',
    client_secret='secret',
)

User Operations

# Create a user
user_id = client.create_user(
    username='john.doe',
    email='[email protected]',
    first_name='John',
    last_name='Doe',
    enabled=True,
    email_verified=False,
)

# Get user by username
user = client.get_user('john.doe')

# Get user ID by username or email
user_id = client.get_user_id_by_username('john.doe')
user_id = client.get_user_id_by_email('[email protected]')

# Search users
users = client.search_users(
    search='john',          # General search
    username='john.doe',    # Filter by username
    email='[email protected]',  # Filter by email
    exact=False,            # Substring match
    max_results=50,         # Limit results
)

# Send execute actions email
client.send_execute_actions_email(
    user_id=user_id,
    actions=['UPDATE_PASSWORD', 'VERIFY_EMAIL'],
    lifespan=86400,  # 24 hours
    redirect_uri='https://your-site.com',
    client_id='your-client-id',
)

# Set required actions
client.set_user_required_actions(user_id, ['UPDATE_PASSWORD'])

Group Operations

# Search groups
groups = client.search_groups(
    search='editors',
    exact=False,
    max_results=100,
)

# Get group by name
group = client.get_group_by_name('editors', exact=True)

# Get group by UUID
group = client.get_group(group_id)

# Create a group
group_id = client.create_group('new-group')

# Delete a group
client.delete_group(group_id)

# Get groups for a user
groups = client.get_groups_for_user(user_id)

# Get group members
members = client.get_group_members(group_id, max_results=1000)

# Add/remove user from group
client.add_user_to_group(user_id, group_id)
client.remove_user_from_group(user_id, group_id)

Exception Handling

from wcs.backend.login.keycloak_client import (
    KeycloakError,
    KeycloakAuthenticationError,
    KeycloakUserCreationError,
    KeycloakUserExistsError,
)

try:
    client.create_user(username='existing', email='[email protected]')
except KeycloakUserExistsError:
    # User already exists
    pass
except KeycloakUserCreationError as e:
    # Other creation error
    logger.error(f"Failed to create user: {e}")
except KeycloakAuthenticationError as e:
    # Authentication with Keycloak failed
    logger.error(f"Auth failed: {e}")

OIDC Authentication

In addition to the Admin API integration, 7inOne provides enhanced OIDC authentication via pas.plugins.oidc with custom callback handling.

Registry Settings

OIDC Registry Records

Record Name

Description

wcs.backend.oidc.use_access_token

Use access token for user info endpoint (default: False)

wcs.backend.oidc.allowed_hosts

List of allowed hosts for redirect after login

wcs.backend.oidc.include_api_token

Include API token in redirect URL (default: True)

Custom Callback

The custom CallbackView enhances the standard OIDC callback with:

  1. Support for access token authentication to userinfo endpoint

  2. Configurable allowed hosts for post-login redirect

  3. Optional API token inclusion in redirect URL

# Custom callback handles:
# 1. Token exchange
# 2. User info retrieval
# 3. Identity remembering (creates/updates Plone user)
# 4. Redirect to allowed host with optional API token

Testing

Integration tests require a running Keycloak instance. Configure test settings in keycloak_testing.py:

KEYCLOAK_SERVER_URL = 'http://localhost:8080'
KEYCLOAK_REALM = 'test-realm'
KEYCLOAK_ADMIN_CLIENT_ID = 'admin-cli'
KEYCLOAK_ADMIN_CLIENT_SECRET = 'test-secret'

Run Keycloak tests:

# All Keycloak tests
bin/test -t keycloak

# Specific test modules
bin/test -t test_keycloak_client
bin/test -t test_keycloak_enumeration
bin/test -t test_keycloak_properties
bin/test -t test_keycloak_user_adder

Test Files

Test Modules

Module

Description

test_keycloak_client.py

KeycloakAdminClient API tests

test_keycloak_plugin_basic.py

Plugin instantiation and storage tests

test_keycloak_enumeration.py

User enumeration and PAS integration

test_keycloak_properties.py

User properties and property lookup

test_keycloak_user_adder.py

User creation via plugin

Troubleshooting

Common Issues

1. Authentication fails with Keycloak

  • Verify client credentials are correct

  • Ensure client has “Service accounts roles” enabled

  • Check that required roles are assigned to service account

2. Users not appearing in search

  • Check that IUserEnumerationPlugin is activated for the plugin

  • Verify Keycloak user has required attributes (username, email)

  • Check Keycloak API connectivity

3. Groups not syncing

  • Verify wcs.backend.keycloak.sync_groups is True

  • Check that Keycloak client has group management permissions

  • Run @@sync-keycloak-groups manually and check response

4. Email actions not sent

  • Verify SMTP is configured in Keycloak realm settings

  • Check that actions are configured in plugin properties

  • Review Keycloak server logs for email errors

Logging

Enable debug logging for troubleshooting:

# In your zope.conf or logging configuration
<logger name="wcs.backend.login.keycloak_client">
    level DEBUG
</logger>
<logger name="wcs.backend.login.keycloak_pas_plugin">
    level DEBUG
</logger>
<logger name="wcs.backend.login.group_sync">
    level DEBUG
</logger>