From 48501d69973003a4b83fe4cf0161b6c27e4e8928 Mon Sep 17 00:00:00 2001 From: Robitmoh Date: Tue, 30 Apr 2024 15:56:05 +0200 Subject: [PATCH 01/10] Add keycloak support require stevenmaguire/oauth2-keycloak --- src/Social/Mapper/Keycloak.php | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/Social/Mapper/Keycloak.php diff --git a/src/Social/Mapper/Keycloak.php b/src/Social/Mapper/Keycloak.php new file mode 100644 index 0000000..bd1e0f6 --- /dev/null +++ b/src/Social/Mapper/Keycloak.php @@ -0,0 +1,49 @@ + 'given_name', + 'last_name' => 'family_name', + 'email' => 'email', + 'username' => 'preferred_username', + 'id' => 'sub', + 'link' => 'website', + 'roles' => 'Cakedc' + ]; + protected $_rolesMatch = 'CakeDc-'; + + + function _roles($data){ # Client Scopes > roles > Mappers > realm roles -> Add to userinfo := Enable + $roles = array_filter(@$data[$this->_mapFields['roles']]['cakedc'], + function ($var) { return (strpos($var, $this->_rolesMatch) !== false); }); + + $role= str_ireplace($this->_rolesMatch, '',array_pop($roles)); + # + # Set the cakedc default user role from keycloak roles + # + Configure::write('Users.Registration.defaultRole',$role); + Configure::write('Users.Registration.KeycloakRole',$role); + return $role; + } +} From debf42e315c8c84c68391a34e2d1b15b8fa2c0c1 Mon Sep 17 00:00:00 2001 From: Robitmoh Date: Fri, 24 May 2024 09:50:20 +0200 Subject: [PATCH 02/10] Change role mapping New Exceptions for error handling --- src/Social/Mapper/Keycloak.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Social/Mapper/Keycloak.php b/src/Social/Mapper/Keycloak.php index bd1e0f6..823df9f 100644 --- a/src/Social/Mapper/Keycloak.php +++ b/src/Social/Mapper/Keycloak.php @@ -29,16 +29,22 @@ class Keycloak extends AbstractMapper 'username' => 'preferred_username', 'id' => 'sub', 'link' => 'website', - 'roles' => 'Cakedc' + 'roles' => 'realm_access' ]; protected $_rolesMatch = 'CakeDc-'; function _roles($data){ # Client Scopes > roles > Mappers > realm roles -> Add to userinfo := Enable - $roles = array_filter(@$data[$this->_mapFields['roles']]['cakedc'], + if (is_null($data[$this->_mapFields['roles']])){ + throw new \Exception("No roles in UserInfo token. Set realm roles 'Add to userinfo' field to ON in Client scopes or check the roles field in _mapFields"); + } + $roles = array_filter($data[$this->_mapFields['roles']]['roles'], function ($var) { return (strpos($var, $this->_rolesMatch) !== false); }); - - $role= str_ireplace($this->_rolesMatch, '',array_pop($roles)); + + if (empty($roles)){ + throw new \Exception("No CakeDc-* role mapped in keycloak"); + } + $role= str_ireplace($this->_rolesMatch, '',array_pop($roles)); # # Set the cakedc default user role from keycloak roles # From 0700e2202ff995fe01d0acaf98b7b293cbb1eaf9 Mon Sep 17 00:00:00 2001 From: Robitmoh Date: Fri, 19 Jul 2024 11:36:16 +0200 Subject: [PATCH 03/10] Fix variables types --- src/Social/Mapper/Keycloak.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Social/Mapper/Keycloak.php b/src/Social/Mapper/Keycloak.php index 823df9f..6e47872 100644 --- a/src/Social/Mapper/Keycloak.php +++ b/src/Social/Mapper/Keycloak.php @@ -31,10 +31,11 @@ class Keycloak extends AbstractMapper 'link' => 'website', 'roles' => 'realm_access' ]; - protected $_rolesMatch = 'CakeDc-'; + protected string $_rolesMatch = 'CakeDc-'; - function _roles($data){ # Client Scopes > roles > Mappers > realm roles -> Add to userinfo := Enable + function _roles(array: $data) :string + { # Client Scopes > roles > Mappers > realm roles -> Add to userinfo := Enable if (is_null($data[$this->_mapFields['roles']])){ throw new \Exception("No roles in UserInfo token. Set realm roles 'Add to userinfo' field to ON in Client scopes or check the roles field in _mapFields"); } From a92a1c4e306beb7c884f1ea70ba0c5a04e548260 Mon Sep 17 00:00:00 2001 From: Robitmoh Date: Fri, 19 Jul 2024 11:49:26 +0200 Subject: [PATCH 04/10] Typo error --- src/Social/Mapper/Keycloak.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Social/Mapper/Keycloak.php b/src/Social/Mapper/Keycloak.php index 6e47872..5a5f852 100644 --- a/src/Social/Mapper/Keycloak.php +++ b/src/Social/Mapper/Keycloak.php @@ -34,7 +34,7 @@ class Keycloak extends AbstractMapper protected string $_rolesMatch = 'CakeDc-'; - function _roles(array: $data) :string + function _roles(array $data) :string { # Client Scopes > roles > Mappers > realm roles -> Add to userinfo := Enable if (is_null($data[$this->_mapFields['roles']])){ throw new \Exception("No roles in UserInfo token. Set realm roles 'Add to userinfo' field to ON in Client scopes or check the roles field in _mapFields"); From 3600ad2fa7513bad847765954de1df85da5432af Mon Sep 17 00:00:00 2001 From: Robitmoh Date: Tue, 1 Apr 2025 10:07:59 +0200 Subject: [PATCH 05/10] Add keycloak support to Social auth --- Docs/Documentation/Social.md | 106 ++++++++++++++++++++++++++++++++- config/auth.php | 23 ++++++- src/Social/MapUser.php | 16 +++++ src/Social/Mapper/Keycloak.php | 62 ++++++++++++++----- 4 files changed, 187 insertions(+), 20 deletions(-) diff --git a/Docs/Documentation/Social.md b/Docs/Documentation/Social.md index c63d374..2591471 100644 --- a/Docs/Documentation/Social.md +++ b/Docs/Documentation/Social.md @@ -92,4 +92,108 @@ Working with cakephp/authentication If you're using the new cakephp/authentication we recommend you to use the SocialAuthenticator and SocialMiddleware provided in this plugin. For more details of how to handle social authentication with cakephp/authentication, please check -how we implemented at CakeDC/Users plugins. \ No newline at end of file +how we implemented at CakeDC/Users plugins. + +Working with Keycloak +------------------- + +Keycloak is an open source identity and access management solution that can be integrated with this plugin. Here's how to set it up: + +### Configuration + +Add the Keycloak provider configuration to your `config/users.php` file: + +```php +'OAuth' => [ + 'providers' => [ + 'keycloak' => [ + 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', + 'className' => 'Stevenmaguire\OAuth2\Client\Provider\Keycloak', + 'mapper' => 'CakeDC\Auth\Social\Mapper\Keycloak', + 'rolesMap' => [ + 'CakeDc-Admin' => 'admin', + 'CakeDc-User' => 'user', + 'CakeDc-Worker' => 'user' + ], + 'authParams' => ['scope' => ['openid', 'roles']], + 'skipSocialAccountValidation' => true, + 'options' => [ + 'redirectUri' => Router::fullBaseUrl() . '/auth/keycloak', + 'linkSocialUri' => Router::fullBaseUrl() . '/auth/link-social/keycloak', + 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/auth/callback-link-social/keycloak', + 'realm' => env('KEYCLOAK_REALM', null), + 'clientId' => env('KEYCLOAK_CLIENT_ID', null), + 'clientSecret' => env('KEYCLOAK_CLIENT_SECRET', null), + 'authServerUrl' => env('KEYCLOAK_AUTH_SERVER_URL', null), + ] + ], + ], +], +``` + +### Keycloak Server Configuration + +1. **Client Scopes Setup**: + - Enable the `openid` scope + - Add a `roles` scope with Mappers configuration + - Configure the `realm_roles` mapper with "Add to userinfo" set to ON + +2. **Realm Roles**: + - Create roles that match your configuration (e.g., `CakeDc-Admin`, `CakeDc-User`, `CakeDc-Worker`) + - Assign these roles to users or groups in Keycloak + +3. **User Attributes**: + - You can add additional attributes like `website` to users if needed + +### Role Mapping + +The plugin maps Keycloak roles to application roles using the `rolesMap` configuration. When a user logs in, their Keycloak roles are checked against this map and the corresponding application role is assigned. + +### Event Listener for Role Updates + +You can create a custom event listener to update user roles during login: + +```php + 'changeRole', + ]; + } + + public function changeRole(EventInterface $event) + { + $data = $event->getData('data'); + $userEntity = $event->getData('userEntity'); + + if (isset($data['provider']) && $data['provider'] === 'keycloak' && isset($data['roles'])) { + $userEntity->set('role', $data['roles']); + return $userEntity; + } + + return null; + } +} +``` + +### Environment Variables + +For security, store your Keycloak configuration in environment variables: + +``` +KEYCLOAK_REALM=your-realm +KEYCLOAK_CLIENT_ID=your-client-id +KEYCLOAK_CLIENT_SECRET=your-client-secret +KEYCLOAK_AUTH_SERVER_URL=https://your-keycloak-server/auth +``` + +This setup allows your CakePHP application to authenticate users through Keycloak and map their roles appropriately. \ No newline at end of file diff --git a/config/auth.php b/config/auth.php index 2dafc60..9c65fce 100644 --- a/config/auth.php +++ b/config/auth.php @@ -86,11 +86,28 @@ 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/azure', ] ], + 'keycloak' => [ + 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', + 'className' => 'Stevenmaguire\OAuth2\Client\Provider\Keycloak', + 'mapper' => 'CakeDC\Auth\Social\Mapper\Keycloak', + 'rolesMap' => [ + 'CakeDc-Admin' => 'Admin', + 'CakeDc-User' => 'User', + 'CakeDc-Worker' => 'User' + ], + 'authParams' => ['scope' => ['openid','roles']], + 'skipSocialAccountValidation' => true, + 'options' => [ + 'redirectUri' => Router::fullBaseUrl() . '/auth/keycloak', + 'linkSocialUri' => Router::fullBaseUrl() . '/auth/link-social/keycloak', + 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/auth/callback-link-social/keycloak', + ] + ], ], - 'TwoFactorProcessors' => [ + 'TwoFactorProcessors' => [ \CakeDC\Auth\Authentication\TwoFactorProcessor\OneTimePasswordProcessor::class, - \CakeDC\Auth\Authentication\TwoFactorProcessor\Webauthn2faProcessor::class, - ], + \CakeDC\Auth\Authentication\TwoFactorProcessor\Webauthn2faProcessor::class + ], 'OneTimePasswordAuthenticator' => [ 'checker' => \CakeDC\Auth\Authentication\DefaultOneTimePasswordAuthenticationChecker::class, 'verifyAction' => [ diff --git a/src/Social/MapUser.php b/src/Social/MapUser.php index 7391e4f..b3490a4 100644 --- a/src/Social/MapUser.php +++ b/src/Social/MapUser.php @@ -15,9 +15,25 @@ use CakeDC\Auth\Social\Service\ServiceInterface; use InvalidArgumentException; +use Cake\Event\EventManager; +use App\Event\SocialLoginListener; + class MapUser { + /** + * Constructor + * + * Initializes the MapUser class and registers the SocialLoginListener + * to handle social login events. + */ + public function __construct() + { + + $listener = new SocialLoginListener(); + EventManager::instance()->on($listener); + + } /** * Map social user user data * diff --git a/src/Social/Mapper/Keycloak.php b/src/Social/Mapper/Keycloak.php index 5a5f852..a58927d 100644 --- a/src/Social/Mapper/Keycloak.php +++ b/src/Social/Mapper/Keycloak.php @@ -31,26 +31,56 @@ class Keycloak extends AbstractMapper 'link' => 'website', 'roles' => 'realm_access' ]; - protected string $_rolesMatch = 'CakeDc-'; - + + /** + * Map Keycloak roles to CakeDC roles + * + * @var array + */ + protected array $_rolesMap = [ + 'CakeDc-Admin' => 'admin', + 'CakeDc-User' => 'user', + 'CakeDc-Worker' => 'user' + ]; + + /** + * Constructor + */ + public function __construct() + { + $configRoleMap = Configure::read('OAuth.providers.keycloak.rolesMap'); + if (!empty($configRoleMap) && is_array($configRoleMap)) { + $this->_rolesMap = $configRoleMap; + } + } - function _roles(array $data) :string - { # Client Scopes > roles > Mappers > realm roles -> Add to userinfo := Enable - if (is_null($data[$this->_mapFields['roles']])){ - throw new \Exception("No roles in UserInfo token. Set realm roles 'Add to userinfo' field to ON in Client scopes or check the roles field in _mapFields"); + function _roles(array $data): string + { // Client Scopes > roles > Mappers > realm roles -> Add to userinfo := Enable + if (is_null($data[$this->_mapFields['roles']])) { + throw new \Exception("No roles in UserInfo token. Set realm roles 'Add to userinfo' field to ON in Client scopes or check the roles field in _mapFields"); + } + + $keycloakRoles = $data[$this->_mapFields['roles']]['roles']; + + // Ignore case when comparing roles + $mappedRoles = []; + foreach ($keycloakRoles as $keycloakRole) { + foreach (array_keys($this->_rolesMap) as $mapKey) { + if (strcasecmp($keycloakRole, $mapKey) === 0) { + $mappedRoles[] = $mapKey; + break; + } + } } - $roles = array_filter($data[$this->_mapFields['roles']]['roles'], - function ($var) { return (strpos($var, $this->_rolesMatch) !== false); }); - if (empty($roles)){ - throw new \Exception("No CakeDc-* role mapped in keycloak"); + if (empty($mappedRoles)) { + throw new \Exception("No mappable role found in Keycloak. Available roles in map: " . implode(', ', array_keys($this->_rolesMap)) . ' / '. implode(', ', ($keycloakRoles))); } - $role= str_ireplace($this->_rolesMatch, '',array_pop($roles)); - # - # Set the cakedc default user role from keycloak roles - # - Configure::write('Users.Registration.defaultRole',$role); - Configure::write('Users.Registration.KeycloakRole',$role); + + $keycloakRole = array_pop($mappedRoles); + $role = $this->_rolesMap[$keycloakRole]; + // Set the cakedc default user role from keycloak roles + Configure::write('Users.Registration.defaultRole', $role); return $role; } } From 0ec3055f074f786e5ce19fa11a9217d554e68aba Mon Sep 17 00:00:00 2001 From: Robitmoh Date: Tue, 1 Apr 2025 10:39:15 +0200 Subject: [PATCH 06/10] Add composer example --- Docs/Documentation/Social.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Docs/Documentation/Social.md b/Docs/Documentation/Social.md index 2591471..c95d8b1 100644 --- a/Docs/Documentation/Social.md +++ b/Docs/Documentation/Social.md @@ -99,6 +99,14 @@ Working with Keycloak Keycloak is an open source identity and access management solution that can be integrated with this plugin. Here's how to set it up: +### Installation + +First, install the required OAuth2 Keycloak provider package (version 5.1 or newer): + +```bash +composer require stevenmaguire/oauth2-keycloak +``` + ### Configuration Add the Keycloak provider configuration to your `config/users.php` file: From 504de1798d555911038253c52c54028549d714f4 Mon Sep 17 00:00:00 2001 From: Robitmoh Date: Tue, 1 Apr 2025 10:39:51 +0200 Subject: [PATCH 07/10] Add Event listener --- src/Event/SocialLoginListener.php | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/Event/SocialLoginListener.php diff --git a/src/Event/SocialLoginListener.php b/src/Event/SocialLoginListener.php new file mode 100644 index 0000000..fe9e9f5 --- /dev/null +++ b/src/Event/SocialLoginListener.php @@ -0,0 +1,42 @@ + + */ + public function implementedEvents(): array + { + return [ + Plugin::EVENT_SOCIAL_LOGIN_EXISTING_ACCOUNT => 'changeRole', + ]; + } + + /** + * Szerepkör módosítása Keycloak bejelentkezés esetén + * + * @param \Cake\Event\EventInterface $event Az esemény objektum + * @return \Cake\Datasource\EntityInterface|null + */ + public function changeRole(EventInterface $event) + { + $data = $event->getData('data'); + $userEntity = $event->getData('userEntity'); + debug($data); + if (isset($data['provider']) && $data['provider'] === 'keycloak' && isset($data['roles'])) { + $userEntity->set('role', $data['roles']); + return $userEntity; + } + + return null; + } +} \ No newline at end of file From f18f379ace5c547ef4a594f7ace6fba383e0d405 Mon Sep 17 00:00:00 2001 From: Robitmoh Date: Tue, 1 Apr 2025 13:24:25 +0200 Subject: [PATCH 08/10] Namespace corrections --- src/Event/SocialLoginListener.php | 3 +-- src/Social/MapUser.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Event/SocialLoginListener.php b/src/Event/SocialLoginListener.php index fe9e9f5..94a71d0 100644 --- a/src/Event/SocialLoginListener.php +++ b/src/Event/SocialLoginListener.php @@ -1,7 +1,7 @@ getData('data'); $userEntity = $event->getData('userEntity'); - debug($data); if (isset($data['provider']) && $data['provider'] === 'keycloak' && isset($data['roles'])) { $userEntity->set('role', $data['roles']); return $userEntity; diff --git a/src/Social/MapUser.php b/src/Social/MapUser.php index b3490a4..4ae7cf6 100644 --- a/src/Social/MapUser.php +++ b/src/Social/MapUser.php @@ -16,7 +16,7 @@ use CakeDC\Auth\Social\Service\ServiceInterface; use InvalidArgumentException; use Cake\Event\EventManager; -use App\Event\SocialLoginListener; +use CakeDC\Auth\Event\SocialLoginListener; class MapUser From 405b444754f6bd72c63c2e0b12c96e6d5cd7d8d5 Mon Sep 17 00:00:00 2001 From: Robitmoh Date: Mon, 14 Apr 2025 11:54:52 +0200 Subject: [PATCH 09/10] Fix: Remove dependency on CakeDC\Users\Plugin class --- src/Event/SocialLoginListener.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Event/SocialLoginListener.php b/src/Event/SocialLoginListener.php index 94a71d0..b426805 100644 --- a/src/Event/SocialLoginListener.php +++ b/src/Event/SocialLoginListener.php @@ -5,7 +5,6 @@ use Cake\Event\EventInterface; use Cake\Event\EventListenerInterface; -use CakeDC\Users\Plugin; class SocialLoginListener implements EventListenerInterface { @@ -17,7 +16,12 @@ class SocialLoginListener implements EventListenerInterface public function implementedEvents(): array { return [ - Plugin::EVENT_SOCIAL_LOGIN_EXISTING_ACCOUNT => 'changeRole', + /** + * Event name directly used from CakeDC/Users plugin + * Original source: CakeDC\Users\Plugin::EVENT_SOCIAL_LOGIN_EXISTING_ACCOUNT + * If the constant is changed in the CakeDC/Users plugin, this string must be updated accordingly + */ + 'CakeDC.Users.Social.afterIdentify' => 'changeRole', ]; } From 83d34d8adae40122ddfc467dee6b3bfc3b8fe9f1 Mon Sep 17 00:00:00 2001 From: Robitmoh Date: Mon, 27 Oct 2025 10:44:38 +0100 Subject: [PATCH 10/10] Add 'validated' field mapping and enhance constructor to support custom map fields in Keycloak mapper --- src/Social/Mapper/Keycloak.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Social/Mapper/Keycloak.php b/src/Social/Mapper/Keycloak.php index a58927d..eb2bf21 100644 --- a/src/Social/Mapper/Keycloak.php +++ b/src/Social/Mapper/Keycloak.php @@ -29,7 +29,8 @@ class Keycloak extends AbstractMapper 'username' => 'preferred_username', 'id' => 'sub', 'link' => 'website', - 'roles' => 'realm_access' + 'roles' => 'realm_access', + 'validated' => 'email_verified' ]; /** @@ -49,9 +50,13 @@ class Keycloak extends AbstractMapper public function __construct() { $configRoleMap = Configure::read('OAuth.providers.keycloak.rolesMap'); + $configMapFields = Configure::read('OAuth.providers.keycloak.mapFields'); if (!empty($configRoleMap) && is_array($configRoleMap)) { $this->_rolesMap = $configRoleMap; } + if (!empty($configMapFields) && is_array($configMapFields)) { + $this->_mapFields = $configMapFields; + } } function _roles(array $data): string