Keycloak integration for Plone 6
Project description
wcs.keycloak
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.
Features
- 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
Architecture
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).
Modules
| 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 |
Sync Strategy
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.
Client Authentication
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).
Testing Infrastructure
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 |
Installation
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.
Keycloak Client Setup
Before configuring the plugin, you need to create a service account client in Keycloak with the appropriate permissions.
Creating the Service Account Client
- 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
Assigning Required Roles
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
Getting the Client Secret
- Go to your client's Credentials tab
- Copy the Client secret value
Plugin Configuration
Adding the Plugin via ZMI
- 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
Connection Properties
| 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 |
User Creation Options
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) |
Group Sync Options
| Property | Description | Default |
|---|---|---|
| Enable Keycloak Group Sync | Sync all groups and the logged-in user's memberships on every login | False |
User Sync Options
| 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.
Activating Plugin Interfaces
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
Group Synchronization
The group sync feature provides one-way synchronization from Keycloak to Plone. Keycloak is the authoritative source for group membership.
How It Works
- 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
Automatic Sync on Login
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
Manual/Scheduled Group Sync
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
Group Sync Response Format
{
"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
}
}
User Synchronization
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.
How It Works
- 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
Dedicated User Sync Endpoint
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
User Sync Response Format
{
"success": true,
"message": "User sync complete: 50 users synced, 2 removed.",
"stats": {
"users_synced": 50,
"users_removed": 2,
"errors": 0
}
}
Full Synchronization
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-keycloak
Full Sync Response Format
When 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
}
}
Sync Endpoints Overview
| 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 |
Usage Examples
Querying Users from Keycloak
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();
Creating Users via Registration
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.)
Working with Synced Groups
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'])
Testing
The package includes comprehensive integration tests that run against a real Keycloak instance using Docker.
Running Tests
make install
make test
Or run specific tests:
bin/test -s wcs.keycloak -t test_enumeration
bin/test -s wcs.keycloak -t TestKeycloakEnumerateUsers
Development
# Create virtual environment and install dependencies
make install
# Run tests
make test
# Start development instance
make start
License
GPL-2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wcs_keycloak-1.0.0a2.tar.gz.
File metadata
- Download URL: wcs_keycloak-1.0.0a2.tar.gz
- Upload date:
- Size: 61.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96b5f9d46f6ca0b0d69e360d947b2caca236df1a49898636a7ddf66501b492f3
|
|
| MD5 |
af9c129f5513f81ade5f722a79259a48
|
|
| BLAKE2b-256 |
8557b23350091874c4be500dec5661efec7cc305027da0b3f4e96036257459c1
|
File details
Details for the file wcs_keycloak-1.0.0a2-py3-none-any.whl.
File metadata
- Download URL: wcs_keycloak-1.0.0a2-py3-none-any.whl
- Upload date:
- Size: 76.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
024262d0223668727be1f6c6a2da6e6d40e0ac0e0bba0d936079dd39fc61bb7f
|
|
| MD5 |
8da3cf550ec7c18fd4c513df18c2dc43
|
|
| BLAKE2b-256 |
6ca4cc4ce410cc7abef05bd0294e31e736a3062295a220237643f49f4e7c659f
|