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: 1. **KeycloakAdminClient** - REST API client for Keycloak Admin operations 2. **KeycloakPlugin (PAS)** - Pluggable Authentication Service plugin for user enumeration and properties 3. **Group Sync** - One-way synchronization of Keycloak groups to native Plone groups .. code-block:: text ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ 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. .. list-table:: Keycloak Registry Records :widths: 30 70 :header-rows: 1 * - Record Name - Description * - ``wcs.backend.keycloak.server_url`` - Base URL of the Keycloak server (e.g., ``https://keycloak.example.com``) * - ``wcs.backend.keycloak.realm`` - The Keycloak realm to manage users in * - ``wcs.backend.keycloak.admin_client_id`` - Client ID with admin permissions for user management (service account) * - ``wcs.backend.keycloak.admin_client_secret`` - Client secret for the admin service account * - ``wcs.backend.keycloak.sync_groups`` - Enable/disable Keycloak group synchronization (default: ``False``) Example configuration via GenericSetup (``registry.xml``): .. code-block:: xml https://keycloak.example.com my-realm plone-admin your-client-secret True Keycloak Client Setup ^^^^^^^^^^^^^^^^^^^^^ Create a dedicated client in Keycloak for Plone integration: 1. **Create Client** - Go to: Keycloak Admin Console → Clients → Create - Client ID: ``plone-admin`` (or your preferred name) - Client Protocol: ``openid-connect`` 2. **Configure Settings** - Client authentication: **Enabled** (this enables the Credentials tab) - Service accounts roles: **Enabled** - Authorization: Optional 3. **Assign Service Account Roles** Go to: Client → Service Account Roles → Assign role Required roles from ``realm-management``: - ``manage-users`` - Create, update, delete users - ``view-users`` - List and search users - ``query-users`` - Query user information - ``manage-realm`` - Required for group operations (optional) 4. **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): 1. Navigate to: ``/acl_users/manage_main`` 2. Select "Keycloak Plugin" from the dropdown 3. Click "Add" 4. Configure plugin properties Plugin Properties ^^^^^^^^^^^^^^^^^ .. list-table:: KeycloakPlugin Properties :widths: 30 20 50 :header-rows: 1 * - Property - Default - Description * - ``send_password_reset`` - ``True`` - Send password reset email (``UPDATE_PASSWORD`` action) * - ``send_verify_email`` - ``True`` - Send email verification (``VERIFY_EMAIL`` action) * - ``require_totp`` - ``False`` - Require 2FA/TOTP setup (``CONFIGURE_TOTP`` action) * - ``email_link_lifespan`` - ``86400`` - Email link validity in seconds (default: 24 hours) * - ``redirect_uri`` - ``""`` - Redirect URI after completing Keycloak actions * - ``redirect_client_id`` - ``""`` - Client ID for redirect (required if redirect URI is set) User Enumeration ^^^^^^^^^^^^^^^^ The plugin optimizes user enumeration with a persistent local cache: 1. **Exact lookups** (by ID or login) first check the local ``_user_storage`` (OOBTree) 2. **Cache miss** triggers a Keycloak API call 3. Results are stored persistently for future lookups 4. Supports search by: username, email, fullname .. code-block:: python # 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: 1. User is created in Keycloak via Admin REST API 2. Configured required actions are set (password reset, email verification, 2FA) 3. Execute-actions email is sent to the user 4. User data is cached in local storage .. code-block:: python # 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 groups - **Automatic Sync**: Groups are synced when a user logs in (if ``sync_groups`` is enabled) - **Manual Sync**: Use the ``@@sync-keycloak-groups`` view for full synchronization - **Cleanup**: Deleted users are removed from local storage during sync Sync Operations ^^^^^^^^^^^^^^^ The sync process performs: 1. **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 2. **Membership Sync** (``sync_all_memberships``) - Adds users to groups based on Keycloak membership - Removes users from groups they're no longer members of 3. **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: .. code-block:: bash # Browser https://your-site.com/@@sync-keycloak-groups # Cron (with authentication) curl -u admin:password https://your-site.com/@@sync-keycloak-groups Response (JSON): .. code-block:: 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: 1. Syncs all groups from Keycloak (ensures groups exist) 2. Syncs the logged-in user's group memberships .. code-block:: python # Configured in configure.zcml KeycloakAdminClient API ----------------------- The ``KeycloakAdminClient`` provides a Python interface to the Keycloak Admin REST API. Initialization ^^^^^^^^^^^^^^ .. code-block:: python 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 ^^^^^^^^^^^^^^^ .. code-block:: python # 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 ^^^^^^^^^^^^^^^^ .. code-block:: python # 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 ^^^^^^^^^^^^^^^^^^ .. code-block:: python 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 ^^^^^^^^^^^^^^^^^ .. list-table:: OIDC Registry Records :widths: 30 70 :header-rows: 1 * - Record Name - Description * - ``wcs.backend.oidc.use_access_token`` - Use access token for user info endpoint (default: ``False``) * - ``wcs.backend.oidc.allowed_hosts`` - List of allowed hosts for redirect after login * - ``wcs.backend.oidc.include_api_token`` - Include API token in redirect URL (default: ``True``) Custom Callback ^^^^^^^^^^^^^^^ The custom ``CallbackView`` enhances the standard OIDC callback with: 1. Support for access token authentication to userinfo endpoint 2. Configurable allowed hosts for post-login redirect 3. Optional API token inclusion in redirect URL .. code-block:: python # 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``: .. code-block:: python 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: .. code-block:: bash # 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 ^^^^^^^^^^ .. list-table:: Test Modules :widths: 40 60 :header-rows: 1 * - Module - Description * - ``test_keycloak_client.py`` - KeycloakAdminClient API tests * - ``test_keycloak_plugin_basic.py`` - Plugin instantiation and storage tests * - ``test_keycloak_enumeration.py`` - User enumeration and PAS integration * - ``test_keycloak_properties.py`` - User properties and property lookup * - ``test_keycloak_user_adder.py`` - 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 ``IUserEnumerationPlugin`` is activated for the plugin - Verify Keycloak user has required attributes (username, email) - Check Keycloak API connectivity **3. Groups not syncing** - Verify ``wcs.backend.keycloak.sync_groups`` is ``True`` - Check that Keycloak client has group management permissions - Run ``@@sync-keycloak-groups`` manually 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: .. code-block:: ini # In your zope.conf or logging configuration level DEBUG level DEBUG level DEBUG