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¶
Registry Settings¶
Configure Keycloak integration via the Plone registry. These settings are required for the integration to function.
Record Name |
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: |
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:
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”
Configure plugin properties
Plugin Properties¶
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='[email protected]')
# 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='[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 groupsAutomatic Sync: Groups are synced when a user logs in (if
sync_groupsis enabled)Manual Sync: Use the
@@sync-keycloak-groupsview for full synchronizationCleanup: Deleted users are removed from local storage during 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)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:
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 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¶
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
wcs.backend.keycloak.sync_groupsisTrueCheck 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>