Skip to content

webcloud7/wcs.keycloak

Repository files navigation

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

  1. Log into your Keycloak Admin Console
  2. Select your realm
  3. Navigate to Clients and click Create client
  4. Configure the client:
    • Client ID: Choose a descriptive name (e.g., plone-service-account)
    • Client Protocol: openid-connect
  5. On the Capability config tab, enable:
    • Client authentication: On (enables the Credentials tab)
    • Service accounts roles: On
  6. Click Save

Assigning Required Roles

The service account needs permissions to manage users and groups:

  1. Go to your client's Service accounts roles tab
  2. Click Assign role
  3. Filter by clients and select realm-management
  4. Assign these roles:
    • manage-users - Required for creating users and sending emails
    • view-users - Required for user enumeration
    • query-users - Required for user search

Getting the Client Secret

  1. Go to your client's Credentials tab
  2. Copy the Client secret value

Plugin Configuration

Adding the Plugin via ZMI

  1. Navigate to your Plone site's ZMI: /acl_users/manage_main
  2. Select "Keycloak Plugin" from the dropdown and click Add
  3. Enter the plugin ID (e.g., keycloak)
  4. 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

  1. Groups from Keycloak are created in Plone with a keycloak_ prefix
  2. Group memberships are synced to match Keycloak
  3. Groups deleted in Keycloak are removed from Plone
  4. 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-users endpoint returns a 400 response
  • The @@sync-keycloak full 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

  1. All users from Keycloak are fetched and stored in the plugin's local storage
  2. User properties (email, first name, last name) are kept in sync
  3. 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:

  1. Be created in Keycloak
  2. 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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors