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:
KeycloakAdminClient - REST API client for Keycloak Admin operations
KeycloakPlugin (PAS) - Pluggable Authentication Service plugin for user enumeration and properties
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¶
Plugin Connection Settings¶
Keycloak connection settings are configured directly on the KeycloakPlugin in acl_users.
These settings are required for the integration to function.
Property |
Description |
|---|---|
|
Base URL of the Keycloak server (e.g., |
|
The Keycloak realm to manage users in |
|
Client ID with admin permissions for user management (service account) |
|
Client secret for the admin service account |
|
Enable/disable Keycloak group synchronization (default: |
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:
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:
Create Client
Go to: Keycloak Admin Console → Clients → Create
Client ID:
plone-admin(or your preferred name)Client Protocol:
openid-connect
Configure Settings
Client authentication: Enabled (this enables the Credentials tab)
Service accounts roles: Enabled
Authorization: Optional
Assign Service Account Roles
Go to: Client → Service Account Roles → Assign role
Required roles from
realm-management:manage-users- Create, update, delete usersview-users- List and search usersquery-users- Query user informationmanage-realm- Required for group operations (optional)
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):
Navigate to:
/acl_users/manage_mainSelect “Keycloak Plugin” from the dropdown
Click “Add”
Fill in the connection settings (server URL, realm, client ID, client secret)
Activate the plugin interfaces (IUserAdderPlugin, IUserEnumerationPlugin, IPropertiesPlugin)
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:
Property |
Default |
Description |
|---|---|---|
|
|
Keycloak server URL (e.g., |
|
|
Keycloak realm name |
|
|
Service account client ID with |
|
|
Service account client secret |
|
|
Enable Keycloak group sync on user login |
User Registration Settings:
Property |
Default |
Description |
|---|---|---|
|
|
Send password reset email ( |
|
|
Send email verification ( |
|
|
Require 2FA/TOTP setup ( |
|
|
Email link validity in seconds (default: 24 hours) |
|
|
Redirect URI after completing Keycloak actions |
|
|
Client ID for redirect (required if redirect URI is set) |
User Enumeration¶
The plugin optimizes user enumeration with a persistent local cache:
Exact lookups (by ID or login) first check the local
_user_storage(OOBTree)Cache miss triggers a Keycloak API call
Results are stored persistently for future lookups
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='john@example.com')
# General search
users = acl_users.searchUsers(fullname='John')
User Creation¶
When a user is created through Plone’s registration:
User is created in Keycloak via Admin REST API
Configured required actions are set (password reset, email verification, 2FA)
Execute-actions email is sent to the user
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='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 groupsAutomatic Sync: Groups are synced when a user logs in (if
sync_groupsis enabled)Manual Sync: Use the
@@sync-keycloak-groupsview for full synchronizationUser Cleanup: Users deleted in Keycloak are removed from the plugin’s
_user_storagecache during sync, keeping the enumeration plugin in sync
Sync Operations¶
The sync process performs:
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
Membership Sync (
sync_all_memberships)Adds users to groups based on Keycloak membership
Removes users from groups they’re no longer members of
User Cleanup (
cleanup_deleted_users)Compares users in the plugin’s
_user_storagewith users in KeycloakRemoves 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:
# 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:
Syncs all groups from Keycloak (ensures groups exist)
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 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¶
# 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¶
# 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='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¶
Record Name |
Description |
|---|---|
|
Use access token for user info endpoint (default: |
|
List of allowed hosts for redirect after login |
|
Include API token in redirect URL (default: |
Custom Callback¶
The custom CallbackView enhances the standard OIDC callback with:
Support for access token authentication to userinfo endpoint
Configurable allowed hosts for post-login redirect
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¶
Module |
Description |
|---|---|
|
KeycloakAdminClient API tests |
|
Plugin instantiation and storage tests |
|
User enumeration and PAS integration |
|
User properties and property lookup |
|
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
IUserEnumerationPluginis activated for the pluginVerify Keycloak user has required attributes (username, email)
Check Keycloak API connectivity
3. Groups not syncing
Verify
sync_groupsisTrueon the KeycloakPlugin propertiesCheck that Keycloak client has group management permissions
Run
@@sync-keycloak-groupsmanually 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>