From 09ee2c4a56dcbcdc6ffdaa3065ad55c9167c56ff Mon Sep 17 00:00:00 2001 From: Tim Filler Date: Sat, 23 Sep 2023 22:19:30 +0200 Subject: [PATCH] Add google via OAuth2 auth as login method --- composer.json | 3 +- composer.lock | 57 +++++- docs/08_extern_auth.md | 37 ++++ plugins/auth/boot.php | 3 + plugins/auth/install.php | 2 +- plugins/auth/install/oauth2_google.php | 8 + .../yform/trait_value_auth_oauth2_google.php | 164 ++++++++++++++++++ .../yform/value/ycom_auth_oauth2_google.php | 137 +++++++++++++++ .../value.ycom_auth_oauth2_google.tpl.php | 11 ++ 9 files changed, 419 insertions(+), 3 deletions(-) create mode 100644 plugins/auth/install/oauth2_google.php create mode 100644 plugins/auth/lib/yform/trait_value_auth_oauth2_google.php create mode 100644 plugins/auth/lib/yform/value/ycom_auth_oauth2_google.php create mode 100644 plugins/auth/ytemplates/bootstrap/value.ycom_auth_oauth2_google.tpl.php diff --git a/composer.json b/composer.json index a135f28c..2d235cd3 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,8 @@ "apereo/phpcas": "^1", "league/oauth2-client": "^2", "psr/log": "^1", - "vertisan/oauth2-twitch-helix": "^2.0" + "vertisan/oauth2-twitch-helix": "^2.0", + "league/oauth2-google": "^4.0" }, "replace": { "psr/container": "*", diff --git a/composer.lock b/composer.lock index 32597f65..d5b3b024 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": "15976251497e431c1fe47602ba5a41c6", + "content-hash": "1cde028b4d7fa9dedc85a5c1ebc5cba7", "packages": [ { "name": "apereo/phpcas", @@ -472,6 +472,61 @@ }, "time": "2023-04-16T18:19:15+00:00" }, + { + "name": "league/oauth2-google", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-google.git", + "reference": "1b01ba18ba31b29e88771e3e0979e5c91d4afe76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-google/zipball/1b01ba18ba31b29e88771e3e0979e5c91d4afe76", + "reference": "1b01ba18ba31b29e88771e3e0979e5c91d4afe76", + "shasum": "" + }, + "require": { + "league/oauth2-client": "^2.0", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "eloquent/phony-phpunit": "^6.0 || ^7.1", + "phpunit/phpunit": "^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Woody Gilk", + "email": "hello@shadowhand.com", + "homepage": "https://shadowhand.com" + } + ], + "description": "Google OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "keywords": [ + "Authentication", + "authorization", + "client", + "google", + "oauth", + "oauth2" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-google/issues", + "source": "https://github.com/thephpleague/oauth2-google/tree/4.0.1" + }, + "time": "2023-03-17T15:20:52+00:00" + }, { "name": "onelogin/php-saml", "version": "3.6.1", diff --git a/docs/08_extern_auth.md b/docs/08_extern_auth.md index a7b80ab1..429c0d9e 100644 --- a/docs/08_extern_auth.md +++ b/docs/08_extern_auth.md @@ -95,6 +95,43 @@ ycom_auth_oauth2_twitch|twitch|error_msg|[allowed returnTo domains: DomainA,Doma Zusätzlice Scopes lassen sich in der Datei `oauth2_twitch.php` als Array in der Variable `scopes` eintragen. Die Scopes müssen mit einem Komma getrennt werden. Weitere Scopes findet man hier: https://dev.twitch.tv/docs/authentication/scopes/ - z.B. `user:read:email` um die E-Mail-Adresse des Users zu erhalten (diese bringt der Provider aber bereits nativ mit). +## OAuth2 mit Google + +Mit der OAuth2 Authentifizierung via Google ist es möglich, sich mit einem Google-Account in der YCOM zu registrieren und einzuloggen. Dazu muss dieser Provider entsprechend vorbereitet sein. + +### Einrichtung + +Im ersten Schritt muss man eine App anlegen bei Google. Hierfür einmal zu https://console.developers.google.com/ wechseln. Dort über den Button "Projekt auswählen" eine neues Projekt erstellen. Anschließend die App bearbeiten und die folgenden Einstellungen vornehmen: + +- OAuth Zustimmungsbildschirm: Einstellungen vornehmen +-- Anwendungsname: Name der App +-- Nutzersupport-E-Mail: E-Mail-Adresse für Support +-- Anwendungslogo: Logo der App +-- Startseite der App: Deine Domain +-- Kontaktdaten des Entwicklers: E-Mail-Adresse für Support +-- Autorisierte Domains: Deine Domain (optional für GSuite Nutzer) +-- Bereiche: +--- ./auth/userinfo.email (E-Mail-Adresse des Users) +--- ./auth/userinfo.profile (Profilinformationen des Users) + +Anschließend auf Anmeldedaten klicken und die folgenden Einstellungen vornehmen: +- Anmeldedaten erstellen: OAuth 2.0 Client IDs erstellen +-- Autorisierte JavaScript-Quellen: Deine Domain +-- Autorisierte Weiterleitungs-URIs: https://your-url.com/maybe-a-subpage/?rex_ycom_auth_mode=oauth2_google&rex_ycom_auth_func=code + +Danach kann die Client ID und der Clientschlüssel kopiert oder als JSON Datei heruntergeladen werden. + +In den Ordner `redaxo/data/addons/ycom/` sollte bereits die Datei `oauth2_google.php` kopiert worden sein. Diese Datei muss nun entsprechend angepasst werden mit den kopierten Daten. + +Damit die Authentifizierung funktioniert, muss im Loginformular von YCOM folgender String (angepasst auf die eigenen Bedürfnisse) eingefügt werden: + +```php +ycom_auth_oauth2_google|label|error_msg|[allowed returnTo domains: DomainA,DomainB]|default Userdata as Json{"ycom_groups": 3, "termsofuse_accepted": 1}|direct_link 0,1 +``` + +#### GSuite / Google Workspace Nutzer +In der Datei `oauth2_google.php` kann die Variable `hostedDomain` mit der Domain des GSuite / Google Workspace Accounts befüllt werden. Damit wird die Anmeldung auf Nutzer mit dieser Domain beschränkt. + ## Allgemeines diff --git a/plugins/auth/boot.php b/plugins/auth/boot.php index 7601b934..b4848362 100644 --- a/plugins/auth/boot.php +++ b/plugins/auth/boot.php @@ -52,6 +52,9 @@ case 'oauth2_twitch': $data = rex_extension::registerPoint(new rex_extension_point('YCOM_AUTH_OAUTH2_TWITCH_MATCHING', $data, ['Userdata' => $Userdata])); break; + case 'oauth2_google': + $data = rex_extension::registerPoint(new rex_extension_point('YCOM_AUTH_OAUTH2_GOOGLE_MATCHING', $data, ['Userdata' => $Userdata])); + break; case 'saml': $data = rex_extension::registerPoint(new rex_extension_point('YCOM_AUTH_SAML_MATCHING', $data, ['Userdata' => $Userdata])); break; diff --git a/plugins/auth/install.php b/plugins/auth/install.php index 1771fe73..547d96fd 100644 --- a/plugins/auth/install.php +++ b/plugins/auth/install.php @@ -46,7 +46,7 @@ rex_sql::factory()->setQuery('UPDATE rex_article SET `ycom_auth_type` = `ycom_auth_type` -1'); } -foreach (['saml', 'oauth2', 'oauth2_twitch', 'cas'] as $settingType) { +foreach (['saml', 'oauth2', 'oauth2_twitch', 'oauth2_google', 'cas'] as $settingType) { $pathFrom = __DIR__ . '/install/' . $settingType . '.php'; $pathTo = rex_addon::get('ycom')->getDataPath($settingType . '.php'); if (!file_exists($pathTo)) { diff --git a/plugins/auth/install/oauth2_google.php b/plugins/auth/install/oauth2_google.php new file mode 100644 index 00000000..023534ea --- /dev/null +++ b/plugins/auth/install/oauth2_google.php @@ -0,0 +1,8 @@ + 'someJibberish', // Go to https://console.cloud.google.com/ and setup a project. Setup an OAuth consent screen and choose scopes + 'clientSecret' => 'moreJibbereish', // In your project at https://console.cloud.google.com/, click on "Credential" and then "Create credentials => OAuth client ID". Fill out, add return url, get ID and secret + 'redirectUri' => 'https://your-url.com/maybe-a-subpage/?rex_ycom_auth_mode=oauth2_google&rex_ycom_auth_func=code' // do not fill out first and wait for the login error message to fill it out + //'hostedDomain' => 'your-url.com', // optional; used to restrict access to users on your G Suite/Google Apps for Business accounts +]; diff --git a/plugins/auth/lib/yform/trait_value_auth_oauth2_google.php b/plugins/auth/lib/yform/trait_value_auth_oauth2_google.php new file mode 100644 index 00000000..fde25810 --- /dev/null +++ b/plugins/auth/lib/yform/trait_value_auth_oauth2_google.php @@ -0,0 +1,164 @@ +auth_ClassKey.'.php'; + $SettingsPath = rex_addon::get('ycom')->getDataPath($SettingFile); + if (!file_exists($SettingsPath)) { + throw new rex_exception($this->auth_ClassKey . '-Settings file not found ['.$SettingsPath.']'); + } + + $settings = []; + include $SettingsPath; + return $settings; + } + + private function auth_getReturnTo(): string + { + $returnTos = []; + $returnTos[] = rex_request('returnTo', 'string'); // wenn returnTo übergeben wurde, diesen nehmen + $returnTos[] = rex_getUrl(rex_config::get('ycom/auth', 'article_id_jump_ok'), '', [], '&'); // Auth Ok -> article_id_jump_ok / Current Language will be selected + return rex_ycom_auth::getReturnTo($returnTos, ('' == $this->getElement(3)) ? [] : explode(',', $this->getElement(3))); + } + + private function auth_FormOutput(string $url): void + { + if ($this->needsOutput()) { + $this->params['form_output'][$this->getId()] = $this->parse(['value.ycom_auth_' . $this->auth_ClassKey. '.tpl.php', 'value.ycom_auth_extern.tpl.php'], [ + 'url' => $url, + 'name' => '{{ ' . $this->auth_ClassKey. '_auth }}', + ]); + } + } + + private function auth_redirectToFailed(string $message = ''): string + { + if ($this->params['debug']) { + dump($message); + return $message; + } + if ($this->auth_directLink) { + rex_response::sendCacheControl(); + rex_response::sendRedirect(rex_getUrl(rex_config::get('ycom/auth', 'article_id_jump_not_ok'))); + } + return ''; + } + + /** + * @param array $Userdata + * @param string $returnTo + * @throws rex_exception + * @return void + */ + private function auth_createOrUpdateYComUser(array $Userdata, string $returnTo): void + { + $defaultUserAttributes = []; + if ('' != $this->getElement(4)) { + if (null == $defaultUserAttributes = json_decode($this->getElement(4), true)) { + throw new rex_exception($this->auth_ClassKey . '-DefaultUserAttributes is not a json'.$this->getElement(4)); + } + } + + $data = []; + $data['email'] = ''; + foreach (['User.email', 'emailAddress', 'email'] as $Key) { + if (isset($Userdata[$Key])) { + $data['email'] = is_array($Userdata[$Key]) ? implode(' ', $Userdata[$Key]) : $Userdata[$Key]; + } + } + + $data['firstname'] = ''; + foreach (['given_name'] as $Key) { + if (isset($Userdata[$Key])) { + $data['firstname'] = is_array($Userdata[$Key]) ? implode(' ', $Userdata[$Key]) : $Userdata[$Key]; + } + } + + $data['name'] = ''; + foreach (['family_name'] as $Key) { + if (isset($Userdata[$Key])) { + $data['name'] = is_array($Userdata[$Key]) ? implode(' ', $Userdata[$Key]) : $Userdata[$Key]; + } + } + + $data['user_image'] = ''; + foreach (['picture'] as $Key) { + if (isset($Userdata[$Key])) { + $data['user_image'] = is_array($Userdata[$Key]) ? implode(' ', $Userdata[$Key]) : $Userdata[$Key]; + } + } + + foreach ($defaultUserAttributes as $defaultUserAttributeKey => $defaultUserAttributeValue) { + $data[$defaultUserAttributeKey] = $defaultUserAttributeValue; + } + + $data = rex_extension::registerPoint(new rex_extension_point('YCOM_AUTH_MATCHING', $data, ['Userdata' => $Userdata, 'AuthType' => $this->auth_ClassKey])); + + self::auth_clearUserSession(); + + // not logged in - check if available + $params = [ + 'loginName' => $data['email'], + 'loginStay' => true, + 'filter' => 'status > 0', + 'ignorePassword' => true, + ]; + + $loginStatus = rex_ycom_auth::login($params); + if (2 == $loginStatus) { + // already logged in + rex_ycom_user::updateUser($data); + rex_response::sendCacheControl(); + rex_response::sendRedirect($returnTo); + } + + // if user not found, check if exists, but no permission + $user = rex_ycom_user::query()->where('email', $data['email'])->findOne(); + if ($user) { + $this->auth_redirectToFailed('{{ ' . $this->auth_ClassKey . '.error.ycom_login_failed }}'); + $this->params['warning_messages'][] = ('' != $this->getElement(2)) ? $this->getElement(2) : '{{ ' . $this->auth_ClassKey . '.error.ycom_login_failed }}'; + return; + } + + $user = rex_ycom_user::createUserByEmail($data); + if (!$user || count($user->getMessages()) > 0) { + if ($user && $this->params['debug']) { + dump($user->getMessages()); + } + $this->auth_redirectToFailed('{{ ' . $this->auth_ClassKey . '.error.ycom_create_user }}'); + $this->params['warning_messages'][] = ('' != $this->getElement(2)) ? $this->getElement(2) : '{{ ' . $this->auth_ClassKey . '.error.ycom_create_user }}'; + return; + } + + $params = []; + $params['loginName'] = $user->getValue('email'); + $params['ignorePassword'] = true; + $loginStatus = rex_ycom_auth::login($params); + + if (2 != $loginStatus) { + if ($this->params['debug']) { + dump($loginStatus); + dump($user); + } + $this->auth_redirectToFailed('{{ ' . $this->auth_ClassKey . '.error.ycom_login_created_user }}'); + $this->params['warning_messages'][] = ('' != $this->getElement(2)) ? $this->getElement(2) : '{{ ' . $this->auth_ClassKey . '.error.ycom_login_created_user }}'; + return; + } + + rex_response::sendCacheControl(); + rex_response::sendRedirect($returnTo); + } + + private function auth_clearUserSession(): void + { + foreach ($this->auth_SessionVars as $SessionKey) { + rex_ycom_auth::unsetSessionVar($SessionKey); + } + } +} diff --git a/plugins/auth/lib/yform/value/ycom_auth_oauth2_google.php b/plugins/auth/lib/yform/value/ycom_auth_oauth2_google.php new file mode 100644 index 00000000..352d95a5 --- /dev/null +++ b/plugins/auth/lib/yform/value/ycom_auth_oauth2_google.php @@ -0,0 +1,137 @@ +www.yakamara.de + */ + +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; +use League\OAuth2\Client\Provider\Google; + +class rex_yform_value_ycom_auth_oauth2_google extends rex_yform_value_abstract +{ + use rex_yform_trait_value_auth_oauth2_google; + + /** + * @var array|string[] + */ + private array $auth_requestFunctions = ['init', 'code', 'state']; + private bool $auth_directLink = false; + /** + * @var array|string[] + */ + private array $auth_SessionVars = ['OAUTH2_oauth2state']; + private string $auth_ClassKey = 'oauth2_google'; + + public function enterObject(): void + { + if (rex::isFrontend()) { + $this->auth_directLink = 1 == $this->getElement(5) ? true : false; + } + + rex_login::startSession(); + + $settings = $this->auth_loadSettings(); + $returnTo = $this->auth_getReturnTo(); + $this->auth_FormOutput(rex_getUrl('', '', ['rex_ycom_auth_mode' => 'oauth2_google', 'rex_ycom_auth_func' => 'init', 'returnTo' => $returnTo])); + + $requestMode = rex_request('rex_ycom_auth_mode', 'string', ''); + $requestFunction = rex_request('rex_ycom_auth_func', 'string', ''); + if (!in_array($requestFunction, $this->auth_requestFunctions, true) || $this->auth_ClassKey != $requestMode) { + if ($this->auth_directLink) { + $requestFunction = 'init'; + } else { + return; + } + } + + if ('' == $settings['redirectUri']) { + echo 'use this URL for redirect'; + dump(rex_yrewrite::getFullUrlByArticleId(rex_article::getCurrentId(), '', ['rex_ycom_auth_mode' => 'oauth2_google', 'rex_ycom_auth_func' => 'code'], '&')); + return; + } + + $provider = new Google($settings); + + $Userdata = []; + switch ($requestFunction) { + case 'code': + $code = rex_request('code', 'string'); + if ('' != $code) { + if ('' == rex_ycom_auth::getSessionVar('OAUTH2_oauth2state') || empty($_GET['state']) || $_GET['state'] != rex_ycom_auth::getSessionVar('OAUTH2_oauth2state')) { + if ($this->params['debug']) { + echo 'OAuth session saved state != OAuth State'; + dump(rex_ycom_auth::getSessionVar('OAUTH2_oauth2state')); + dump($_GET['state']); + } + $this->auth_redirectToFailed('{{ oauth.error.state_code }}'); + $this->params['warning_messages'][] = ('' != $this->getElement(3)) ? $this->getElement(3) : '{{ oauth.error.state_code }}'; + return; + } + + $accessToken = null; + try { + /** @var \League\OAuth2\Client\Token\AccessToken $accessToken */ + $accessToken = $provider->getAccessToken('authorization_code', [ + 'code' => $code, + ]); + + if ($accessToken->hasExpired()) { + $this->auth_redirectToFailed('{{ oauth.error.access_expired }}'); + $this->params['warning_messages'][] = ('' != $this->getElement(3)) ? $this->getElement(3) : '{{ oauth.error.access_expired }}'; + return; + } + $resourceOwner = $provider->getResourceOwner($accessToken); + $Userdata = $resourceOwner->toArray(); + // print_r($Userdata); + // exit; + $returnTo = rex_ycom_auth::getSessionVar('OAUTH2_oauth2returnTo'); + rex_ycom_auth::unsetSessionVar('OAUTH2_oauth2returnTo'); + } catch (IdentityProviderException $e) { + if ($this->params['debug']) { + dump($e); + } + $this->auth_redirectToFailed('{{ oauth.error.code }}'); + $this->params['warning_messages'][] = ('' != $this->getElement(3)) ? $this->getElement(3) : '{{ oauth.error.code }}'; + return; + } catch (Exception $e) { + if ($this->params['debug']) { + echo 'OAuth Error'; + dump($accessToken); + dump($e); + dump($e->getMessage()); + } + $this->auth_redirectToFailed('{{ oauth.error.code }}'); + $this->params['warning_messages'][] = ('' != $this->getElement(3)) ? $this->getElement(3) : '{{ oauth.error.code }}'; + return; + } + } else { + $this->auth_redirectToFailed('{{ oauth.error.no_code }}'); + $this->params['warning_messages'][] = ('' != $this->getElement(3)) ? $this->getElement(3) : '{{ oauth.error.no_code }}'; + return; + } + break; + + case 'init': + $authorizationUrl = $provider->getAuthorizationUrl(); + rex_ycom_auth::setSessionVar('OAUTH2_oauth2state', $provider->getState()); + rex_ycom_auth::setSessionVar('OAUTH2_oauth2returnTo', $returnTo); + rex_response::sendCacheControl(); + rex_response::sendRedirect($authorizationUrl); + } + + $this->auth_createOrUpdateYComUser($Userdata, $returnTo); + } + + public function getDescription(): string + { + return 'ycom_auth_oauth2_google|label|error_msg|[allowed returnTo domains: DomainA,DomainB]|default Userdata as Json{"ycom_groups": 3, "termsofuse_accepted": 1}|direct_link 0,1'; + } +} diff --git a/plugins/auth/ytemplates/bootstrap/value.ycom_auth_oauth2_google.tpl.php b/plugins/auth/ytemplates/bootstrap/value.ycom_auth_oauth2_google.tpl.php new file mode 100644 index 00000000..8b64d548 --- /dev/null +++ b/plugins/auth/ytemplates/bootstrap/value.ycom_auth_oauth2_google.tpl.php @@ -0,0 +1,11 @@ +name .'" href="'.$url.'">' . $this->name . ''; \ No newline at end of file