diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..5e1c513
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,26 @@
+# editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.py]
+indent_size = 4
+
+[*.yml]
+indent_size = 4
+
+[*.php]
+indent_size = 4
+
+[*.json]
+indent_size = 4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c5cd487
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+# IDE & OS
+.idea/
+.DS_Store
+
+# Project
+vendor
+composer.phar
+composer.lock
+phpunit.xml
+Tests/Controller/App/*/
+Tests/Controller/App/sqlite.db.cache
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..38e9534
--- /dev/null
+++ b/README.md
@@ -0,0 +1,47 @@
+Contao Discourse SSO Bundle
+=============================
+
+Contao Discourse SSO Bundle for Symfony
+
+Installation
+------------
+
+### Step 1: Download the Bundle
+
+Open a command console, enter your project directory and execute the
+following command to download the latest stable version of this bundle:
+
+```bash
+$ composer require craffft/contao-discourse "~2.0"
+```
+
+This command requires you to have Composer installed globally, as explained
+in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
+of the Composer documentation.
+
+### Step 2: Enable the Bundle
+
+Then, enable the bundle by adding it to the list of registered bundles
+in the `app/AppKernel.php` file of your project:
+
+```php
+
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- *
- * PHP version 5
- * @copyright Florian Bender 2015
- * @author Florian Bender
- * @package Discourse
- * @license AGPLv3 (GNU Affero GPL v3.0)
- * @filesource
- */
-// based on github.com/cviebrock/discourse-php
-// @see https://raw.githubusercontent.com/cviebrock/discourse-php/master/src/SSOHelper.php
-// @license (TBD)
-
-class SSOProviderPayload {
-
- /**
- * Endpoint which receives SSO response; add host and query like this:
- * http://discourse_site.tld{API_ENDPOINT}?sso={PAYLOAD}&sig={SIG}
- * @var string
- */
- const API_ENDPOINT = '/session/sso_login';
-
- /**
- * Secret used for signing payload data
- * @var string
- */
- private $strSignatureSecret = '';
-
- /**
- * Nonce retrieved from challenge payload
- * @var string
- */
- protected $strPayloadNonce = '';
-
- /**
- * Initalize the object and set signature secret
- * @param string $strSecret Shared secret used for the payload signature
- */
- public function __construct($strSecret) {
- $this->strSignatureSecret = $strSecret;
- }
-
- /**
- * Check signature (and thus integrity) of payload
- * @return boolean
- */
- public function isPayloadValid($strPayload, $strSignature) {
- return ($this->getPayloadSignature($strPayload) === $strSignature);
- }
-
- /**
- * Validate and parse payload as well as retrieve and store nonce
- * @param string $strPayload Challenge payload (must be urldecode()d!)
- * @param string $strSignature The payload's signature
- * @return true
- * @throws \Exception
- */
- public function parseChallengePayload($strPayload, $strSignature) {
- if (!$this->isPayloadValid($strPayload, $strSignature)) {
- throw new \Exception('Payload could not be validated against signature (Payload: "'.$strPayload.'", Signature: "'.$strSignature.'")');
- }
- // parse payload
- $arrPayloadData = array();
- parse_str(base64_decode($strPayload), $arrPayloadData);
- // retrieve nonce
- if (!array_key_exists('nonce', $arrPayloadData)) {
- throw new \Exception('Invalid payload: Nonce not found');
- }
- $this->strPayloadNonce = $arrPayloadData['nonce'];
- return true;
- }
-
- /**
- * Generate and return response payload with signature ready for http_build_query()
- * @see self::generateResponsePayload
- * @param string $strUserId (External) user ID
- * @param string $strUserEmail E-mail address of user
- * @param array $arrOptionalParameters More parameters to include in payload
- * @todo Use func_get_args resp. http://php.net/manual/functions.arguments.html#functions.variable-arg-list
- */
- public function getResponseDataForUser($strUserId, $strUserEmail, $arrOptionalParameters = array()) {
- $arrPayloadData = array(
- // 'nonce' => $this->strPayloadNonce,
- 'external_id' => $strUserId,
- 'email' => $strUserEmail
- );
- $arrPayloadData = array_merge($arrPayloadData, $arrOptionalParameters);
- $strPayload = $this->generateResponsePayload($arrPayloadData);
- return array(
- 'sso' => $strPayload,
- 'sig' => $this->getPayloadSignature($strPayload)
- );
- }
-
- /**
- * Generate and return response payload using nonce from challenge payload
- * @param array $arrPayloadParameters Parameters to include in payload
- * @return string
- * @todo Check input array for required / valid values?
- * @todo Consider making this protected
- */
- public function generateResponsePayload($arrPayloadParameters) {
- // $arrPayloadParameters required values: nonce, email, external_id
- // … optional values: 'username', (full) 'name', 'avatar_url',
- // 'require_activation', 'custom.*' (custom fields), etc.
- // augment payload data with nonce
- $arrPayloadParameters['nonce'] = $this->strPayloadNonce;
- // create & return payload string
- return base64_encode(http_build_query($arrPayloadParameters));
- }
-
- /**
- * Return signature of payload using secret
- * @param string $strPayload
- * @return string
- * @todo Consider making this protected
- */
- public function getPayloadSignature($strPayload) {
- return hash_hmac('sha256', $strPayload, $this->strSignatureSecret);
- }
-
-}
diff --git a/TL_ROOT/system/modules/discourse/ModuleSSOProvider.php b/TL_ROOT/system/modules/discourse/ModuleSSOProvider.php
deleted file mode 100644
index 6c62095..0000000
--- a/TL_ROOT/system/modules/discourse/ModuleSSOProvider.php
+++ /dev/null
@@ -1,131 +0,0 @@
-
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- *
- * PHP version 5
- * @copyright Florian Bender 2015
- * @author Florian Bender
- * @package Discourse
- * @license AGPLv3 (GNU Affero GPL v3.0)
- * @filesource
- */
-
-
-require_once(TL_ROOT . '/plugins/Discourse/SSOProviderPayload.php');
-
-/**
- * Class ModuleSSOProvider
- *
- * Module to provide user authentication for Discourse instances against the
- * user database of a Contao instance.
- * @copyright Florian Bender 2015
- * @author Florian Bender
- * @package Discourse
- */
-class ModuleSSOProvider extends Module
-{
-
- /**
- * Template
- * @var string
- */
- protected $strTemplate;
-
-
- /**
- * Validate the current user and redirect (if permissions allow).
- * @return string
- */
- public function generate()
- {
-
- // Show placeholder in Backend
- if (TL_MODE == 'BE')
- {
- $objTemplate = new BackendTemplate('be_wildcard');
-
- $objTemplate->wildcard = '### DISCOURSE SSO PROVIDER MODULE ###';
- $objTemplate->title = $this->headline;
- $objTemplate->id = $this->id;
- $objTemplate->link = $this->name;
- $objTemplate->href = 'contao/main.php?do=themes&table=tl_module&act=edit&id=' . $this->id;
-
- return $objTemplate->parse();
- }
-
- // Return nothing if necessary parameters were not provided
- if (!isset($_GET['sso']) || !isset($_GET['sig'])) {
- return ''; // TODO: return error? log??
- }
-
- // FIX: use raw data instead of sanitized data from Contao Input class
- $strSSOPayload = urldecode($_GET['sso']);
- $strSSOSignature = $_GET['sig'];
-
- // TODO: Redirect to current URL (without sso/sig parameters) if user is not logged in or no payload was provided
- if (!FE_USER_LOGGED_IN || empty($strSSOPayload) || empty($strSSOSignature)) {
- return '';
- }
-
- $objSSOPayload = new \fbender\Discourse\SSOProviderPayload($GLOBALS['TL_CONFIG']['discourseSSOSecret']);
- $objSSOPayload->parseChallengePayload($strSSOPayload, $strSSOSignature); // TODO: catch exception?
-
- $this->import('FrontendUser', 'User');
-
- // TODO: add moderator group support
- // optional values: 'username', (full) 'name', 'avatar_url',
- // 'require_activation', 'custom.*' (custom fields), etc.
- $arrParameters = array(
- 'name' => $this->User->firstname.' '.$this->User->lastname,
- // 'avatar_url' => $this->User->portrait,
- // 'custom.xyz' => '', // see Discourse Plugins & Discourse, Admin, Customize, User Fields; https://meta.discourse.org/t/custom-user-fields-for-plugins/14956
- // 'admin' => 0,
- 'moderator' => 0
- );
- // TODO: reduce amount of data being logged?
- $this->log('User "' . $this->User->username . '" used SSO ('.json_encode($arrParameters).')', get_class($this) . ' generate()', TL_ACCESS);
- $arrResponseData = $objSSOPayload->getResponseDataForUser($this->User->id, $this->User->email, $arrParameters);
-
- // create redirect URL
- $arrDiscourseHostParts = parse_url($GLOBALS['TL_CONFIG']['discourseSSOHost']);
-
- if ($arrDiscourseHostParts === false || !isset($arrDiscourseHostParts['scheme']) || !isset($arrDiscourseHostParts['host'])) {
- throw new Exception("Invalid setting: 'discourseSSOHost' (must be a valid URL including protocol)");
- }
-
- $strDiscourseSSOEndpoint = $arrDiscourseHostParts['scheme'] . '://' . $arrDiscourseHostParts['host'];
- $strDiscourseSSOEndpoint .= $objSSOPayload::API_ENDPOINT;
- $strDiscourseSSOEndpoint .= '?' . http_build_query($arrResponseData);
-
- $this->redirect($strDiscourseSSOEndpoint);
-
- return '';
- }
-
- /**
- * Generate module
- */
- protected function compile()
- {
- return;
- }
-
-}
-
-
-#EOF
diff --git a/TL_ROOT/system/modules/discourse/config/config.php b/TL_ROOT/system/modules/discourse/config/config.php
deleted file mode 100644
index db19c5a..0000000
--- a/TL_ROOT/system/modules/discourse/config/config.php
+++ /dev/null
@@ -1,37 +0,0 @@
-
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- *
- * PHP version 5
- * @copyright Florian Bender 2015
- * @author Florian Bender
- * @package Discourse
- * @license AGPLv3 (GNU Affero GPL v3.0)
- * @filesource
- */
-
-
-/**
- * -------------------------------------------------------------------------
- * FRONT END MODULES
- * -------------------------------------------------------------------------
- */
-$GLOBALS['FE_MOD']['application']['discourseSSOProvider'] = 'ModuleSSOProvider';
-
-
- #EOF
diff --git a/TL_ROOT/system/modules/discourse/dca/tl_module.php b/TL_ROOT/system/modules/discourse/dca/tl_module.php
deleted file mode 100644
index 48b640c..0000000
--- a/TL_ROOT/system/modules/discourse/dca/tl_module.php
+++ /dev/null
@@ -1,31 +0,0 @@
-
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- *
- * PHP version 5
- * @copyright Florian Bender 2015
- * @author Florian Bender
- * @package Discourse
- * @license AGPLv3 (GNU Affero GPL v3.0)
- * @filesource
- */
-
-$GLOBALS['TL_DCA']['tl_module']['palettes']['discourseSSOProvider'] = '{title_legend},name;{protected_legend},protected;{expert_legend:hide},guests,cssID';
-
-
-#EOF
diff --git a/TL_ROOT/system/modules/discourse/dca/tl_settings.php b/TL_ROOT/system/modules/discourse/dca/tl_settings.php
deleted file mode 100644
index b58a7b4..0000000
--- a/TL_ROOT/system/modules/discourse/dca/tl_settings.php
+++ /dev/null
@@ -1,64 +0,0 @@
-
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- *
- * PHP version 5
- * @copyright Florian Bender 2015
- * @author Florian Bender
- * @package Discourse
- * @license AGPLv3 (GNU Affero GPL v3.0)
- * @filesource
- */
-
-/*
- * dca: tl_settings
- */
-$GLOBALS['TL_DCA']['tl_settings']['palettes']['default'] .= ';{discourse_legend},discourseSSOHost,discourseSSOSecret';
-
-$GLOBALS['TL_DCA']['tl_settings']['fields']['discourseSSOHost'] = array
-(
- 'label' => &$GLOBALS['TL_LANG']['tl_settings']['discourseSSOHost'],
- 'exclude' => true,
- 'inputType' => 'text',
- 'eval' => array('rgxp'=>'url', 'decodeEntities'=>true, 'tl_class'=>'w50'),
- 'save_callback' => array(
- array('tl_settings_discourse', 'validateURL')
- )
-);
-
-$GLOBALS['TL_DCA']['tl_settings']['fields']['discourseSSOSecret'] = array
-(
- 'label' => &$GLOBALS['TL_LANG']['tl_settings']['discourseSSOSecret'],
- 'exclude' => true,
- 'inputType' => 'text',
- 'eval' => array('decodeEntities'=>false, 'tl_class'=>'w50')
-);
-
-
-class tl_settings_discourse extends tl_settings {
- public function validateURL($varValue) {
- $varValue = $this->idnaEncodeUrl($varValue); // method of System class
- if (filter_var($varValue, FILTER_VALIDATE_URL) === false) {
- throw new Exception('Not a valid URL: ' + $varValue);
- }
- return $varValue;
- }
-}
-
-
-#EOF
diff --git a/TL_ROOT/system/modules/discourse/languages/de/modules.php b/TL_ROOT/system/modules/discourse/languages/de/modules.php
deleted file mode 100644
index 7a97be2..0000000
--- a/TL_ROOT/system/modules/discourse/languages/de/modules.php
+++ /dev/null
@@ -1,42 +0,0 @@
-
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation, either
- * version 3 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, please visit the Free
- * Software Foundation website at .
- *
- * PHP version 5
- * @copyright Florian Bender 2015
- * @author Florian Bender
- * @package Discourse
- * @license LGPL
- * @filesource
- */
-
-
-/**
- * Extension folder
- */
-$GLOBALS['TL_LANG']['MOD']['discourse'] = array('Discourse-Anbindung');
-
-
-/**
- * Front end modules
- */
-$GLOBALS['TL_LANG']['FMD']['discourseSSOProvider'] = array('Discourse SSO Provider', 'Dieses Modul ermöglicht einen Single Sign-On von einer Discourse-Installation. Nach erfolgreicher Authentisierung wir der Nutzer auf den Discourse Host (s. Contao Einstellungen) weitergleitet. Das Modul erzeugt keine Ausgabe (ähnlich dem "Logout"-Modul).');
-
-
-#EOF
diff --git a/TL_ROOT/system/modules/discourse/languages/de/tl_settings.php b/TL_ROOT/system/modules/discourse/languages/de/tl_settings.php
deleted file mode 100644
index 76cbfa7..0000000
--- a/TL_ROOT/system/modules/discourse/languages/de/tl_settings.php
+++ /dev/null
@@ -1,42 +0,0 @@
-
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation, either
- * version 3 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, please visit the Free
- * Software Foundation website at .
- *
- * PHP version 5
- * @copyright Florian Bender 2015
- * @author Florian Bender
- * @package Discourse
- * @license LGPL
- * @filesource
- */
-
-
-/**
- * Fields
- */
-$GLOBALS['TL_LANG']['tl_settings']['discourseSSOHost'] = array('Host-Adresse', 'Host-Adresse der Discourse-Installation, für die Single Sign-On angeboten werden soll.');
-$GLOBALS['TL_LANG']['tl_settings']['discourseSSOSecret'] = array('SSO Secret', '"Shared Secret" des Single Sign-On für die Discourse-Installation, für die Single Sign-On angeboten werden soll.');
-
-/**
- * Legends
- */
-$GLOBALS['TL_LANG']['tl_settings']['discourse_legend'] = 'Discourse Einstellungen';
-
-
-#EOF
diff --git a/TL_ROOT/system/modules/discourse/languages/en/modules.php b/TL_ROOT/system/modules/discourse/languages/en/modules.php
deleted file mode 100644
index c4977eb..0000000
--- a/TL_ROOT/system/modules/discourse/languages/en/modules.php
+++ /dev/null
@@ -1,42 +0,0 @@
-
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation, either
- * version 3 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, please visit the Free
- * Software Foundation website at .
- *
- * PHP version 5
- * @copyright Florian Bender 2015
- * @author Florian Bender
- * @package Discourse
- * @license LGPL
- * @filesource
- */
-
-
-/**
- * Extension folder
- */
-$GLOBALS['TL_LANG']['MOD']['discourse'] = array('Discourse Connector');
-
-
-/**
- * Front end modules
- */
-$GLOBALS['TL_LANG']['FMD']['discourseSSOProvider'] = array('Discourse SSO Provider', 'This module enables Single Sign-On of a Discourse installation. Users will be redirected to the Discourse Host (see Contao Settings) after successful authentication. This module does not produce any output (similar to the "Logout" module).');
-
-
-#EOF
diff --git a/TL_ROOT/system/modules/discourse/languages/en/tl_settings.php b/TL_ROOT/system/modules/discourse/languages/en/tl_settings.php
deleted file mode 100644
index ff0a443..0000000
--- a/TL_ROOT/system/modules/discourse/languages/en/tl_settings.php
+++ /dev/null
@@ -1,42 +0,0 @@
-
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation, either
- * version 3 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, please visit the Free
- * Software Foundation website at .
- *
- * PHP version 5
- * @copyright Florian Bender 2015
- * @author Florian Bender
- * @package Discourse
- * @license LGPL
- * @filesource
- */
-
-
-/**
- * Fields
- */
-$GLOBALS['TL_LANG']['tl_settings']['discourseSSOHost'] = array('Host Address', 'Host address of the Discourse installation, for which Single Sign-On will be provided.');
-$GLOBALS['TL_LANG']['tl_settings']['discourseSSOSecret'] = array('SSO Secret', 'Single Sign-On "Shared Secret" of the Discourse installation, for which Single Sign-On will be provided.');
-
-/**
- * Legends
- */
-$GLOBALS['TL_LANG']['tl_settings']['discourse_legend'] = 'Discourse settings';
-
-
-#EOF
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..4685f3a
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,40 @@
+{
+ "name": "craffft/contao-discourse",
+ "type": "symfony-bundle",
+ "description": "ContaoDiscourseSSOBundle for Symfony with Contao",
+ "keywords": [
+ "discourse",
+ "SSO",
+ "authentication"
+ ],
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Florian Bender",
+ "email": "fb+git@quantumedia.de"
+ },
+ {
+ "name": "Daniel Kiesel",
+ "homepage": "https://github.com/iCodr8"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/Craffft/contao-discourse/issues",
+ "source": "https://github.com/Craffft/contao-discourse"
+ },
+ "require": {
+ "php": ">=7.1",
+ "contao/core-bundle": "~4.4"
+ },
+ "require-dev": {
+ "contao/manager-plugin": "^2.0"
+ },
+ "extra": {
+ "contao-manager-plugin": "Craffft\\ContaoDiscourseSSOBundle\\ContaoManager\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "Craffft\\ContaoDiscourseSSOBundle\\": "src/"
+ }
+ }
+}
diff --git a/src/ContaoManager/Plugin.php b/src/ContaoManager/Plugin.php
new file mode 100644
index 0000000..84d5dab
--- /dev/null
+++ b/src/ContaoManager/Plugin.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Craffft\ContaoDiscourseSSOBundle\ContaoManager;
+
+use Contao\CoreBundle\ContaoCoreBundle;
+use Contao\ManagerPlugin\Bundle\BundlePluginInterface;
+use Contao\ManagerPlugin\Bundle\Config\BundleConfig;
+use Contao\ManagerPlugin\Bundle\Parser\ParserInterface;
+use Craffft\ContaoDiscourseSSOBundle\CraffftContaoDiscourseSSOBundle;
+
+class Plugin implements BundlePluginInterface
+{
+ public function getBundles(ParserInterface $parser)
+ {
+ return [
+ BundleConfig::create(CraffftContaoDiscourseSSOBundle::class)
+ ->setLoadAfter([ContaoCoreBundle::class])
+ ->setReplace(['discourse-sso']),
+ ];
+ }
+}
diff --git a/src/CraffftContaoDiscourseSSOBundle.php b/src/CraffftContaoDiscourseSSOBundle.php
new file mode 100644
index 0000000..6c0fd47
--- /dev/null
+++ b/src/CraffftContaoDiscourseSSOBundle.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Craffft\ContaoDiscourseSSOBundle;
+
+use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+class CraffftContaoDiscourseSSOBundle extends Bundle
+{
+}
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
new file mode 100644
index 0000000..1245da4
--- /dev/null
+++ b/src/DependencyInjection/Configuration.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Craffft\ContaoDiscourseSSOBundle\DependencyInjection;
+
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\ConfigurationInterface;
+
+/**
+ * This is the class that validates and merges configuration from your app/config files
+ *
+ * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
+ */
+class Configuration implements ConfigurationInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfigTreeBuilder()
+ {
+ $treeBuilder = new TreeBuilder();
+ $treeBuilder->root('craffft_contao_discourse_sso');
+
+ return $treeBuilder;
+ }
+}
diff --git a/src/DependencyInjection/CraffftContaoDiscourseSSOExtension.php b/src/DependencyInjection/CraffftContaoDiscourseSSOExtension.php
new file mode 100644
index 0000000..337f2df
--- /dev/null
+++ b/src/DependencyInjection/CraffftContaoDiscourseSSOExtension.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Craffft\ContaoDiscourseSSOBundle\DependencyInjection;
+
+use Symfony\Component\DependencyInjection\Container;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\Config\FileLocator;
+use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+use Symfony\Component\DependencyInjection\Loader;
+
+/**
+ * This is the class that loads and manages your bundle configuration
+ *
+ * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
+ */
+class CraffftContaoDiscourseSSOExtension extends Extension
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function load(array $configs, ContainerBuilder $container)
+ {
+ $configuration = new Configuration();
+ $config = $this->processConfiguration($configuration, $configs);
+
+ $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
+ $loader->load('services.yml');
+ }
+
+ /**
+ * Returns the recommended alias to use in XML.
+ *
+ * This alias is also the mandatory prefix to use when using YAML.
+ *
+ * @return string The alias
+ */
+ public function getAlias()
+ {
+ return 'craffft_contao_discourse_sso';
+ }
+}
diff --git a/src/FrontendModule/ModuleSSOProvider.php b/src/FrontendModule/ModuleSSOProvider.php
new file mode 100644
index 0000000..07b5f18
--- /dev/null
+++ b/src/FrontendModule/ModuleSSOProvider.php
@@ -0,0 +1,115 @@
+
+ * (c) Daniel Kiesel
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Craffft\ContaoDiscourseSSOBundle\FrontendModule;
+
+use Contao\BackendTemplate;
+use Contao\Module;
+
+/**
+ * Class ModuleSSOProvider
+ *
+ * Module to provide user authentication for Discourse instances against the
+ * user database of a Contao instance.
+ * @copyright Florian Bender 2015
+ * @author Florian Bender
+ * @author Daniel Kiesel
+ * @package Discourse
+ */
+class ModuleSSOProvider extends Module
+{
+ /**
+ * Template
+ * @var string
+ */
+ protected $strTemplate = '';
+
+ /**
+ * Validate the current user and redirect (if permissions allow).
+ * @return string
+ */
+ public function generate()
+ {
+ // Show placeholder in Backend
+ if (TL_MODE == 'BE') {
+ $objTemplate = new BackendTemplate('be_wildcard');
+
+ $objTemplate->wildcard = '### DISCOURSE SSO PROVIDER MODULE ###';
+ $objTemplate->title = $this->headline;
+ $objTemplate->id = $this->id;
+ $objTemplate->link = $this->name;
+ $objTemplate->href = 'contao/main.php?do=themes&table=tl_module&act=edit&id=' . $this->id;
+
+ return $objTemplate->parse();
+ }
+
+ // Return nothing if necessary parameters were not provided
+ if (!isset($_GET['sso']) || !isset($_GET['sig'])) {
+ return ''; // TODO: return error? log??
+ }
+
+ // FIX: use raw data instead of sanitized data from Contao Input class
+ $strSSOPayload = urldecode($_GET['sso']);
+ $strSSOSignature = $_GET['sig'];
+
+ // TODO: Redirect to current URL (without sso/sig parameters) if user is not logged in or no payload was provided
+ if (!FE_USER_LOGGED_IN || empty($strSSOPayload) || empty($strSSOSignature)) {
+ return '';
+ }
+
+ $container = \System::getContainer();
+ /** @var SSOProviderPayload $objSSOPayload */
+ $objSSOPayload = $container->get('craffft.sso.sso_provider_payload');
+ $objSSOPayload->setSignatureSecret($GLOBALS['TL_CONFIG']['discourseSSOSecret']);
+ $objSSOPayload->parseChallengePayload($strSSOPayload, $strSSOSignature); // TODO: catch exception?
+
+ $this->import('FrontendUser', 'User');
+
+ // TODO: add moderator group support
+ // optional values: 'username', (full) 'name', 'avatar_url',
+ // 'require_activation', 'custom.*' (custom fields), etc.
+ $arrParameters = array(
+ 'name' => $this->User->firstname . ' ' . $this->User->lastname,
+ // 'avatar_url' => $this->User->portrait,
+ // 'custom.xyz' => '', // see Discourse Plugins & Discourse, Admin, Customize, User Fields; https://meta.discourse.org/t/custom-user-fields-for-plugins/14956
+ // 'admin' => 0,
+ 'moderator' => 0
+ );
+ // TODO: reduce amount of data being logged?
+ $this->log('User "' . $this->User->username . '" used SSO (' . json_encode($arrParameters) . ')',
+ get_class($this) . ' generate()', TL_ACCESS);
+ $arrResponseData = $objSSOPayload->getResponseDataForUser($this->User->id, $this->User->email, $arrParameters);
+
+ // create redirect URL
+ $arrDiscourseHostParts = parse_url($GLOBALS['TL_CONFIG']['discourseSSOHost']);
+
+ if ($arrDiscourseHostParts === false || !isset($arrDiscourseHostParts['scheme']) || !isset($arrDiscourseHostParts['host'])) {
+ throw new Exception("Invalid setting: 'discourseSSOHost' (must be a valid URL including protocol)");
+ }
+
+ $strDiscourseSSOEndpoint = $arrDiscourseHostParts['scheme'] . '://' . $arrDiscourseHostParts['host'];
+ $strDiscourseSSOEndpoint .= $objSSOPayload::API_ENDPOINT;
+ $strDiscourseSSOEndpoint .= '?' . http_build_query($arrResponseData);
+
+ $this->redirect($strDiscourseSSOEndpoint);
+
+ return '';
+ }
+
+ /**
+ * Generate the module
+ */
+ protected function compile()
+ {
+ return;
+ }
+}
diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml
new file mode 100644
index 0000000..74cbb50
--- /dev/null
+++ b/src/Resources/config/services.yml
@@ -0,0 +1,5 @@
+services:
+
+ # SSO
+ craffft.sso.sso_provider_payload:
+ class: Craffft\ContaoDiscourseSSOBundle\SSO\SSOProviderPayload
diff --git a/src/Resources/contao/config/autoload.ini b/src/Resources/contao/config/autoload.ini
new file mode 100644
index 0000000..dd4f5c8
--- /dev/null
+++ b/src/Resources/contao/config/autoload.ini
@@ -0,0 +1,14 @@
+;;
+; Configure what you want the autoload creator to register
+;;
+register_namespaces = false
+register_classes = false
+register_templates = false
+
+;;
+; Override the default configuration for certain sub directories
+;;
+[vendor/*]
+register_namespaces = false
+register_classes = false
+register_templates = false
diff --git a/src/Resources/contao/config/config.php b/src/Resources/contao/config/config.php
new file mode 100644
index 0000000..5ad6e4c
--- /dev/null
+++ b/src/Resources/contao/config/config.php
@@ -0,0 +1,13 @@
+
+ * (c) Daniel Kiesel
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+$GLOBALS['FE_MOD']['application']['discourseSSOProvider'] = '\\Craffft\\ContaoDiscourseSSOBundle\\FrontendModule\\ModuleSSOProvider';
diff --git a/src/Resources/contao/dca/tl_module.php b/src/Resources/contao/dca/tl_module.php
new file mode 100644
index 0000000..6e9752f
--- /dev/null
+++ b/src/Resources/contao/dca/tl_module.php
@@ -0,0 +1,13 @@
+
+ * (c) Daniel Kiesel
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+$GLOBALS['TL_DCA']['tl_module']['palettes']['discourseSSOProvider'] = '{title_legend},name,type;{protected_legend},protected;{expert_legend:hide},guests,cssID';
diff --git a/src/Resources/contao/dca/tl_settings.php b/src/Resources/contao/dca/tl_settings.php
new file mode 100644
index 0000000..16f7688
--- /dev/null
+++ b/src/Resources/contao/dca/tl_settings.php
@@ -0,0 +1,46 @@
+
+ * (c) Daniel Kiesel
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+$GLOBALS['TL_DCA']['tl_settings']['palettes']['default'] .= ';{discourse_legend},discourseSSOHost,discourseSSOSecret';
+
+$GLOBALS['TL_DCA']['tl_settings']['fields']['discourseSSOHost'] = array
+(
+ 'label' => &$GLOBALS['TL_LANG']['tl_settings']['discourseSSOHost'],
+ 'exclude' => true,
+ 'inputType' => 'text',
+ 'eval' => array('rgxp'=>'url', 'decodeEntities'=>true, 'tl_class'=>'w50'),
+ 'save_callback' => array(
+ array('tl_settings_discourse', 'validateURL')
+ )
+);
+
+$GLOBALS['TL_DCA']['tl_settings']['fields']['discourseSSOSecret'] = array
+(
+ 'label' => &$GLOBALS['TL_LANG']['tl_settings']['discourseSSOSecret'],
+ 'exclude' => true,
+ 'inputType' => 'text',
+ 'eval' => array('decodeEntities'=>false, 'tl_class'=>'w50')
+);
+
+
+class tl_settings_discourse extends tl_settings
+{
+ public function validateURL($varValue)
+ {
+ $varValue = $this->idnaEncodeUrl($varValue); // method of System class
+ if (filter_var($varValue, FILTER_VALIDATE_URL) === false) {
+ throw new Exception('Not a valid URL: ' + $varValue);
+ }
+
+ return $varValue;
+ }
+}
diff --git a/src/Resources/contao/languages/de/modules.php b/src/Resources/contao/languages/de/modules.php
new file mode 100644
index 0000000..ee46557
--- /dev/null
+++ b/src/Resources/contao/languages/de/modules.php
@@ -0,0 +1,21 @@
+
+ * (c) Daniel Kiesel
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Extension folder
+ */
+$GLOBALS['TL_LANG']['MOD']['discourse'] = array('Discourse-Anbindung');
+
+/**
+ * Front end modules
+ */
+$GLOBALS['TL_LANG']['FMD']['discourseSSOProvider'] = array('Discourse SSO Provider', 'Dieses Modul ermöglicht einen Single Sign-On von einer Discourse-Installation. Nach erfolgreicher Authentisierung wir der Nutzer auf den Discourse Host (s. Contao Einstellungen) weitergleitet. Das Modul erzeugt keine Ausgabe (ähnlich dem "Logout"-Modul).');
diff --git a/src/Resources/contao/languages/de/tl_settings.php b/src/Resources/contao/languages/de/tl_settings.php
new file mode 100644
index 0000000..b116832
--- /dev/null
+++ b/src/Resources/contao/languages/de/tl_settings.php
@@ -0,0 +1,22 @@
+
+ * (c) Daniel Kiesel
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Fields
+ */
+$GLOBALS['TL_LANG']['tl_settings']['discourseSSOHost'] = array('Host-Adresse', 'Host-Adresse der Discourse-Installation, für die Single Sign-On angeboten werden soll.');
+$GLOBALS['TL_LANG']['tl_settings']['discourseSSOSecret'] = array('SSO Secret', '"Shared Secret" des Single Sign-On für die Discourse-Installation, für die Single Sign-On angeboten werden soll.');
+
+/**
+ * Legends
+ */
+$GLOBALS['TL_LANG']['tl_settings']['discourse_legend'] = 'Discourse Einstellungen';
diff --git a/src/Resources/contao/languages/en/modules.php b/src/Resources/contao/languages/en/modules.php
new file mode 100644
index 0000000..3989fa3
--- /dev/null
+++ b/src/Resources/contao/languages/en/modules.php
@@ -0,0 +1,21 @@
+
+ * (c) Daniel Kiesel
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Extension folder
+ */
+$GLOBALS['TL_LANG']['MOD']['discourse'] = array('Discourse Connector');
+
+/**
+ * Front end modules
+ */
+$GLOBALS['TL_LANG']['FMD']['discourseSSOProvider'] = array('Discourse SSO Provider', 'This module enables Single Sign-On of a Discourse installation. Users will be redirected to the Discourse Host (see Contao Settings) after successful authentication. This module does not produce any output (similar to the "Logout" module).');
diff --git a/src/Resources/contao/languages/en/tl_settings.php b/src/Resources/contao/languages/en/tl_settings.php
new file mode 100644
index 0000000..3ae4403
--- /dev/null
+++ b/src/Resources/contao/languages/en/tl_settings.php
@@ -0,0 +1,22 @@
+
+ * (c) Daniel Kiesel
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Fields
+ */
+$GLOBALS['TL_LANG']['tl_settings']['discourseSSOHost'] = array('Host Address', 'Host address of the Discourse installation, for which Single Sign-On will be provided.');
+$GLOBALS['TL_LANG']['tl_settings']['discourseSSOSecret'] = array('SSO Secret', 'Single Sign-On "Shared Secret" of the Discourse installation, for which Single Sign-On will be provided.');
+
+/**
+ * Legends
+ */
+$GLOBALS['TL_LANG']['tl_settings']['discourse_legend'] = 'Discourse settings';
diff --git a/src/SSO/SSOProviderPayload.php b/src/SSO/SSOProviderPayload.php
new file mode 100644
index 0000000..5707bc6
--- /dev/null
+++ b/src/SSO/SSOProviderPayload.php
@@ -0,0 +1,136 @@
+
+ * (c) Daniel Kiesel
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+// based on github.com/cviebrock/discourse-php
+// @see https://raw.githubusercontent.com/cviebrock/discourse-php/master/src/SSOHelper.php
+// @license (TBD)
+
+namespace Craffft\ContaoDiscourseSSOBundle\SSO;
+
+class SSOProviderPayload
+{
+ /**
+ * Endpoint which receives SSO response; add host and query like this:
+ * http://discourse_site.tld{API_ENDPOINT}?sso={PAYLOAD}&sig={SIG}
+ * @var string
+ */
+ const API_ENDPOINT = '/session/sso_login';
+
+ /**
+ * Secret used for signing payload data
+ * @var string
+ */
+ private $strSignatureSecret = '';
+
+ /**
+ * Nonce retrieved from challenge payload
+ * @var string
+ */
+ protected $strPayloadNonce = '';
+
+ /**
+ * Set signature secret
+ * @param string $strSecret Shared secret used for the payload signature
+ */
+ public function setSignatureSecret(string $strSecret)
+ {
+ $this->strSignatureSecret = $strSecret;
+ }
+
+ /**
+ * Check signature (and thus integrity) of payload
+ * @return boolean
+ */
+ public function isPayloadValid($strPayload, $strSignature)
+ {
+ return ($this->getPayloadSignature($strPayload) === $strSignature);
+ }
+
+ /**
+ * Validate and parse payload as well as retrieve and store nonce
+ * @param string $strPayload Challenge payload (must be urldecode()d!)
+ * @param string $strSignature The payload's signature
+ * @return true
+ * @throws \Exception
+ */
+ public function parseChallengePayload($strPayload, $strSignature)
+ {
+ if (!$this->isPayloadValid($strPayload, $strSignature)) {
+ throw new \Exception('Payload could not be validated against signature (Payload: "' . $strPayload . '", Signature: "' . $strSignature . '")');
+ }
+ // parse payload
+ $arrPayloadData = array();
+ parse_str(base64_decode($strPayload), $arrPayloadData);
+ // retrieve nonce
+ if (!array_key_exists('nonce', $arrPayloadData)) {
+ throw new \Exception('Invalid payload: Nonce not found');
+ }
+ $this->strPayloadNonce = $arrPayloadData['nonce'];
+
+ return true;
+ }
+
+ /**
+ * Generate and return response payload with signature ready for http_build_query()
+ * @see self::generateResponsePayload
+ * @param string $strUserId (External) user ID
+ * @param string $strUserEmail E-mail address of user
+ * @param array $arrOptionalParameters More parameters to include in payload
+ * @todo Use func_get_args resp. http://php.net/manual/functions.arguments.html#functions.variable-arg-list
+ */
+ public function getResponseDataForUser($strUserId, $strUserEmail, $arrOptionalParameters = array())
+ {
+ $arrPayloadData = array(
+ // 'nonce' => $this->strPayloadNonce,
+ 'external_id' => $strUserId,
+ 'email' => $strUserEmail
+ );
+ $arrPayloadData = array_merge($arrPayloadData, $arrOptionalParameters);
+ $strPayload = $this->generateResponsePayload($arrPayloadData);
+
+ return array(
+ 'sso' => $strPayload,
+ 'sig' => $this->getPayloadSignature($strPayload)
+ );
+ }
+
+ /**
+ * Generate and return response payload using nonce from challenge payload
+ * @param array $arrPayloadParameters Parameters to include in payload
+ * @return string
+ * @todo Check input array for required / valid values?
+ * @todo Consider making this protected
+ */
+ public function generateResponsePayload($arrPayloadParameters)
+ {
+ // $arrPayloadParameters required values: nonce, email, external_id
+ // … optional values: 'username', (full) 'name', 'avatar_url',
+ // 'require_activation', 'custom.*' (custom fields), etc.
+ // augment payload data with nonce
+ $arrPayloadParameters['nonce'] = $this->strPayloadNonce;
+
+ // create & return payload string
+ return base64_encode(http_build_query($arrPayloadParameters));
+ }
+
+ /**
+ * Return signature of payload using secret
+ * @param string $strPayload
+ * @return string
+ * @todo Consider making this protected
+ */
+ public function getPayloadSignature($strPayload)
+ {
+ return hash_hmac('sha256', $strPayload, $this->strSignatureSecret);
+ }
+
+}