Keycloak integration for Plone 6.
This plugin works best in combination with pas.plugins.oidc for OpenID Connect authentication. When using both packages together, make sure to disable user creation in pas.plugins.oidc (create_user = False) since wcs.keycloak provides its own IUserAdderPlugin that handles user creation in Keycloak. Having both plugins create users will lead to conflicts.
Performance note on IUserEnumerationPlugin: The user enumeration plugin checks its local cache first and falls back to a Keycloak API call for every cache miss. Since getMemberById is called frequently throughout Plone (e.g. content listings, permission checks), this adds significant overhead when multiple user sources are active. Only activate IUserEnumerationPlugin if Keycloak is the sole user source.
History: This plugin was implemented by myself in a privte project and extracted via AI into it's own package.
- PAS Plugin: Pluggable Authentication Service plugin for Keycloak integration
- User Enumeration: Query and list users from Keycloak
- User Creation: Create users in Keycloak through Plone's registration workflow
- User Properties: Retrieve user properties (email, fullname) from Keycloak
- Group Synchronization: One-way sync of groups and memberships from Keycloak to Plone
- User Synchronization: One-way sync of users from Keycloak to the plugin's local storage
The plugin implements multiple PAS (Pluggable Authentication Service) interfaces:
- IUserAdderPlugin: Intercepts user creation to create users in Keycloak
- IUserEnumerationPlugin: Provides user enumeration from Keycloak
- IPropertiesPlugin: Provides user properties from Keycloak
Group and user synchronization is handled separately via event subscribers (automatic on login) and browser views (manual/scheduled).
| Module | Description |
|---|---|
plugin |
KeycloakPlugin PAS plugin with _v_ volatile client caching |
client |
KeycloakAdminClient REST API client using OAuth2 client credentials flow with automatic token refresh |
sync |
Group sync, membership sync, sync_all() orchestrator. Groups are prefixed with keycloak_ to coexist with native Plone groups |
user_sync |
User sync to _user_storage OOBTree |
interfaces |
IKeycloakLayer browser layer, IKeycloakPlugin marker interface |
browser/base |
BaseSyncView base class for the 3 sync views |
browser/user_management |
Overrides for Plone's user/group control panels with Keycloak sync buttons and admin links |
Keycloak is the single source of truth. All sync operations are one-way from Keycloak to Plone. Changes to synced groups or users in Plone will be overwritten on the next sync.
Groups synced from Keycloak are prefixed with keycloak_ to distinguish them from native Plone groups. This allows clear identification, safe deletion, and coexistence with native groups.
The KeycloakAdminClient authenticates using the client_credentials OAuth2 grant type. Tokens are automatically refreshed when they expire (on 401 response). The client provides operations for user management (create, search, get, email actions) and group management (create, delete, search, membership).
All tests run against a real Keycloak Docker container (no mocks):
| Component | Description |
|---|---|
BaseDockerServiceLayer |
Base layer for running Docker containers as test fixtures |
KeyCloakLayer |
Starts Keycloak Docker container and creates test realm |
KeycloakTestMixin |
Utilities for admin client creation, authentication, user/group cleanup |
KeycloakPluginTestMixin |
Plugin setup with interface activation and service account configuration |
Add wcs.keycloak to your Plone installation requirements:
wcs.keycloak
After installation, install the add-on profile through the Plone control panel or via GenericSetup.
Before configuring the plugin, you need to create a service account client in Keycloak with the appropriate permissions.
- Log into your Keycloak Admin Console
- Select your realm
- Navigate to Clients and click Create client
- Configure the client:
- Client ID: Choose a descriptive name (e.g.,
plone-service-account) - Client Protocol:
openid-connect
- Client ID: Choose a descriptive name (e.g.,
- On the Capability config tab, enable:
- Client authentication: On (enables the Credentials tab)
- Service accounts roles: On
- Click Save
The service account needs permissions to manage users and groups:
- Go to your client's Service accounts roles tab
- Click Assign role
- Filter by clients and select realm-management
- Assign these roles:
manage-users- Required for creating users and sending emailsview-users- Required for user enumerationquery-users- Required for user search
- Go to your client's Credentials tab
- Copy the Client secret value
- Navigate to your Plone site's ZMI:
/acl_users/manage_main - Select "Keycloak Plugin" from the dropdown and click Add
- Enter the plugin ID (e.g.,
keycloak) - Configure the connection settings
| Property | Description | Example |
|---|---|---|
| Server URL | Base URL of your Keycloak server | https://keycloak.example.com |
| Realm | The Keycloak realm name | my-realm |
| Admin Client ID | Service account client ID | plone-service-account |
| Admin Client Secret | Service account client secret | your-secret-here |
These options control behavior when users are created through Plone's registration:
| Property | Description | Default |
|---|---|---|
| Send password reset email | Send UPDATE_PASSWORD action email | True |
| Send verify email | Send VERIFY_EMAIL action email | True |
| Require 2FA/TOTP setup | Require CONFIGURE_TOTP action | False |
| Email link lifespan | How long email links are valid (seconds) | 86400 (24h) |
| Redirect URI | Where to redirect after Keycloak actions | (empty) |
| Redirect Client ID | Client ID for redirect | (empty) |
| Property | Description | Default |
|---|---|---|
| Enable Keycloak Group Sync | Sync all groups and the logged-in user's memberships on every login | False |
| Property | Description | Default |
|---|---|---|
| Enable Keycloak User Sync | Bulk-copy all Keycloak users (email, fullname) into local storage via sync endpoints | False |
User sync is only available when IUserEnumerationPlugin is not active. When enumeration is active, users are discovered live from Keycloak on every request, making local sync redundant. See User Synchronization for details.
After adding the plugin, activate the required interfaces in ZMI under acl_users/plugins/manage_main:
- IUserAdderPlugin: Enable to create users in Keycloak during registration
- IUserEnumerationPlugin: Enable to enumerate/search users from Keycloak
- IPropertiesPlugin: Enable to fetch user properties from Keycloak
The group sync feature provides one-way synchronization from Keycloak to Plone. Keycloak is the authoritative source for group membership.
- Groups from Keycloak are created in Plone with a
keycloak_prefix - Group memberships are synced to match Keycloak
- Groups deleted in Keycloak are removed from Plone
- Native Plone groups (without the prefix) are not affected
When Enable Keycloak Group Sync is enabled:
- All groups are synced when any user logs in
- The logged-in user's group memberships are updated
Trigger a group-only sync by calling the group sync endpoint:
curl (cron job):
curl -u admin:secret https://plone.example.com/@@sync-keycloak-groups{
"success": true,
"message": "Sync complete: 5 groups created, 0 updated, 0 deleted. 12 users added to groups, 0 removed. 0 stale users cleaned up.",
"stats": {
"groups_created": 5,
"groups_updated": 0,
"groups_deleted": 0,
"users_added": 12,
"users_removed": 0,
"users_cleaned": 0,
"errors": 0
}
}The user sync feature provides one-way synchronization of users from Keycloak to the plugin's local storage. This ensures that user properties (email, fullname) are available locally without querying Keycloak on every request.
User sync is automatically disabled when IUserEnumerationPlugin is active for the Keycloak plugin. Since active enumeration already discovers users live from Keycloak, storing them locally would be redundant. When enumeration is active:
- The sync button is hidden in the Users control panel
- The
@@sync-keycloak-usersendpoint returns a 400 response - The
@@sync-keycloakfull sync skips the user sync step
To use user sync, keep IUserEnumerationPlugin deactivated and enable the "Enable Keycloak User Sync" property instead.
- All users from Keycloak are fetched and stored in the plugin's local storage
- User properties (email, first name, last name) are kept in sync
- Users deleted in Keycloak are removed from local storage
Trigger a standalone user sync by calling the user sync endpoint:
curl (cron job):
curl -u admin:secret https://plone.example.com/@@sync-keycloak-users{
"success": true,
"message": "User sync complete: 50 users synced, 2 removed.",
"stats": {
"users_synced": 50,
"users_removed": 2,
"errors": 0
}
}The @@sync-keycloak view performs a complete synchronization of all Keycloak data to Plone. It combines group sync, membership sync, user sync (when enabled), and cleanup of deleted users into a single operation.
This is the recommended endpoint for cron jobs that need to keep everything in sync.
curl (cron job):
curl -u admin:secret https://plone.example.com/@@sync-keycloakWhen user sync is enabled:
{
"success": true,
"message": "Sync complete: 5 groups created, 0 updated, 0 deleted. 12 users added to groups, 0 removed. User sync: 50 synced, 2 removed.",
"stats": {
"groups_created": 5,
"groups_updated": 0,
"groups_deleted": 0,
"users_added": 12,
"users_removed": 0,
"users_synced": 50,
"users_sync_removed": 2,
"users_cleaned": 0,
"errors": 0
}
}When user sync is disabled, the response includes cleanup stats instead:
{
"success": true,
"message": "Sync complete: 5 groups created, 0 updated, 0 deleted. 12 users added to groups, 0 removed.",
"stats": {
"groups_created": 5,
"groups_updated": 0,
"groups_deleted": 0,
"users_added": 12,
"users_removed": 0,
"users_cleaned": 0,
"errors": 0
}
}| Endpoint | Scope | Use Case |
|---|---|---|
@@sync-keycloak |
Groups + memberships + users + cleanup | Recommended for cron jobs |
@@sync-keycloak-groups |
Groups + memberships + stale user cleanup | When you only need group data |
@@sync-keycloak-users |
Users only | When you only need user data |
Python (requests):
import requests
# Search users via Plone's user enumeration
response = requests.get(
'https://plone.example.com/@users',
params={'query': 'john'},
headers={'Accept': 'application/json'},
auth=('admin', 'secret')
)
users = response.json()JavaScript (fetch):
const response = await fetch('https://plone.example.com/@users?query=john', {
headers: {
'Accept': 'application/json',
'Authorization': 'Basic ' + btoa('admin:secret')
}
});
const users = await response.json();Users created through Plone's registration form (or @users endpoint) are automatically created in Keycloak when the IUserAdderPlugin is active.
Python (requests):
import requests
response = requests.post(
'https://plone.example.com/@users',
json={
'username': 'newuser',
'email': 'newuser@example.com',
'fullname': 'New User'
},
headers={'Accept': 'application/json', 'Content-Type': 'application/json'},
auth=('admin', 'secret')
)The user will:
- Be created in Keycloak
- Receive an email with actions based on plugin configuration (password setup, email verification, etc.)
Synced groups can be used like any Plone group:
Python (requests):
import requests
# List groups (includes keycloak_ prefixed groups)
response = requests.get(
'https://plone.example.com/@groups',
headers={'Accept': 'application/json'},
auth=('admin', 'secret')
)
groups = response.json()
# Get members of a synced group
response = requests.get(
'https://plone.example.com/@groups/keycloak_developers',
headers={'Accept': 'application/json'},
auth=('admin', 'secret')
)
group = response.json()
print(group['users'])The package includes comprehensive integration tests that run against a real Keycloak instance using Docker.
make install
make testOr run specific tests:
bin/test -s wcs.keycloak -t test_enumeration
bin/test -s wcs.keycloak -t TestKeycloakEnumerateUsers# Create virtual environment and install dependencies
make install
# Run tests
make test
# Start development instance
make startGPL-2.0