diff --git a/composer.json b/composer.json index 48b35840f0..983f597478 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "colinodell/json5": "^2.3", "doctrine/annotations": "^1.14.3", "guzzlehttp/guzzle": "^7.7", + "jumbojett/openid-connect-php": "^0.9.10", "paragonie/sodium_compat": "^1.20", "phpoffice/phpspreadsheet": "^1.28", "s1syphos/php-simple-captcha": "^2.3", diff --git a/composer.lock b/composer.lock index eec3a50993..94c8f70f10 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e7d7cdeeeb3c15d98430d3be05c71e89", + "content-hash": "de82ac57bfede1d62e79920ed78f058b", "packages": [ { "name": "async-aws/core", @@ -1065,6 +1065,48 @@ ], "time": "2023-04-17T16:11:26+00:00" }, + { + "name": "jumbojett/openid-connect-php", + "version": "v0.9.10", + "source": { + "type": "git", + "url": "https://github.com/jumbojett/OpenID-Connect-PHP.git", + "reference": "45aac47b525f0483dd4db3324bb1f1cab4666061" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jumbojett/OpenID-Connect-PHP/zipball/45aac47b525f0483dd4db3324bb1f1cab4666061", + "reference": "45aac47b525f0483dd4db3324bb1f1cab4666061", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "paragonie/random_compat": ">=2", + "php": ">=5.4", + "phpseclib/phpseclib": "~2.0 || ^3.0" + }, + "require-dev": { + "roave/security-advisories": "dev-master", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Bare-bones OpenID Connect client", + "support": { + "issues": "https://github.com/jumbojett/OpenID-Connect-PHP/issues", + "source": "https://github.com/jumbojett/OpenID-Connect-PHP/tree/v0.9.10" + }, + "time": "2022-09-30T12:34:46+00:00" + }, { "name": "maennchen/zipstream-php", "version": "2.4.0", @@ -1313,6 +1355,73 @@ ], "time": "2022-08-04T09:53:51+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.6.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "58c3f47f650c94ec05a151692652a868995d2938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2022-06-14T06:56:20+00:00" + }, { "name": "paragonie/random_compat", "version": "v9.99.100", @@ -1554,6 +1663,116 @@ }, "time": "2023-06-14T22:48:31+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.21", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/4580645d3fc05c189024eb3b834c6c1e4f0f30a1", + "reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.21" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2023-07-09T15:24:48+00:00" + }, { "name": "psr/cache", "version": "2.0.0", diff --git a/plugins/keycloak_oidc_login/Module.php b/plugins/keycloak_oidc_login/Module.php new file mode 100644 index 0000000000..3d5b14eab1 --- /dev/null +++ b/plugins/keycloak_oidc_login/Module.php @@ -0,0 +1,45 @@ + '/keycloak_oidc_login/login/login', + ]; + } + + public static function getAllUrlRoutes(array $urls, string $dom, string $dommotion, string $dommotionOld, string $domamend, string $domamendOld): array + { + return array_merge( + [ + $dom . 'keycloak-oidc' => '/keycloak_oidc_login/login/login', + ], + parent::getAllUrlRoutes($urls, $dom, $dommotion, $dommotionOld, $domamend, $domamendOld) + ); + } +} diff --git a/plugins/keycloak_oidc_login/OidcLogin.php b/plugins/keycloak_oidc_login/OidcLogin.php new file mode 100644 index 0000000000..052b96aae4 --- /dev/null +++ b/plugins/keycloak_oidc_login/OidcLogin.php @@ -0,0 +1,133 @@ +issuerUrl, '/')){ + $this->issuerUrl .= '/'; + } + } + + // map KC Claims to User attributes + private const PARAM_EMAIL = 'email'; + private const PARAM_USERNAME = 'preferred_username'; + private const PARAM_GIVEN_NAME = 'given_name'; + private const PARAM_FAMILY_NAME = 'family_name'; + + public function getId(): string + { + return Module::LOGIN_KEY; + } + + public function getName(): string + { + return 'Keycloak'; + } + + public function renderLoginForm(string $backUrl, bool $active): string + { + if (!$active){ + return ''; + } + return \Yii::$app->controller->renderPartial('@app/plugins/keycloak_oidc_login/views/login', [ + 'backUrl' => $backUrl + ]); + } + + /** + * @throws \Exception + */ + private function getOrCreateUser(array $params): User + { + $email = $params[self::PARAM_EMAIL]; + $givenname = $params[self::PARAM_GIVEN_NAME]; + $familyname = $params[self::PARAM_FAMILY_NAME]; + $username = $params[self::PARAM_USERNAME]; + $auth = Module::AUTH_KEY_USERS . ':' . $username; + + /** @var User|null $user */ + $user = User::findOne(['auth' => $auth]); + if (!$user) { + $user = new User(); + } + + $user->name = $givenname . ' ' . $familyname; + $user->nameGiven = $givenname; + $user->nameFamily = $familyname; + $user->email = $email; + $user->emailConfirmed = 1; + $user->fixedData = User::FIXED_NAME; + $user->auth = $auth; + $user->status = User::STATUS_CONFIRMED; + $user->organization = ''; + if (!$user->save()) { + throw new \Exception('Could not save / create user'); + } + + return $user; + } + + public function performLoginAndReturnUser(): User + { + $oidc = new OpenIDConnectClient( + $this->issuerUrl, + $this->clientId, + $this->clientSecret + ); + $oidc->setRedirectURL('http://localhost:8080/keycloak-oidc'); + $oidc->authenticate(); + $params = (array) $oidc->requestUserInfo(); + + $user = $this->getOrCreateUser($params); + RequestContext::getUser()->login($user, AntragsgruenApp::getInstance()->autoLoginDuration); + + $user->dateLastLogin = date('Y-m-d H:i:s'); + $user->save(); + + return $user; + } + + public function userWasLoggedInWithProvider(?User $user): bool + { + if (!$user || !$user->auth) { + return false; + } + $authParts = explode(':', $user->auth); + + return $authParts[0] === Module::AUTH_KEY_USERS; + } + + public function usernameToAuth(string $username): string + { + return Module::AUTH_KEY_USERS . ':' . $username; + } + + public function getSelectableUserOrganizations(User $user): ?array + { + return null; + } + + + public function logoutCurrentUserIfRelevant(string $backUrl): ?string + { + $user = User::getCurrentUser(); + if (!$this->userWasLoggedInWithProvider($user)) { + return null; + } + RequestContext::getUser()->logout(true); + return $this->issuerUrl . "protocol/openid-connect/logout"; + } + + public function renderAddMultipleUsersForm(): ?string + { + return null; + } +} diff --git a/plugins/keycloak_oidc_login/controllers/LoginController.php b/plugins/keycloak_oidc_login/controllers/LoginController.php new file mode 100644 index 0000000000..a9af672a5b --- /dev/null +++ b/plugins/keycloak_oidc_login/controllers/LoginController.php @@ -0,0 +1,38 @@ +site->getSettings()->loginMethods, true)) { + throw new \Exception('This login method is not enabled'); + } + + if ($backUrl === '') { + $backUrl = $this->getPostValue('backUrl', UrlHelper::homeUrl()); + } + + try { + Module::getDedicatedLoginProvider()->performLoginAndReturnUser(); + + $this->redirect($backUrl); + } catch (\Exception $e) { + $this->showErrorpage( + 500, + \Yii::t('user', 'err_unknown') . ':
"' . Html::encode($e->getMessage()) . '"' + ); + } + } +} diff --git a/plugins/keycloak_oidc_login/messages/de/keycloak_oidc_login.php b/plugins/keycloak_oidc_login/messages/de/keycloak_oidc_login.php new file mode 100644 index 0000000000..d6d06ed7cf --- /dev/null +++ b/plugins/keycloak_oidc_login/messages/de/keycloak_oidc_login.php @@ -0,0 +1,5 @@ + 'Mit Keycloak anmelden', +]; diff --git a/plugins/keycloak_oidc_login/views/login.php b/plugins/keycloak_oidc_login/views/login.php new file mode 100644 index 0000000000..f6c5a7ba9d --- /dev/null +++ b/plugins/keycloak_oidc_login/views/login.php @@ -0,0 +1,30 @@ + +
+

+
+ domainPlain . 'keycloak-oidc'; + echo Html::beginForm($action, 'post', ['class' => 'col-sm-4', 'id' => 'keycloakLoginForm']); + + $absoluteBack = UrlHelper::absolutizeLink($backUrl); + ?> + + + + +