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
.. code-block:: text
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Keycloak │────▶│ KeycloakPlugin │────▶│ Plone PAS │
│ (Auth Server) │ │ (PAS Plugin) │ │ (acl_users) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
│ ▼
│ ┌──────────────────┐
└──────────────▶│ Group Sync │
│ (Plone Groups) │
└──────────────────┘
Configuration
-------------
Plugin Connection Settings
^^^^^^^^^^^^^^^^^^^^^^^^^^
Keycloak connection settings are configured directly on the KeycloakPlugin in ``acl_users``.
These settings are required for the integration to function.
.. list-table:: Plugin Connection Properties
:widths: 30 70
:header-rows: 1
* - Property
- Description
* - ``server_url``
- Base URL of the Keycloak server (e.g., ``https://keycloak.example.com``)
* - ``realm``
- The Keycloak realm to manage users in
* - ``admin_client_id``
- Client ID with admin permissions for user management (service account)
* - ``admin_client_secret``
- Client secret for the admin service account
* - ``sync_groups``
- Enable/disable Keycloak group synchronization (default: ``False``)
Configuration Methods
"""""""""""""""""""""
**1. Via ZMI during plugin installation:**
When adding the KeycloakPlugin through the ZMI (``/acl_users/manage_main``), a form
prompts for the connection settings (server URL, realm, client ID, and secret).
**2. Via ZMI plugin properties page:**
Navigate to the plugin properties page (``/acl_users/keycloak/manage_propertiesForm``)
to view and modify all settings after installation.
**3. Programmatically:**
.. code-block:: python
from plone import api
from Products.CMFCore.utils import getToolByName
from wcs.backend.login.keycloak_pas_plugin import manage_addKeycloakPlugin
portal = api.portal.get()
acl_users = getToolByName(portal, 'acl_users')
# Add the plugin with connection settings
manage_addKeycloakPlugin(
acl_users,
'keycloak',
title='Keycloak',
server_url='https://keycloak.example.com',
realm='my-realm',
admin_client_id='plone-admin',
admin_client_secret='your-client-secret',
)
# Or configure an existing plugin
plugin = acl_users['keycloak']
plugin.server_url = 'https://keycloak.example.com'
plugin.realm = 'my-realm'
plugin.admin_client_id = 'plone-admin'
plugin.admin_client_secret = 'your-client-secret'
plugin.sync_groups = True
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. Fill in the connection settings (server URL, realm, client ID, client secret)
5. Activate the plugin interfaces (IUserAdderPlugin, IUserEnumerationPlugin, IPropertiesPlugin)
6. Configure additional plugin properties as needed
Plugin Properties
^^^^^^^^^^^^^^^^^
All Keycloak settings are configured as plugin properties. The table below lists all available
properties grouped by function.
**Connection Settings:**
.. list-table::
:widths: 30 20 50
:header-rows: 1
* - Property
- Default
- Description
* - ``server_url``
- ``""``
- Keycloak server URL (e.g., ``https://keycloak.example.com``)
* - ``realm``
- ``""``
- Keycloak realm name
* - ``admin_client_id``
- ``""``
- Service account client ID with ``manage-users`` permission
* - ``admin_client_secret``
- ``""``
- Service account client secret
* - ``sync_groups``
- ``False``
- Enable Keycloak group sync on user login
**User Registration Settings:**
.. list-table::
:widths: 30 20 50
:header-rows: 1
* - 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
.. code-block:: python
# 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='john@example.com')
# 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
.. code-block:: python
# User registration triggers KeycloakPlugin.doAddUser()
from plone import api
# This creates the user in Keycloak
api.user.create(
username='newuser',
email='newuser@example.com',
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
- **User Cleanup**: Users deleted in Keycloak are removed from the plugin's ``_user_storage`` cache during sync, keeping the enumeration plugin in 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``)
- Compares users in the plugin's ``_user_storage`` with users in Keycloak
- Removes users from local storage that no longer exist in Keycloak
- This keeps the enumeration plugin's cache in sync with Keycloak
- Prevents deleted Keycloak users from appearing in user searches
Manual Sync View
^^^^^^^^^^^^^^^^
Trigger a full sync via browser or cron job:
.. code-block:: bash
# Browser
https://your-site.com/@@sync-keycloak-groups
# Cron (with authentication)
curl -u admin:password https://your-site.com/@@sync-keycloak-groups
Response (JSON):
.. code-block:: 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
.. code-block:: python
# Configured in configure.zcml
KeycloakAdminClient API
-----------------------
The ``KeycloakAdminClient`` provides a Python interface to the Keycloak Admin REST API.
Initialization
^^^^^^^^^^^^^^
.. code-block:: python
from wcs.backend.login.keycloak_client import KeycloakAdminClient, get_keycloak_client
# Option 1: Use factory function (reads from plugin properties)
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',
)
The ``get_keycloak_client()`` factory function automatically retrieves the ``KeycloakPlugin``
from ``acl_users`` and reads connection settings from its properties (``server_url``,
``realm``, ``admin_client_id``, ``admin_client_secret``). Returns ``None`` if the plugin
is not found or not fully configured.
User Operations
^^^^^^^^^^^^^^^
.. code-block:: python
# Create a user
user_id = client.create_user(
username='john.doe',
email='john@example.com',
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('john@example.com')
# Search users
users = client.search_users(
search='john', # General search
username='john.doe', # Filter by username
email='john@example.com', # 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
^^^^^^^^^^^^^^^^
.. code-block:: python
# 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
^^^^^^^^^^^^^^^^^^
.. code-block:: python
from wcs.backend.login.keycloak_client import (
KeycloakError,
KeycloakAuthenticationError,
KeycloakUserCreationError,
KeycloakUserExistsError,
)
try:
client.create_user(username='existing', email='test@example.com')
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
^^^^^^^^^^^^^^^^^
.. list-table:: OIDC Registry Records
:widths: 30 70
:header-rows: 1
* - 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
.. code-block:: python
# 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``:
.. code-block:: python
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:
.. code-block:: bash
# 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
^^^^^^^^^^
.. list-table:: Test Modules
:widths: 40 60
:header-rows: 1
* - 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 ``sync_groups`` is ``True`` on the KeycloakPlugin properties
- 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:
.. code-block:: ini
# In your zope.conf or logging configuration
level DEBUG
level DEBUG
level DEBUG