From 647c77c1a7cd3adcad6715b3e9a8b3603e5d9579 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+studip@gmail.com> Date: Fri, 16 Aug 2024 11:10:00 +0000 Subject: [PATCH] add oauth2 as auth plugin, fixes #4482 Closes #4482 Merge request studip/studip!3266 --- composer.json | 3 +- composer.lock | 443 +++++++++++++++++- config/config_defaults.inc.php | 30 +- lib/classes/auth_plugins/StudipAuthOAuth2.php | 113 +++++ lib/phplib/Seminar_Auth.php | 8 +- lib/seminar_open.php | 9 + 6 files changed, 598 insertions(+), 8 deletions(-) create mode 100644 lib/classes/auth_plugins/StudipAuthOAuth2.php diff --git a/composer.json b/composer.json index e7fb7e5d2b7..4a8096741b2 100644 --- a/composer.json +++ b/composer.json @@ -123,7 +123,8 @@ "symfony/polyfill-php83": "1.30.0", "symfony/polyfill-php84": "1.30.0", "nyholm/psr7": "1.8.1", - "nyholm/psr7-server": "1.1.0" + "nyholm/psr7-server": "1.1.0", + "league/oauth2-client": "2.7.0" }, "replace": { "symfony/polyfill-php73": "*", diff --git a/composer.lock b/composer.lock index 8af2df65860..0cc31d19a4a 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": "4d8cd43aecf3277942d94871e22bf861", + "content-hash": "cad2a823e38968efd43dc8b63fdd8812", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -360,6 +360,331 @@ ], "time": "2023-11-12T22:16:48+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2024-07-24T11:22:20+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2024-07-18T10:29:17+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" + }, { "name": "illuminate/collections", "version": "v10.48.12", @@ -1019,6 +1344,76 @@ }, "time": "2018-11-26T11:52:41+00:00" }, + { + "name": "league/oauth2-client", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "160d6274b03562ebeb55ed18399281d8118b76c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8", + "reference": "160d6274b03562ebeb55ed18399281d8118b76c8", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "paragonie/random_compat": "^1 || ^2 || ^9.99", + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5", + "squizlabs/php_codesniffer": "^2.3 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0" + }, + "time": "2023-04-16T18:19:15+00:00" + }, { "name": "league/oauth2-server", "version": "8.5.4", @@ -3382,6 +3777,50 @@ }, "time": "2024-04-02T15:57:53+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "scssphp/scssphp", "version": "v1.12.1", @@ -8217,5 +8656,5 @@ "platform-overrides": { "php": "8.1" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.6.0" } diff --git a/config/config_defaults.inc.php b/config/config_defaults.inc.php index 78d53569a9f..ce484922be6 100644 --- a/config/config_defaults.inc.php +++ b/config/config_defaults.inc.php @@ -245,13 +245,14 @@ $_language_domain = "studip"; // the name of the language file. Should not be c ---------------------------------------------------------------- the following plugins are available: Standard authentication using the local Stud.IP database -StandardExtern authentication using an alternative Stud.IP database, e.g. another installation +StandardExtern authentication using an alternative Stud.IP database, e.g. another installation Ldap authentication using an LDAP server, this plugin uses anonymous bind against LDAP to retrieve the user dn, - then it uses the submitted password to authenticate with this user dn + then it uses the submitted password to authenticate with this user dn LdapReader authentication using an LDAP server, this plugin binds to the server using a given dn and a password, - this account must have read access to gather the attributes for the user who tries to authenticate. -CAS authentication using a central authentication server (CAS) + this account must have read access to gather the attributes for the user who tries to authenticate. +CAS authentication using a central authentication server (CAS) Shib authentication using a Shibboleth identity provider (IdP) +OAuth2 authentication using an OAuth2 identity provider If you write your own plugin put it in studip-htdocs/lib/classes/auth_plugins and enable it here. The name of the plugin is the classname excluding "StudipAuth". @@ -267,6 +268,7 @@ $STUDIP_AUTH_PLUGIN[] = "Standard"; // $STUDIP_AUTH_PLUGIN[] = "LTI"; // $STUDIP_AUTH_PLUGIN[] = "Shib"; // $STUDIP_AUTH_PLUGIN[] = "IP"; +// $STUDIP_AUTH_PLUGIN[] = 'OAuth2'; $STUDIP_AUTH_CONFIG_STANDARD = ["error_head" => "intern"]; @@ -378,6 +380,26 @@ $STUDIP_AUTH_CONFIG_SHIB = array("session_initiator" => "https://sp.studip.de/Sh $STUDIP_AUTH_CONFIG_IP = array('allowed_users' => array ('root' => array('127.0.0.1', '::1'))); + +$STUDIP_AUTH_CONFIG_OAUTH2 = [ + 'client_id' => '', + 'client_secret' => '', + 'redirect_uri' => '', + + 'url_authorize' => '', + 'url_access_token' => '', + 'url_resource_owner_details' => '', + + 'login_description' => 'Login with OAuth2', + + 'user_data_mapping' => [ + 'auth_user_md5.username' => ['callback' => 'getUserData', 'map_args' => 'nickname'], + 'auth_user_md5.password' => ['callback' => 'dummy', 'map_args' => ''], + 'auth_user_md5.Vorname' => ['callback' => 'getUserData', 'map_args' => 'given_name'], + 'auth_user_md5.Nachname' => ['callback' => 'getUserData', 'map_args' => 'family_name'], + 'auth_user_md5.EMail' => ['callback' => 'getUserData', 'map_args' => 'email'], + ], +]; */ //some additional authification-settings diff --git a/lib/classes/auth_plugins/StudipAuthOAuth2.php b/lib/classes/auth_plugins/StudipAuthOAuth2.php new file mode 100644 index 00000000000..aa9077633e8 --- /dev/null +++ b/lib/classes/auth_plugins/StudipAuthOAuth2.php @@ -0,0 +1,113 @@ +<?php +use League\OAuth2\Client\Provider\GenericProvider; + +/** + * StudipAuthOAuth2.php - Stud.IP authentication using OAuth2 + * + * @copyright 2024 Jan-Hendrik Willms <tleilax@gmail.com> + * @license GPL2 or any later version + * @since Stud.IP 6.0 + */ +final class StudipAuthOAuth2 extends StudipAuthSSO +{ + protected string $client_id; + protected string $client_secret; + protected string $redirect_uri; + + protected string $url_authorize; + protected string $url_access_token; + protected string $url_resource_owner_details; + + private GenericProvider $oauth2_provider; + + private ?array $user_data = null; + + public function __construct($config = []) + { + parent::__construct($config); + + if (!isset($this->plugin_fullname)) { + $this->plugin_fullname = _('OAuth2'); + } + + if (Request::option('sso') === $this->plugin_name) { + $options = [ + 'clientId' => $this->client_id, + 'clientSecret' => $this->client_secret, + 'redirectUri' => $this->redirect_uri, + 'urlAuthorize' => $this->url_authorize, + 'urlAccessToken' => $this->url_access_token, + 'urlResourceOwnerDetails' => $this->url_resource_owner_details, + ]; + + if (Config::get()->getValue('HTTP_PROXY')) { + $options['proxy'] = Config::get()->getValue('HTTP_PROXY'); + $options['verify'] = false; + } + + $this->oauth2_provider = new GenericProvider($options); + } + } + + public function getUser() + { + return $this->getUserData($this->getUsernameKey()); + } + + public function verifyUsername($username) + { + if (isset($this->user_data)) { + return parent::verifyUsername($this->getUser()); + } + + if (!Request::get('code')) { + $authorizationUrl = $this->oauth2_provider->getAuthorizationUrl(['scope' => 'profile email']); + + $_SESSION[self::class] = [ + 'state' => $this->oauth2_provider->getState(), + 'redirect' => Request::url(), + ]; + + page_close(); + header('Location: ' . $authorizationUrl); + die; + } elseif ( + !Request::get('state') + || empty($_SESSION[self::class]['state']) + || Request::get('state') !== $_SESSION[self::class]['state'] + ) { + if (isset($_SESSION[self::class])) { + unset($_SESSION[self::class]); + } + } else { + $accessToken = $this->oauth2_provider->getAccessToken('authorization_code', [ + 'code' => Request::get('code'), + ]); + + $resourceOwner = $this->oauth2_provider->getResourceOwner($accessToken); + + $this->user_data = $resourceOwner->toArray(); + + return parent::verifyUsername($this->getUser()); + } + + return null; + } + + /** + * Callback that can be used in user_data_mapping array. + */ + public function getUserData(string $key): ?string + { + return $this->user_data[$key]; + } + + /** + * Returns the key used to store the username from user_data_mapping if + * present. Defaults to 'nickname'. + */ + private function getUsernameKey(): string + { + return $this->user_data_mapping['map_args']['auth_user_md5.username'] ?? 'nickname'; + } +} diff --git a/lib/phplib/Seminar_Auth.php b/lib/phplib/Seminar_Auth.php index 92ee2c1f64d..546d6d83937 100644 --- a/lib/phplib/Seminar_Auth.php +++ b/lib/phplib/Seminar_Auth.php @@ -125,7 +125,13 @@ class Seminar_Auth # Check for user supplied automatic login procedure if ($uid = $this->auth_preauth()) { $this->auth["uid"] = $uid; - $sess->regenerate_session_id(['auth', '_language', 'phpCAS', 'contrast']); + $sess->regenerate_session_id([ + '_language', + 'auth', + 'contrast', + 'phpCAS', + StudipAuthOAuth2::class + ]); $sess->freeze(); $GLOBALS['user'] = new Seminar_User($this->auth['uid']); return true; diff --git a/lib/seminar_open.php b/lib/seminar_open.php index 2e831bf7ce1..7e3acf61e82 100644 --- a/lib/seminar_open.php +++ b/lib/seminar_open.php @@ -158,6 +158,15 @@ if (Navigation::hasItem('/profile/edit')) { } if ($user_did_login) { + if (isset($_SESSION[StudipAuthOAuth2::class]['redirect'])) { + $redirect = $_SESSION[StudipAuthOAuth2::class]['redirect']; + unset($_SESSION[StudipAuthOAuth2::class]); + + page_close(); + header('Location: ' . $redirect); + die; + } + NotificationCenter::postNotification('UserDidLogin', $user->id); } -- GitLab