Skip to content

savantly-net/Wordpress-OIDC-plugin-modifications

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

WordPress OpenID Connect Generic - Keycloak Customizations

This repository contains modifications to the OpenID Connect Generic WordPress plugin to enhance Keycloak integration with advanced role mapping and synchronization features.

License

This project is licensed under GPL-2.0-or-later, in compliance with the original OpenID Connect Generic plugin license.

Repository Contents

  • openid-connect-generic/ - The base OpenID Connect Generic plugin (v3.10.0)
  • mu-plugins/oidc-keycloak-custom.php - Must-Use plugin that extends the base plugin with Keycloak-specific features

What Was Modified

Base Plugin Status

The base plugin (openid-connect-generic/) is included unmodified from version 3.10.0. It serves as a dependency for the customization plugin.

MU-Plugin Modifications (mu-plugins/oidc-keycloak-custom.php)

MODIFIED: October 23, 2024

This customization plugin adds extensive Keycloak-specific functionality. Below is a detailed comparison of what changed from the original implementation.


Detailed Modification Summary

1. User Claim Key Change (Security Enhancement)

Original Code:

if ( ! empty( $user_claim['user-realm-role'] ) ) {
    foreach ( $user_claim['user-realm-role'] as $idp_role ) {

Modified Code:

if ( ! empty( $user_claim['groups'] ) ) {
    foreach ( $user_claim['groups'] as $idp_role ) {

Why: Changed the expected claim key from user-realm-role to groups to align with Keycloak's standard group claims structure. This makes the integration more compatible with typical Keycloak configurations.

Impact:

  • Affects: oidc_keycloak_user_creation_test() and oidc_keycloak_map_user_role() functions
  • Better compatibility with Keycloak's default token structure

2. Role Synchronization Security Enhancement (Critical Change)

Original Code:

foreach ( $user_claim['user-realm-role'] as $idp_role ) {
    foreach ( $roles as $role_id => $role_name ) {
        if ( ! empty( $settings[ 'oidc_idp_' . strtolower( $role_name ) . '_roles' ] ) ) {
            if ( in_array( $idp_role, explode( ';', $settings[ 'oidc_idp_' . strtolower( $role_name ) . '_roles' ] ) ) ) {
                $user->add_role( $role_id );  // Only adds roles, never removes
                $role_count++;
            }
        }
    }
}

Modified Code:

foreach ( $user_claim['groups'] as $idp_role ) {
    foreach ( $roles as $role_id => $role_name ) {
        if ( ! empty( $settings[ 'oidc_idp_' . strtolower( $role_name ) . '_roles' ] ) ) {
            $mapped_roles = explode( ';', $settings[ 'oidc_idp_' . strtolower( $role_name ) . '_roles' ] );
            if ( in_array( $idp_role, $mapped_roles ) ) {
                // SECURITY: Use set_role() for first role to remove all existing roles
                // This ensures full sync with Keycloak - roles removed in Keycloak are removed in WordPress
                if ( $role_count === 0 ) {
                    $user->set_role( $role_id );  // Removes ALL existing roles first
                } else {
                    $user->add_role( $role_id );   // Add additional roles
                }
                $role_count++;
            }
        }
    }
}

Why: The original implementation only added roles but never removed them. This created a security issue where:

  • Users kept accumulating roles over time
  • Roles removed in Keycloak were not removed in WordPress
  • No true role synchronization occurred

Security Impact:

  • CRITICAL SECURITY FIX: Ensures WordPress roles fully synchronize with Keycloak
  • Prevents privilege escalation from accumulated roles
  • First mapped role uses set_role() to clear all existing roles
  • Subsequent roles use add_role() to support multiple roles
  • If user has no matching roles in Keycloak, they are stripped of all WordPress roles

3. Enhanced Default Role Handling

Original Code:

if ( intval( $role_count ) == 0 && ! empty( $settings['default_user_role'] ) ) {
    if ( boolval( $settings['default_user_role'] ) ) {
        $user->set_role( $settings['default_user_role'] );
    }
}

Modified Code:

if ( intval( $role_count ) == 0 ) {
    if ( ! empty( $settings['default_user_role'] ) ) {
        if ( boolval( $settings['default_user_role'] ) ) {
            $user->set_role( $settings['default_user_role'] );
        }
    } else {
        // SECURITY: If no roles match and no default, remove all roles
        $user->set_role( '' );
    }
}

Why: Added explicit handling for the case where no roles match and no default is configured. The system now explicitly removes all roles rather than leaving the user with whatever they had before.

Security Impact:

  • Prevents orphaned permissions when role mappings are removed
  • Ensures explicit permission model (deny by default)
  • No ambiguity in user access rights

4. Comprehensive Debug Logging (New Feature)

What Was Added:

// Debug function 1: Log user claim data
function oidc_keycloak_debug_user_claim( $user, $user_claim ) {
    error_log( "=== OIDC DEBUG: User Claim Data ===" );
    error_log( print_r( $user_claim, true ) );
    // ... logs role mapping settings
}

// Debug function 2: Track role mapping process
function oidc_keycloak_map_user_role( $user, $user_claim ) {
    error_log( "=== OIDC DEBUG: Starting role mapping for user {$user->user_login} ===" );
    // ... extensive logging throughout the role assignment process
}

// Debug function 3: Debug logout redirect URLs
function oidc_keycloak_debug_logout( $redirect_url, $requested_redirect_to, $user ) {
    error_log( "=== OIDC DEBUG: Logout redirect ===" );
    error_log( "OIDC DEBUG: redirect_url = " . $redirect_url );
    // ... logs user info and OIDC token status
}

// Debug function 4: Debug FINAL logout redirect after OIDC plugin processing
function oidc_keycloak_debug_final_logout( $redirect_url, $requested_redirect_to, $user ) {
    error_log( "=== OIDC DEBUG: FINAL logout redirect (after OIDC plugin) ===" );
    // ... logs parsed URL components and validates redirect
}

Why: The original plugin had no debug logging, making troubleshooting extremely difficult. Added comprehensive logging for:

  • User claim data from Keycloak
  • Role mapping configuration
  • Role assignment decisions
  • Logout redirect flow (priority 5 and priority 100)

Impact:

  • Dramatically easier troubleshooting
  • Visibility into Keycloak token contents
  • Track role assignment logic
  • Debug logout redirect issues (including URL length and parsing)

5. Additional Hook Actions for Debug Logging

Added Hooks:

// Log user claims on user creation
add_action( 'openid-connect-generic-user-create', 'oidc_keycloak_debug_user_claim', 5, 2 );

// Log user claims on user update
add_action( 'openid-connect-generic-update-user-using-current-claim', 'oidc_keycloak_debug_user_claim', 5, 2 );

// Debug logout at priority 5 (before OIDC plugin)
add_filter( 'logout_redirect', 'oidc_keycloak_debug_logout', 5, 3 );

// Debug logout at priority 100 (after OIDC plugin at 99)
add_filter( 'logout_redirect', 'oidc_keycloak_debug_final_logout', 100, 3 );

Why: Provides visibility at multiple points in the authentication lifecycle, including before and after the base OIDC plugin processes logout redirects.


Features Added by the MU-Plugin

All features from the original customization plugin are preserved:

  1. Customizable Login Button Text - Admin setting to customize SSO button text
  2. IDP Role Mapping - Map Keycloak groups/roles to WordPress roles (semicolon-separated)
  3. Required Role Enforcement - Optionally require valid role mapping for user creation
  4. Default Role Assignment - Assign default role when no IDP role matches
  5. Automatic Role Synchronization - Sync WordPress roles with Keycloak on every login (NEW BEHAVIOR)
  6. Role Removal on Logout - Remove unmapped roles (NEW BEHAVIOR)
  7. Comprehensive Debug Logging - Full visibility into authentication flow (NEW)
  8. Logout Redirect Debugging - Track logout URL transformations (NEW)

Installation

Option 1: Standard Plugin + MU-Plugin (Recommended)

  1. Install the base plugin:

    cp -r openid-connect-generic /path/to/wordpress/wp-content/plugins/
  2. Install the customization as a must-use plugin:

    cp mu-plugins/oidc-keycloak-custom.php /path/to/wordpress/wp-content/mu-plugins/
  3. Activate the base plugin via WordPress admin (Dashboard → Plugins)

  4. MU-plugin is automatically loaded (cannot be deactivated)

Option 2: Docker Volume Mount (Development)

If using a Dockerized WordPress setup:

volumes:
  - ./mu-plugins:/var/www/html/wp-content/mu-plugins:ro
  - ./openid-connect-generic:/var/www/html/wp-content/plugins/openid-connect-generic:ro

Configuration

1. Base Plugin Settings

Navigate to Settings → OpenID Connect Client and configure:

  • Client ID: Your Keycloak client ID
  • Client Secret: Your Keycloak client secret
  • Scope: openid email profile groups (include groups claim)
  • Login Endpoint: https://your-keycloak/realms/{realm}/protocol/openid-connect/auth
  • Token Endpoint: https://your-keycloak/realms/{realm}/protocol/openid-connect/token
  • Userinfo Endpoint: https://your-keycloak/realms/{realm}/protocol/openid-connect/userinfo

2. Keycloak Configuration

In your Keycloak client settings:

  1. Add Groups to Token: Create a client scope or mapper to include groups in the token

    • Mapper Type: Group Membership
    • Token Claim Name: groups
    • Add to ID token: ✓
    • Add to userinfo: ✓
  2. Create Groups/Roles in Keycloak matching your WordPress roles

3. Customization Plugin Settings

The MU-plugin adds these settings to the OpenID Connect settings page:

Client Settings Section:

  • Login Button Text: Customize the SSO button text (e.g., "Login with Keycloak")

User Settings Section:

  • Valid IDP User Role Required: Check to prevent user creation without mapped role
  • Default New User Role: Fallback role when no IDP role matches
  • IDP Role Mappings: For each WordPress role (Administrator, Editor, etc.):
    • Enter semicolon-separated list of Keycloak group names
    • Example: admin;administrators;superusers

Role Mapping Example

Keycloak Groups:

  • wordpress-admin
  • wordpress-editor
  • wordpress-author

WordPress Plugin Configuration:

  • IDP Role for WordPress Administrators: wordpress-admin
  • IDP Role for WordPress Editors: wordpress-editor
  • IDP Role for WordPress Authors: wordpress-author
  • Default New User Role: Subscriber

Behavior:

  • User in wordpress-admin group → WordPress Administrator role
  • User in wordpress-editor group → WordPress Editor role
  • User in multiple groups → Gets multiple WordPress roles
  • User in no mapped groups → Gets Subscriber role (default)
  • User in no mapped groups (no default set) → All roles removed

Security Considerations

Critical Security Enhancements in Modified Version

  1. Full Role Synchronization: WordPress roles now fully mirror Keycloak state

    • Roles removed in Keycloak are immediately removed in WordPress
    • Prevents privilege escalation from orphaned roles
    • Uses set_role() for first role to clear existing permissions
  2. Explicit Permission Denial: When no roles match and no default is set, all roles are stripped

    • Follows principle of least privilege
    • No ambiguous permission states
  3. Groups Claim: Uses standard groups claim instead of non-standard user-realm-role

    • Better alignment with OAuth2/OIDC standards
    • Improved interoperability

Recommendations

  1. Always test role mappings in a staging environment first
  2. Enable debug logging initially to verify role assignment behavior
  3. Set a safe default role or enable "Required Role" to prevent unexpected access
  4. Monitor WordPress logs (wp-content/debug.log) during initial deployment
  5. Use HTTPS for all Keycloak endpoints in production

Debugging

The customization plugin adds extensive logging when WP_DEBUG_LOG is enabled.

Enable WordPress Debug Logging

In wp-config.php:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );

Log Output Location

Logs are written to: wp-content/debug.log

What Gets Logged

On User Login/Creation:

=== OIDC DEBUG: User Claim Data ===
Array (
    [groups] => Array (
        [0] => wordpress-admin
        [1] => developers
    )
    [email] => user@example.com
    ...
)

=== OIDC DEBUG: Role Mapping Settings ===
WordPress Role 'Administrator' maps to IDP roles: wordpress-admin;site-admin
...

=== OIDC DEBUG: Starting role mapping for user john.doe ===
OIDC DEBUG: Found groups in user claim
OIDC DEBUG: Processing IDP role: wordpress-admin
OIDC DEBUG: Assigning WordPress role 'Administrator' (ID: administrator) to user
OIDC DEBUG: Setting role (removes all existing roles): administrator
OIDC DEBUG: Successfully assigned 1 role(s)
=== OIDC DEBUG: Finished role mapping ===

On Logout:

=== OIDC DEBUG: Logout redirect ===
OIDC DEBUG: redirect_url = http://example.com/wp-login.php?loggedout=true
OIDC DEBUG: has_oidc_token = yes

=== OIDC DEBUG: FINAL logout redirect (after OIDC plugin) ===
OIDC DEBUG FINAL: redirect_url = https://keycloak.example.com/realms/myrealm/protocol/openid-connect/logout?...
OIDC DEBUG FINAL: redirect_url length = 437 characters

Comparison: Original vs Modified Behavior

Aspect Original Behavior Modified Behavior
Role Addition Only adds roles, never removes First role removes all existing, then adds mapped roles
Role Removal Roles never removed Roles removed when not in Keycloak
Claim Key user-realm-role groups (more standard)
No Role Match User keeps existing roles User loses all roles (or gets default)
Debugging No logging Extensive debug logging
Security Posture Can accumulate privileges Full synchronization with IDP
Logout Debugging None Tracks redirect transformations

Troubleshooting

Issue: Users not getting roles

Check:

  1. Groups are included in Keycloak token (check debug logs)
  2. Role mapping settings match Keycloak group names exactly (case-sensitive)
  3. groups claim is in the token (verify in logs)

Debug:

// In debug.log, look for:
"OIDC DEBUG: Found groups in user claim"  // Should see this
"OIDC DEBUG: Processing IDP role: YOUR_GROUP_NAME"  // Verify group names

Issue: Users have wrong roles

Check:

  1. Verify role mappings in plugin settings
  2. Check if "Valid IDP User Role Required" is enabled
  3. Verify default role setting

Solution: Roles sync on every login. Have user log out and log back in.

Issue: Users keep old roles

This was the bug in the original code. The modified version fixes this by using set_role() for the first role assignment, which clears all existing roles before assigning new ones.

Issue: Logout redirects to wrong location

Check debug logs for:

OIDC DEBUG: Logout redirect ===
OIDC DEBUG FINAL: FINAL logout redirect

Compare the URLs to identify where transformation occurs.


Credits

  • Original Plugin: OpenID Connect Generic by Jonathan Daggerhart
  • Customizations: Modified for enhanced Keycloak integration with security improvements
  • License: GPL-2.0-or-later (same as original plugin)

Changelog

Modified Version (October 2024)

Security Enhancements:

  • Changed user claim key from user-realm-role to groups for better Keycloak compatibility
  • Implemented full role synchronization using set_role() for first role
  • Added explicit role removal when no mappings match and no default is set
  • Fixed privilege escalation vulnerability from accumulated roles

New Features:

  • Comprehensive debug logging for user claims and role mapping
  • Logout redirect debugging (priority 5 and 100)
  • Enhanced error visibility for troubleshooting

Behavioral Changes:

  • WordPress roles now fully sync with Keycloak on every login
  • Roles removed in Keycloak are immediately removed in WordPress
  • No role match without default = all roles removed (secure by default)

Original Version

Features:

  • Customizable login button text
  • IDP role to WordPress role mapping
  • Required role enforcement
  • Default role assignment
  • Basic role mapping on login/creation

Contributing

This is a GPL-licensed project. Contributions, bug reports, and suggestions are welcome.

Reporting Issues

When reporting issues, please include:

  1. WordPress version
  2. Keycloak version
  3. Relevant debug log output
  4. Steps to reproduce

License

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.


Support

For support with:

About

This repo includes modifications made to the OpenID Connect Generic Plugin used on WordPress

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published