Renoirb has submitted this change and it was merged. Change subject: *Major refactor*, now supporting OAuth2 RS ......................................................................
*Major refactor*, now supporting OAuth2 RS More info at: http://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension INFO: This version runs on https://docs.webplatform.org/test/ since three months. * Make MediaWiki to use a deployment of Mozilla Firefox Accounts as Identity Provider (IdP) * Session recovery using hidden iframe * Require external component: Guzzle that’ll handle async requests * Queries an OAuth2 Resource Server with minimal profile data * Reads from profile data e.g. {username: 'Jdoe', fullName: 'John Doe'} and creates/use local account * Overloads Special:UserLogin, Special:UserLogout * Handling change password, removing from preferences redundant info * Use memcache to keep state * Using MW i18n methods, moving messages to it, making up english, then translating to french * Moved validation responsibility to FirefoxAccountsManager * Wrong re-throwing new exception syntax fix * Reminder: Hook MUST!!1 return true or false * Implementing session Recovery * New behavior between iframe and session recovery Change-Id: If5d3caec9b05b8362190f8ef30658efd135cdb4c --- M .gitignore A README.md M WebPlatformAuth.php A composer.json A composer.lock M i18n/en.json M i18n/fr.json A includes/FirefoxAccountsManager.php M includes/WebPlatformAuthHooks.php A includes/WebPlatformAuthPlugin.php A includes/WebPlatformAuthUserFactory.php D includes/api/ApiWebPlatformAuth.php A includes/specials/AccountsHandlerSpecialPage.php D includes/specials/WPA_RenewSession.php A includes/specials/WebPlatformAuthLogin.php A includes/specials/WebPlatformAuthLogout.php A includes/specials/WebPlatformAuthPassword.php A vendor/.gitkeep 18 files changed, 1,305 insertions(+), 368 deletions(-) Approvals: Renoirb: Verified; Looks good to me, approved diff --git a/.gitignore b/.gitignore index 98b092a..26a012b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *~ *.kate-swp .*.swp +vendor/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb88de8 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# MediaWiki SSO using Firefox Accounts + +See project details on WebPlatform.org wiki [WPD:Projects/SSO/MediaWikiExtension](http://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension). + + +## Project repositories + +Code is synced in the following repositories: + +* https://github.com/webplatform/mediawiki-fxa-sso +* https://gerrit.wikimedia.org/r/#/admin/projects/mediawiki/extensions/WebPlatformAuth + + +## Reference documents + +The following were found to be userful in the realization of the +current fork. + +* https://github.com/wikimedia/mediawiki-extensions-Persona +* http://www.mediawiki.org/wiki/AuthPlugin +* http://www.mediawiki.org/wiki/Manual:Special_pages +* https://github.com/yorn/mwSimpleSamlAuth +* http://www.mediawiki.org/wiki/Extension_talk:CASAuthentication \ No newline at end of file diff --git a/WebPlatformAuth.php b/WebPlatformAuth.php index 8cb538b..3c8033b 100644 --- a/WebPlatformAuth.php +++ b/WebPlatformAuth.php @@ -1,53 +1,59 @@ <?php -# Alert the user that this is not a valid entry point to MediaWiki if they try to access the special pages file directly. -if (!defined('MEDIAWIKI')) { - echo <<<EOT -To install my extension, put the following line in LocalSettings.php: -require_once( "$IP/extensions/WebPlatformAuth/WebPlatformAuth.php" ); -EOT; - exit( 1 ); + +/** + * MediaWiki SSO using Firefox Accounts + * + * Project details are available on the WebPlatform wiki + * https://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension + * + * @ingroup Extensions + * + * @version 2.0-dev + */ + +if ( !defined( 'MEDIAWIKI' ) ) { + echo( "Not an entry point." ); + die( -1 ); } -$wgExtensionCredits['other'][] = array( - 'path' => __FILE__, - 'name' => 'WebPlatformAuth', - 'author' => '[http://www.hallowelt.biz Hallo Welt! Medienwerkstatt GmbH]; Robert Vogel', - 'url' => 'http://www.hallowelt.biz', - 'descriptionmsg' => 'webplatformauth-desc', - 'version' => '1.1.0', -); +//if ( version_compare( $GLOBALS['wgVersion'], '1.22', '<' ) ) { +// die( '<b>Error:</b> This extension requires MediaWiki 1.22 or above' ); +//} $dir = dirname(__FILE__) . '/'; -$wgAutoloadClasses['WebPlatformAuthHooks'] = $dir . 'includes/WebPlatformAuthHooks.php'; -$wgAutoloadClasses['ApiWebPlatformAuth'] = $dir . 'includes/api/ApiWebPlatformAuth.php'; -$wgAutoloadClasses['WPARenewSession'] = $dir . 'includes/specials/WPA_RenewSession.php'; -$wgMessagesDirs['WebPlatformAuth'] = __DIR__ . '/i18n'; -$wgExtensionMessagesFiles['WebPlatformAuth'] = $dir . 'WebPlatformAuth.i18n.php'; +if ( is_readable( __DIR__ . '/vendor/autoload.php' ) ) { + $loader = require( __DIR__ . '/vendor/autoload.php' ); + $loader->add( 'Guzzle\\', $dir . '/vendor/guzzlehttp/guzzle/src/Guzzle/' ); +} else { + die('You MUST install Composer dependencies'); +} -$wgSpecialPages['RenewSession'] = 'WPARenewSession'; -//$wgWhitelistRead[] = 'Special:RenewSession'; - -$wgAPIModules['webplatformauth'] = 'ApiWebPlatformAuth'; - -$wgAjaxExportList[] = 'WebPlatformAuthHooks::ajaxGetUserInfoById'; -$wgAjaxExportList[] = 'WebPlatformAuthHooks::ajaxGetUserInfoByName'; - -$wgHooks['UserLogoutComplete'][] = 'WebPlatformAuthHooks::onUserLogoutComplete'; -$wgHooks['UserLoginComplete'][] = 'WebPlatformAuthHooks::onUserLoginComplete'; -$wgHooks['UserSetCookies'][] = 'WebPlatformAuthHooks::onUserSetCookies'; -//$wgHooks['UserLoadFromSession'][] = 'WebPlatformAuthHooks::onUserLoadFromSession'; -$wgHooks['UserLoadAfterLoadFromSession'][] = 'WebPlatformAuthHooks::onUserLoadAfterLoadFromSession'; -//$wgHooks['UserLoadFromDatabase'][] = 'WebPlatformAuthHooks::onUserLoadFromDatabase'; - -/** - * @deprecated Replaced by $wgWebPlatformAuthSecret, because in WPD setup IP addresses ain't predictable enough - */ -$wgWebPlatformAuthAllowedClients = array( - 'localhost', - '127.0.0.1' +$wgExtensionCredits['other'][] = array( + 'name' => 'WebPlatformAuth', + 'path' => __FILE__, + 'version' => '2.0-dev', + 'author' => array('[https://renoirboulanger.com Renoir Boulanger]'), + 'url' => 'http://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension', + 'description' => 'Single Sign On MediaWiki extension', ); -$wgWebPlatformAuthSecret = 'NqzdqcdCRWd1JZ1DXSI2Eq5BbjYra40nEguT8654C7eNrMldXuMDs4laHQIppAoc'; +$wgAutoloadClasses['WebPlatformAuthHooks'] = $dir . 'includes/WebPlatformAuthHooks.php'; -$wgCookieDomain = '.webplatform.org'; // --> LocalSettings.php +$wgAutoloadClasses['AccountsHandlerSpecialPage'] = $dir . 'includes/specials/AccountsHandlerSpecialPage.php'; +$wgAutoloadClasses['WebPlatformAuthLogin'] = $dir . 'includes/specials/WebPlatformAuthLogin.php'; +$wgAutoloadClasses['WebPlatformAuthLogout'] = $dir . 'includes/specials/WebPlatformAuthLogout.php'; +$wgAutoloadClasses['WebPlatformAuthPassword'] = $dir . 'includes/specials/WebPlatformAuthPassword.php'; + +$wgMessagesDirs['WebPlatformAuth'] = __DIR__ . '/i18n'; +$wgExtensionMessagesFiles['WebPlatformAuth'] = $dir . 'WebPlatformAuth.i18n.php'; + +// Change AccountsHandler for better name later #TODO +$wgSpecialPages['AccountsHandler'] = 'AccountsHandlerSpecialPage'; +$wgSpecialPages['Userlogin'] = 'WebPlatformAuthLogin'; +$wgSpecialPages['Userlogout'] = 'WebPlatformAuthLogout'; +$wgSpecialPages['ChangePassword'] = 'WebPlatformAuthPassword'; + +$wgHooks['UserLoadFromSession'][] = 'WebPlatformAuthHooks::onUserLoadFromSession'; +$wgHooks['GetPreferences'][] = 'WebPlatformAuthHooks::hookLimitPreferences'; +$wgHooks['SpecialPage_initList'][] = 'WebPlatformAuthHooks::hookInitSpecialPages'; \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b71dcf8 --- /dev/null +++ b/composer.json @@ -0,0 +1,51 @@ +{ + "name": "webplatform/mediawiki-fxa-sso", + "type": "mediawiki-extension", + "license": "MIT", + "description": "WebPlatform Docs SSO Extension communicating with WebPlatorm’s own Firefox Accounts server", + "keywords": [ + "MediaWiki", + "WebPlatform", + "authentication", + "Firefox Accounts", + "OAuth2", + "FxA" + ], + "homepage": "http://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension", + "authors": [ + { + "name": "Renoir Boulanger", + "email": "ren...@w3.org", + "role": "Maintainer", + "homepage": "https://renoirboulanger.com" + }, + { + "name": "Doug Schepers", + "email": "schep...@w3.org", + "role": "Project lead" + } + ], + "support": { + "issues": "https://github.com/webplatform/mediawiki-fxa-sso/issues", + "email": "team-webplatform-syst...@w3.org", + "irc": "irc://irc.freenode.net/webplatform", + "source": "https://github.com/webplatform/mediawiki-fxa-sso" + }, + "require": { + "guzzlehttp/guzzle": "~3.8" + }, + "autoload": { + "files" : [ + "WebPlatformAuth.php" + ], + "classmap":[ + "includes/" + ] + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/webplatform/mediawiki-fxa-sso" + } + ] +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..4aba2e3 --- /dev/null +++ b/composer.lock @@ -0,0 +1,176 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" + ], + "hash": "c635d4c923295de15a39b2d5cc345712", + "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "v3.8.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.3", + "symfony/event-dispatcher": ">=2.1" + }, + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", + "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", + "guzzle/parser": "self.version", + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" + }, + "require-dev": { + "doctrine/cache": "*", + "monolog/monolog": "1.*", + "phpunit/phpunit": "3.7.*", + "psr/log": "1.0.*", + "symfony/class-loader": "*", + "zendframework/zend-cache": "<2.3", + "zendframework/zend-log": "<2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.8-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowl...@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" + } + ], + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2014-01-28 22:29:15" + }, + { + "name": "symfony/event-dispatcher", + "version": "v2.5.0", + "target-dir": "Symfony/Component/EventDispatcher", + "source": { + "type": "git", + "url": "https://github.com/symfony/EventDispatcher.git", + "reference": "cb62ec8dd05893fc8e4f0e6e21e326e1fc731fe8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/cb62ec8dd05893fc8e4f0e6e21e326e1fc731fe8", + "reference": "cb62ec8dd05893fc8e4f0e6e21e326e1fc731fe8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.0", + "symfony/dependency-injection": "~2.0", + "symfony/stopwatch": "~2.2" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fab...@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "http://symfony.com", + "time": "2014-04-29 10:13:57" + } + ], + "packages-dev": [ + + ], + "aliases": [ + + ], + "minimum-stability": "stable", + "stability-flags": [ + + ], + "platform": [ + + ], + "platform-dev": [ + + ] +} diff --git a/i18n/en.json b/i18n/en.json index 4f716ca..990c0a5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,8 +1,16 @@ { "@metadata": { "authors": [ - "Robert Vogel" + "Robert Vogel", + "Renoir Boulanger" ] }, - "webplatformauth-desc": "Enables MediaWiki to serve as webplatform.org's authentication provider" -} + "webplatformauth-desc": "Enables a MediaWiki instance to read information from an OAuth2 protected profile server and handles user accounts. The profile server becomes the authority creating SSO for that MediaWiki installation.", + "webplatformauth-missing-required-config": "Required configuration for the extension to work is missing", + "webplatformauth-cannot-repeat-oauth-handshake": "Cannot cannot repeat OAuth handshake, it has to be started over", + "webplatformauth-exception-oauth-handshake-details-below": "OAuth2 handshake error, see Exception error message below", + "webplatformauth-error-flow-state-data-empty": "Error while attempting to resume state prior to the login process", + "webplatformauth-exception-cannot-read-profile": "Error while attempting to read profile server data.", + "webplatformauth-error-user-nodata": "Error while attempting to read profile data, there was nothing", + "webplatformauth-exception-invalid-user-create-entry": "Invalid user data!" +} \ No newline at end of file diff --git a/i18n/fr.json b/i18n/fr.json index 7e6750e..5e585b5 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -1,8 +1,16 @@ { "@metadata": { "authors": [ - "Sherbrooke" + "Sherbrooke", + "Renoir Boulanger" ] }, - "webplatformauth-desc": "Autoriser MediaWiki à devenir un fournisseur d'authentification au nom de webplatform.org" -} + "webplatformauth-desc": "Permet à MediaWiki de créer et connecter des comptes locaux basé sur les données d’un service de profils protégé par OAuth2 tels Mozilla Firefox Accounts et celui de WebPlatform.org", + "webplatformauth-missing-required-config": "Une ou plusieurs des éléments de configuration requises sont manquantes", + "webplatformauth-cannot-repeat-oauth-handshake": "Impossible de répéter le processus d’authorisation OAuth2. Il doit être répété à partir du début", + "webplatformauth-exception-oauth-handshake-details-below": "Erreur lors du processus d’authorisation OAuth2, voir message de l’erreur", + "webplatformauth-error-flow-state-data-empty": "Erreur lors de la tentative de restaurer l’état de votre session avant la récente tentative de connexion", + "webplatformauth-exception-cannot-read-profile": "Erreur survenue lors de la tentative de lire les données du serveur de profil, est-il fonctionnel?", + "webplatformauth-error-user-nodata": "Erreur survenue lorsque lors de la tentative de lire les données du serveur de profile, il n’a rien retourné", + "webplatformauth-exception-invalid-user-create-entry": "Données d’utilisateur reçues sont dans une format incompréhensible" +} \ No newline at end of file diff --git a/includes/FirefoxAccountsManager.php b/includes/FirefoxAccountsManager.php new file mode 100644 index 0000000..b96eb8f --- /dev/null +++ b/includes/FirefoxAccountsManager.php @@ -0,0 +1,266 @@ +<?php + +/** + * MediaWiki SSO using Firefox Accounts + * + * Project details are available on the WebPlatform wiki + * https://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension + **/ + +// Guzzle classes +use Guzzle\Http\Client; + +// Guzzle Exceptions +use Guzzle\Http\Exception\ClientErrorResponseException; + +class FirefoxAccountsManager +{ + + const NO_RESULT_YET = 0; + + const ALREADY_CONNECTED = 2; + + const SUCCESSFUL = 4; + + const USER_CREATED = 8; + + const TTL = 3600; + + const MAX_CLIENT_TIMEOUT_SECONDS = 3; + + private $config = null; + + private $profile_data; + + public function __construct($config) + { + if ( !$this->hasAllRequiredConfiguration( $config ) ) { + throw new Exception( 'Required configuration is missing' ); + } + + $this->config = $config; + + // THIS SHOULD MOVE SOON + $this->memcache = $GLOBALS['wgMemc']; + + return $this; + } + + /** THIS CLASS SHOULD BE HANDLING ONLY WITH OAUTH, NOT MEMCACHE + BELOW THIS LINE WILL HAVE TO MOVE OUTSIDE OF THIS CLASS **/ + + /** + * Serialize and save data to memcache + * + * Note that it also sets a time to live for the + * cached version set to self::TTL + * + * @param string $cacheKey Cache key + * @param mixed $data Data to send to memcached, will use serialize(); + * + * @return null + */ + private function memcacheSave($data) + { + $url_friendly_key = MWCryptRand::generateHex( 16 ); + + $key = wfMemcKey( $url_friendly_key , 'wpdsso' ); + + $this->memcache->set( $key , json_encode( $data ) , self::TTL ); + + return $url_friendly_key; + } + + /** + * Delete entry from memcache from given cache key + * + * @param string $cacheKey Cache key + * + * @return null + */ + private function memcacheRemove($cacheKey) + { + $regen_key = wfMemcKey( $cacheKey , 'wpdsso' ); + + $this->memcache->delete( $regen_key ); + } + + /** + * Read entry from memcache from given cache key + * + * @param string $cacheKey Cache key + */ + private function memcacheRead($cacheKey) + { + $regen_key = wfMemcKey( $cacheKey , 'wpdsso' ); + + return $this->memcache->get( $regen_key ); + } + + /** + * Give a key, get associated state data + * + * @param string $state_key + * + * @return array Data in the same state as when sent to stateStash + */ + public function stateRetrieve($state_key) + { + $data = $this->memcacheRead( $state_key ); + + return json_decode( $data , 1); + } + + public function stateDeleteKey($state_key) + { + $this->memcacheRemove( $state_key ); + } + + public function stateStash($data_array) + { + $key = $this->memcacheSave( $data_array ); + + return $key; + } + + /* END - THIS CLASS SHOULD BE... */ + + /** + * Ask FxA to get a Bearer Token + * + * Typical return array is: + * <code> + * array( + * "access_token" => '...' + * "token_type" => 'bearer' + * "scope" => 'profile' + * ) + * </code> + * + * @param string $code received from POST on FxA OAuth authorize endpiont + * + * @return array Recieved JSON and converted to PHP array + */ + public function getBearerToken($code) + { + $m = $this->config['methods']; + $e = $this->config['endpoints']; + + $uri = $e['fxa_oauth'].$m['token']; + $packageData['client_id'] = $this->config['client']['id']; + $packageData['client_secret'] = $this->config['client']['secret']; + $packageData['code'] = $code; + $postBody = json_encode( $packageData ); + + try { + $client = new Client(); + $client->setDefaultOption('timeout', self::MAX_CLIENT_TIMEOUT_SECONDS); + $subreq = $client->createRequest( 'POST' , $uri , null , $postBody ); + $subreq->setHeader( 'Content-type' , 'application/json' ); + + $r = $client->send( $subreq ); + } catch ( ClientErrorResponseException $e ) { + throw $e; + } + + return $r->json(); + } + + /** + * Build OAuth provider URI + * + * @return string URI to the OAuth signin action on the FxA server + */ + public function initHandshake($return_to=null, $signup=false) + { + $m = $this->config['methods']; + $e = $this->config['endpoints']; + + if( $return_to !== null ) { + $stateData['return_to'] = $return_to; + } + + $stateData['scopes'] = array( 'session' ); + + $state_key = $this->stateStash( $stateData ); // Returns uuid + + $query_params['client_id'] = $this->config['client']['id']; + $query_params['state'] = $state_key; + // Space separated list of scopes keys + $query_params['scope'] = implode( '+' , $stateData['scopes'] ); + + if ( $signup === true ) { + $query_params['action'] = 'signup'; + } + + return $e['fxa_oauth'] + . $m['authorize'] + . '?' + . http_build_query( $query_params ); + } + + /** + * Retrieve profile data from FxA profile server + * + * @param array $token Token array recieved from OAuth handshake + * + * @return array profile data as an array + */ + public function getProfile($token) + { + $m = $this->config['methods']; + $e = $this->config['endpoints']; + + /** + * $token has the following keys: + * array( + * "access_token" => "..." + * "token_type" => "bearer" + * "scope" => "profile" + * ) + * + * At the moment, note that token_type is ONLY of type bearer. + * + * Make $token a strong typed Token class, and enforce at method setter #IMPROVEMENT + */ + $token_value = $token['access_token']; + $uri = $e['fxa_profile'] . 'session/read'; + + //$GLOBALS['poorman_logging'][] = 'Bearer token read : '.$token_value; // DEBUG + + try { + $client = new Client(); + $client->setDefaultOption('timeout', self::MAX_CLIENT_TIMEOUT_SECONDS); + $subreq = $client->createRequest( 'GET' , $uri ); + $subreq->setHeader( 'Authorization' , 'Bearer ' . $token_value ); + $r = $client->send( $subreq ); + } catch ( Exception $e ) { + throw new Exception('ProfileReaderException: Cannot get user profile', null, $e); + } + + return $r->json(); + } + + /** + * Internal check to see if we have all required configuration + * + * @param array $config Constructor injected configuration to validate + * + * @return boolean Whether it has what we need or not + */ + private function hasAllRequiredConfiguration($config) + { + if ( !is_array( $config ) ) { + return false; + } + + return isset( + $config['client']['id'], + $config['client']['secret'], + $config['endpoints']['fxa_oauth'], + $config['endpoints']['fxa_profile'], + $config['methods']['authorize'], + $config['methods']['token'] + ); + } +} diff --git a/includes/WebPlatformAuthHooks.php b/includes/WebPlatformAuthHooks.php index e2aca8b..dcec798 100644 --- a/includes/WebPlatformAuthHooks.php +++ b/includes/WebPlatformAuthHooks.php @@ -1,184 +1,407 @@ <?php -class WebPlatformAuthHooks { +/** + * MediaWiki SSO using Firefox Accounts + * + * Project details are available on the WebPlatform wiki + * https://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension + **/ - /** - * - * @param User $user - * @param string $inject_html - * @return boolean - */ - public static function onUserLoginComplete( $user, &$inject_html ) { - $_SESSION['wsUserEmail'] = $user->getEmail(); - //We've got to flatten the effective groups because MWs MemchacheD - //session handler does not serialize $_SESSION correctly - // -> inludes/MemcachedClient.php:993 - $_SESSION['wsUserEffectiveGroups'] = implode( ',', $user->getEffectiveGroups() ); - //$_SESSION['wsUserRealName'] = $user->getRealName(); - $_SESSION['wsUserPageURL'] = $user->getUserPage()->getFullURL(); +// FIXME, Loader.. :( +require_once( dirname( __FILE__ ) . '/WebPlatformAuthUserFactory.php' ); +require_once( dirname( __FILE__ ) . '/FirefoxAccountsManager.php' ); - self::writeDataToMemcache( $user ); +// Guzzle Exceptions +use Guzzle\Http\Exception\ClientErrorResponseException; - self::checkReturnTo(); - - return true; - } +class WebPlatformAuthHooks +{ + /** + * Disable redundant Special pages + * + * Some pages aren’t needed while using an external authentication + * source. + * + * Explictly disabling local pages: + * * Password change, + * * e-mail confirmation disabled when autoconfirm is disabled. + * + * They will be handled by our external provider anyway + * + * Blantly copied from SimpleSamlAuth::hookInitSpecialPages() + * @link https://github.com/yorn/mwSimpleSamlAuth + * + * @link http://www.mediawiki.org/wiki/Manual:Hooks/SpecialPage_initList + * + * @param $pages string[] List of special pages in MediaWiki + * + * @return boolean|string true on success, false on silent error, string on verbose error + */ + public static function hookInitSpecialPages( &$pages ) { + unset( $pages['PasswordReset'] ); + unset( $pages['ConfirmEmail'] ); + unset( $pages['ChangeEmail'] ); - /** - * - * @param User $user - * @param string $inject_html - * @param string $oldName - * @return boolean - */ - public static function onUserLogoutComplete($user, $inject_html, $oldName) { - //TODO: Maybe - session_destroy(); - self::checkReturnTo(); + // Those are overriden + //unset( $pages['ChangePassword'] ); + //unset( $pages['Userlogout'] ); + //unset( $pages['Userlogin'] ); - return true; - } - - /** - * - * @param User $user User object - * @param array $session session array, will be added to $_SESSION - * @param array $cookies cookies array mapping cookie name to its value - * @return boolean - */ - public static function onUserSetCookies( $user, &$session, &$cookies ) { - $session['wsUserEmail'] = $user->getEmail(); - $session['wsUserEffectiveGroups'] = implode(',',$user->getEffectiveGroups()); - $session['wsUserPageURL'] = $user->getUserPage()->getFullURL(); - - self::writeDataToMemcache( $user ); - return true; - } + return true; + } - /** - * - * @param User $user User object - * @return boolean - */ - public static function onUserLoadAfterLoadFromSession( $user ) { - $_SESSION['wsUserEmail'] = $user->getEmail(); - $_SESSION['wsUserEffectiveGroups'] = implode(',',$user->getEffectiveGroups()); - $_SESSION['wsUserPageURL'] = $user->getUserPage()->getFullURL(); + /** + * Disable redundant preferences + * + * Since an external system is taking care of those, lets + * remove them from the special pages. + * + * Blantly copied from SimpleSamlAuth::hookLimitPreferences() + * @link https://github.com/yorn/mwSimpleSamlAuth + * + * @link http://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences + * + * @param $user User User whose preferences are being modified. + * ignored by this method because it checks the SAML assertion instead. + * @param &$preferences Preferences description array, to be fed to an HTMLForm object. + * + * @return boolean|string true on success, false on silent error, string on verbose error + */ + public static function hookLimitPreferences( $user, &$preferences ) { + unset( $preferences['password'] ); + unset( $preferences['rememberpassword'] ); + unset( $preferences['emailaddress'] ); - self::writeDataToMemcache( $user ); - return true; - } - - /** - * - * @global WebRequest $wgRequest - * @global OutputPage $wgOut - */ - public static function checkReturnTo() { - global $wgRequest; - $returnTo = $wgRequest->getVal('returnto'); - if (!is_null($returnTo) && in_array( substr($returnTo, 0, 3), array( 'qa|', 'wp|' ) ) ) { - //We have to exit() here because otherwise we would be redirected to a MW page - header('Location: ' . substr($returnTo, 3)); - exit(); - } - } + // Should disable realname here and have + // FxA do the handling for us + //unset( $preferences['realname'] ); - /** - * - * @param string $userIds Comma seperated list of user ids - * @param string $secret A secret key to avoid unauthorized use of the ajax interface - * @return string JSON encoded list of requested user information - */ - public static function ajaxGetUserInfoById($userIds, $secret ) { - global $wgWebPlatformAuthSecret; - if( $secret != $wgWebPlatformAuthSecret ) { - return FormatJson::encode( new stdClass() ); - } + return true; + } - $userIds = explode(',', $userIds); - $users = UserArray::newFromIDs($userIds); + /** + * Load session from user + * + * At this time here, we can be in two situations. Either we are an + * anonymous user (user object here has most likely no name set we assume) + * but we also might happen to just be back from our trip to the OAuth + * Resource server. + * + * Documentation says we should read cookies and just pop in that user object + * the name coming from the cookies. I expect that just breaks any security + * steps we’ve taken so far. + * + * Since we came from the OAuth Resource server and the user had a successful + * authentication exchange, the Request URI should have TWO properties + * + * - code + * - state + * + * The Code will be used right after to get a bearer token, so, its safe + * to assume that we can start that validation here instead than later in the + * execution flow. + * + * If that step was successful, we trust that we already saved + * a state object in the Memcache server. Lets use that as a way to check + * **before any html has been given to the browser** to validate that user. + * + * We already might got cookies: + * + * - (.*)Token, + * - (.*)UserID + * - (.*)UserName + * + * Since we already can know expectable data from the resource server, + * use this hook as an event handler to actually do the validation. + * from our OAuth Resource server, and nothing else should be done + * + * @param [type] $user [description] + * @param [type] $result [description] + * @return [type] [description] + */ + public static function onUserLoadFromSession( $user, &$result ) + { + $GLOBALS['poorman_logging'] = array(); - $response = array(); - foreach ($users as $user) { - $response[$user->getId()] = array( - 'user_name' => $user->getName(), - 'user_real_name' => $user->getRealName(), - 'user_email' => $user->getEmail(), - 'user_page_url' => $user->getUserPage()->getFullURL() - ); - } + $diagnosis['session'] = RequestContext::getMain()->getRequest()->getCookie('_session'); + $diagnosis['username'] = RequestContext::getMain()->getRequest()->getCookie('UserName'); + $diagnosis['user_id'] = RequestContext::getMain()->getRequest()->getCookie('UserID'); + if(isset($_COOKIE['wpdSsoUsername'])) $diagnosis['wpdSsoUsername'] = $_COOKIE['wpdSsoUsername']; + if(isset($_POST['recoveryPayload'])) $diagnosis['recoveryPayload'] = $_POST['recoveryPayload']; - //In MW there is no user "0", but in Q2A - if( in_array( 0, $userIds ) ) { - $user = User::newFromId(1); //WikiSysop; - $response[0] = array( - 'user_name' => $user->getName(), - 'user_real_name' => $user->getRealName(), - 'user_email' => $user->getEmail(), - 'user_page_url' => $user->getUserPage()->getFullURL() - ); - } + //header("X-WebPlatform-Debug: ".substr(str_replace('"','', json_encode($diagnosis,true)),1,-1)); + //header("X-WebPlatform-Debug-Cookie: ".substr(str_replace('"','', json_encode($_COOKIE,true)),1,-1)); - return FormatJson::encode($response); - } - - /** - * - * @param string $userNames Comma seperated list of user names - * @param string $secret A secret key to avoid unauthorized use of the ajax interface - * @return string JSON encoded list of requested user information - */ - public static function ajaxGetUserInfoByName($userNames, $secret) { - global $wgWebPlatformAuthSecret; - if( $secret != $wgWebPlatformAuthSecret ) { - return FormatJson::encode( new stdClass() ); - } + //if(isset($_POST['recoveryPayload'])){ + // header("X-WebPlatform-Debug-Edgecase2: recoveryPayload is present"); + //} - $userNames = explode(',', $userNames); - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( - 'user', - 'user_id', - array( 'user_name' => $userNames ) - ); + // Use Native PHP way to check REQUEST params + $state_key = (isset($_GET['state']))?$_GET['state']:null; + $code = (isset($_GET['code']))?$_GET['code']:null; + $bearer_token = null; + $profile = null; + $site_root = str_replace('$1','', $GLOBALS['wgArticlePath']); + //$registeredCookieWithUsername = RequestContext::getMain()->getRequest()->getCookie( 'UserName' ); + //$GLOBALS['poorman_logging'][] = 'Registered cookie username: '.print_r($registeredCookieWithUsername, 1); - $response = array(); - foreach ( $res as $row ) { - $user = User::newFromId($row->user_id); - $response[$user->getId()] = array( - 'user_name' => $user->getName(), - 'user_real_name' => $user->getRealName(), - 'user_email' => $user->getEmail(), - 'user_page_url' => $user->getUserPage()->getFullURL() - ); - } + $sessionToken = ( isset( $_POST['recoveryPayload'] ) ) ? $_POST['recoveryPayload'] : null; + if ( is_string( $sessionToken ) ) { + header('Cache-Control: no-store, no-cache, must-revalidate'); - return FormatJson::encode($response); - } - - protected static function writeDataToMemcache( $user ) { - global $wgMemc; + // TODO: Do some more validation with this, see + // notes at http://docs.webplatform.org/wiki/WPD:Projects/SSO/Improvements_roadmap#Recovering_session_data + try { + $uri = 'https://profile.accounts.webplatform.org/v1/session/recover'; + $client = new Guzzle\Http\Client(); + $client->setDefaultOption( 'timeout', 22 ); + $subreq = $client->get($uri); + $subreq->setHeader( 'Content-type', 'application/json' ); + $subreq->setHeader( 'Authorization', 'Session ' . $sessionToken ); - if ( !is_object($user) ) return true; + $r = $client->send( $subreq ); + } catch ( Guzzle\Http\Exception\ClientErrorResponseException $e ) { + $clientErrorStatusCode = $e->getResponse()->getStatusCode(); + $clientErrorReason = $e->getResponse()->getReasonPhrase(); - $sesskey = false; - if ( isset( $_COOKIE[ 'wpwiki_session' ] ) ) { - $sesskey = $_COOKIE[ 'wpwiki_session' ]; - } - if ( !$sesskey ) return true; + // We are considering that wgUser id 0 means anonymous + if($clientErrorStatusCode === 401 && $GLOBALS['wgUser']->getId() !== 0) { + $GLOBALS['wgUser']->logout(); + header("HTTP/1.1 401 Unauthorized"); + header("X-WebPlatform-Outcome-1: Session closed, closing local too"); - $memcAlternateSessionKey = 'wpwiki:altsession:'.$sesskey; - #error_log( "Docs Alternate Session Key: ". $memcAlternateSessionKey ); - #error_log( "UserId". $user->getId() ); + // 401 AND uid 0 means we have nothing to do + } elseif($clientErrorStatusCode === 401 && $GLOBALS['wgUser']->getId() === 0) { + header("HTTP/1.1 204 No Content"); + header("X-WebPlatform-Outcome-2: Session closed both local and accounts"); + } - $data = array(); - $data['wsUserID'] = $user->getId(); - $data['wsUserName'] = $user->getName(); - $data['wsUserEmail'] = $user->getEmail(); - $data['wsUserEffectiveGroups'] = implode(',',$user->getEffectiveGroups()); - $data['wsUserPageURL'] = $user->getUserPage()->getFullURL(); + return true; - $wgMemc->add( $memcAlternateSessionKey, serialize( $data ) ); - } -} \ No newline at end of file + } catch ( Guzzle\Http\Exception\CurlException $e ) { + header("X-WebPlatform-Outcome-3: CurlException"); + header("HTTP/1.1 400 Bad Request"); + echo($e->getMessage()); + + return true; + } + + try { + $data = $r->json(); + } catch(Exception $e) { + header("HTTP/1.1 400 Bad Request"); + header("X-WebPlatform-Outcome-4: Profile refused communication"); + echo "Profile server refused communication"; + + return true; + } + + # 20140807 + $wgUserName = (is_object($GLOBALS['wgUser']))?$GLOBALS['wgUser']->getName():null; + if(isset($data['username']) && strtolower($wgUserName) === strtolower($data['username'])) { + header("X-WebPlatform-Outcome-5: " . strtolower($wgUserName) . " is " . strtolower($data['username'])); + header("HTTP/1.1 204 No Content"); + + return true; // All is good + } + + $tempUser = WebPlatformAuthUserFactory::prepareUser( $data ); + wfSetupSession(); + if( $tempUser->getId() === 0 ){ + // No user exists whatsoever, create and make current user + $tempUser->ConfirmEmail(); + $tempUser->setEmailAuthenticationTimestamp( time() ); + $tempUser->setPassword( User::randomPassword() ); + $tempUser->setToken(); + $tempUser->setOption( "rememberpassword" , 0 ); + $tempUser->addToDatabase(); + $GLOBALS['poorman_logging'][] = sprintf( 'User %s created' , $tempUser->getName() ) ; + } else { + // User exist in database, load it + $tempUser->loadFromDatabase(); + $GLOBALS['poorman_logging'][] = sprintf( 'Session for %s started' , $tempUser->getName() ) ; + } + $GLOBALS['poorman_logging'][] = $tempUser->getId(); + $GLOBALS['wgUser'] = $tempUser; + $tempUser->saveSettings(); + $tempUser->setCookies(); + + // Ideally, the first false below should be true! But we need SSL at the top level domain + setcookie('wpdSsoUsername', $data['username'], time()+60*60*7, '/', '.webplatform.org', false, true); + + if (isset($_GET['username'])) { + header("X-WebPlatform-Username: ".$_GET['username']); + } + #header("X-WebPlatform-Recovery: " . urlencode(json_encode($data)) ); + + header("HTTP/1.0 201 Created"); + + return true; + } + if ( is_string( $state_key ) && is_string( $code ) ) { // START IF HAS STATE AND CODE + // WE HAVE STATE AND CODE, NOW ARE THEY JUNK? + + //$GLOBALS['poorman_logging'][] = 'About to retrieve data: '.(($user->isLoggedIn())?'logged in':'not logged in'); // DEBUG + + // Since we DO have what we need to get + // to our validation server, please do not cache. + // ... and since WE DIDN’t send any HTML, yet (good boy) + // we can actually do that. + header('Cache-Control: no-store, no-cache, must-revalidate'); + + try { + $apiHandler = new FirefoxAccountsManager( $GLOBALS['wgWebPlatformAuth'] ); + // $code can be used ONLY ONCE! + //$GLOBALS['poorman_logging'][] = 'Code: '.print_r($code,1); // DEBUG + $bearer_token = $apiHandler->getBearerToken( $code ); + + } catch ( ClientErrorResponseException $e ) { + + $msg = 'Could not get authorization token'; + + $GLOBALS['poorman_logging'][] = $msg; + + $msg .= ', returned after FirefoxAccountsManager::getBearerToken(), it said:' . $e->getMessage(); + $obj = json_decode($e->getResponse()->getBody(true), true); + $msg .= (isset($obj['reason'])) ? ', reason: ' . $obj['reason'] : null; + $msg .= (isset($obj['message'])) ? ', message: '.$obj['message'] : null; + + error_log($msg); + + //header('Location: '.$site_root); + + return true; + } catch ( Exception $e ) { + // Other error: e.g. config, or other Guzzle call not expected. + $msg = 'Unknown error, Could not get authorization token'; + $GLOBALS['poorman_logging'][] = $msg; + + $msg .= ', returned a "' . get_class($e); + $msg .= '" after FirefoxAccountsManager::getBearerToken(), it said: '.$e->getMessage(); + + error_log($msg); + + //header('Location: '.$site_root); + + return true; + } + + //$GLOBALS['poorman_logging'][] = 'Bearer token: '.print_r($bearer_token,1); // DEBUG + + // FirefoxAccountsManager::getBearerToken() + // returns an array. + if ( is_array( $bearer_token ) ) { + try { + $profile = $apiHandler->getProfile( $bearer_token ); + + //$GLOBALS['poorman_logging'][] = 'Profile: '.print_r($profile,1); // DEBUG + + $tempUser = WebPlatformAuthUserFactory::prepareUser( $profile ); + } catch ( ClientErrorResponseException $e ) { + + $msg = 'Could not retrieve profile data'; + $GLOBALS['poorman_logging'][] = $msg; + + $msg .= ', returned a "' . get_class($e); + $msg .= '" after calling getProfile(), it said: '.$e->getMessage(); + + $obj = json_decode($e->getResponse()->getBody(true), true); + $msg .= (isset($obj['reason'])) ? ', with reason: ' . $obj['reason'] : null; + $msg .= (isset($obj['message'])) ? ', message: '.$obj['message'] : null; + + error_log($msg); + + return true; + } catch ( Exception $e ) { + $msg = 'Unknown error, Could not get profile data or create new user'; + + $GLOBALS['poorman_logging'][] = $msg; + + $msg .= ', returned a "' . get_class($e); + $msg .= '" after FirefoxAccountsManager::getProfile(), it said: '.$e->getMessage(); + + error_log($msg); + + return true; + } + + // Note that, HERE, whether we use $GLOBALS['wgUser'] + // or $user (passed in this function call from the hook) + // or EVEN the one passed to WebPlatformAuthUserFactory::prepareUser() + // it should be the same. It is assumed that in prepareUser() it the call + // to MW User::loadDefaults($username) makes that binding. + // #DOUBLECHECKLATER + + // Let’s be EXPLICIT + // + // Note that MW User::isLoggedIn() is not **only** checking + // whether the user is logged in per se. But rather do both; + // checking if the user exists in the database. Doesn’t mean + // the session is bound, yet. + wfSetupSession(); + if( $tempUser->getId() === 0 ){ + // No user exists whatsoever, create and make current user + $tempUser->ConfirmEmail(); + $tempUser->setEmailAuthenticationTimestamp( time() ); + $tempUser->setPassword( User::randomPassword() ); + $tempUser->setToken(); + $tempUser->setOption( "rememberpassword" , 0 ); + $tempUser->addToDatabase(); + $GLOBALS['poorman_logging'][] = sprintf( 'User %s created' , $tempUser->getName() ) ; + } else { + // User exist in database, load it + $tempUser->loadFromDatabase(); + $GLOBALS['poorman_logging'][] = sprintf( 'Session for %s started' , $tempUser->getName() ) ; + } + $GLOBALS['poorman_logging'][] = $tempUser->getId(); + $GLOBALS['wgUser'] = $tempUser; + $tempUser->saveSettings(); + $tempUser->setCookies(); + + //$GLOBALS['poorman_logging'][] = ($GLOBALS['wgUser']->isLoggedIn())?'logged in':'not logged in'; // DEBUG + //$GLOBALS['poorman_logging'][] = $tempUser->getId(); // DEBUG + $state_data = $apiHandler->stateRetrieve( $state_key ); + + if ( is_array($state_data) && isset( $state_data['return_to'] )) { + $apiHandler->stateDeleteKey( $state_key ); + $GLOBALS['poorman_logging'][] = 'State data: '.print_r($state_data, 1); + + header('Location: ' . $state_data['return_to'] ); + + return true; // Even though it might just be sent elsewhere, making sure. + } + } else { + $GLOBALS['poorman_logging'][] = 'No bearer tokens'; + + header('Location: '.$site_root); + } + } + + //$GLOBALS['poorman_logging'][] = ($GLOBALS['wgUser']->isLoggedIn())?'logged in':'not logged in'; + + /** + * I can put true or false because we wont be using local authentication + * whatsoever. Hopefully that’s the way to do. + * + * Quoting the doc + * + * "When the authentication should continue undisturbed + * after the hook was executed, do not touch $result. When + * the normal authentication should not happen + * (e.g., because $user is completely initialized), + * set $result to any boolean value." + * + * -- 2014-05-22 http://www.mediawiki.org/wiki/Manual:Hooks/UserLoadFromSession + * + * But, if I set $result to either true or false, it doesn’t make the UI to + * act as if you are logged in, AT ALL. Even though I created + * the user and set it to the global object. I’d like to investigate on why we cannot + * set either true or false here because it is unclear what it means undisturbed... we are + * creating local users, based on remote data, but authentication implies password, we arent using + * local ones, what gives? #DOUBLECHECKLATER + */ + //$result = false; // Doesn’t matter true or false, and its passed here by-reference. + + return true; // Hook MUST return true if it was as intended, was it? (hopefully so far) + } +} diff --git a/includes/WebPlatformAuthPlugin.php b/includes/WebPlatformAuthPlugin.php new file mode 100644 index 0000000..7fc84a3 --- /dev/null +++ b/includes/WebPlatformAuthPlugin.php @@ -0,0 +1,37 @@ +<?php + +/** + * MediaWiki SSO using Firefox Accounts + * + * Project details are available on the WebPlatform wiki + * https://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension + **/ + +require_once( dirname( __FILE__ ) . '/WebPlatformAuthHooks.php' ); +require_once( dirname( __FILE__ ) . '/FirefoxAccountsManager.php' ); + +// Guzzle Exceptions +use Guzzle\Http\Exception\ClientErrorResponseException; + +/** + * Extending MediaWiki Authentication + * + * Based on GodAuth MediaWiki Extension + * + * @link https://github.com/iamcal/MediaWiki-SSO/blob/master/GodAuth.php + */ +class WebPlatformAuthPlugin extends AuthPlugin +{ + protected $apiHandler; + + protected function _init() + { + try { + $this->apiHandler = new FirefoxAccountsManager( $GLOBALS['wgWebPlatformAuth'] ); + } catch( Exception $e ) { + error_log('Problem initiating MediaWiki WebPlatformAuth AuthPlugin handler:' . $e->getMessage() ); + + throw $e; + } + } +} \ No newline at end of file diff --git a/includes/WebPlatformAuthUserFactory.php b/includes/WebPlatformAuthUserFactory.php new file mode 100644 index 0000000..5ea8bfd --- /dev/null +++ b/includes/WebPlatformAuthUserFactory.php @@ -0,0 +1,77 @@ +<?php + +/** + * MediaWiki SSO using Firefox Accounts + * + * Project details are available on the WebPlatform wiki + * https://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension + **/ + +class WebPlatformAuthUserFactory +{ + /** + * Return a user object + * + * Doesn’t add to the database, to keep an instance + * you have to call addToDatabase(); to your new user. + * + * + * <code> + * // Expected entered array format + * $user_array = array( + * 'fullName'=>'John Doe', + * 'username'=>'jdoe', + * 'email'=>'j...@doe.name'); + * + * // Check if the user can be created (i.e. has no entry in DB). + * + * // Then... + * $user = self::prepareUser($GLOBALS['wgUser'], $user_array); + * + * // (poor man) persist —current author badly miss Doctrine2— + * $user->addToDatabase(); + * + * // Flag confirmation + * $user->ConfirmEmail(); + * + * // When making changes + * $user->saveSettings(); + * + * // Replace global scope object with our new one + * // .. start session ... + * $GLOBALS['wgUser'] = $user; // Yep, like that :/ + * $GLOBALS['wgUser']->setCookies(); // ^ + * </code> + * + * @param User &$user MediaWiki User instance passed as reference + * @param array $user_array Array of provided by our profile server data to use inside our local user + * + * @return void + */ + public static function prepareUser( $user_array ) + { + $desired_keys = array( 'fullName' , 'email' , 'username' ); + $input_keys = array_keys( $user_array ); + $diff = array_diff( $desired_keys , $input_keys ); + + if ( count( $diff ) >= 1 ) { + throw new Exception( sprintf( 'Recieved data has required keys that are missing: %s' , implode( ', ' , $diff ) ) ); + } + + $username = ucfirst( $user_array['username'] ); + + if ( !User::isUsableName( $username ) ) { + throw new Exception( sprintf( 'Username %s has invalid characters' , $username ) ); + } + + /* + * Based off of UserLoadFromSession Talk page documentation + * http://www.mediawiki.org/wiki/Manual_talk:Hooks/UserLoadFromSession + */ + $user = User::newFromName( $username ); + $user->setRealName( $user_array['fullName'] ); + $user->setEmail( $user_array['email'] ); + + return $user; + } +} \ No newline at end of file diff --git a/includes/api/ApiWebPlatformAuth.php b/includes/api/ApiWebPlatformAuth.php deleted file mode 100644 index dd7ceb7..0000000 --- a/includes/api/ApiWebPlatformAuth.php +++ /dev/null @@ -1,138 +0,0 @@ -<?php -/** - * - * - * Created on Sep 29, 2012 - * - * API module for MediaWiki's WebPlatformSearchAutocomplete extension - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * @ingroup WebPlatformAuth - */ -class ApiWebPlatformAuth extends ApiBase { - - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - - public function getCustomPrinter() { - return $this->getMain()->createPrinterByName( 'json' ); - } - - public function execute() { - global $wgSearchSuggestCacheExpiry, $wgWebPlatformAuthSecret; - $params = $this->extractRequestParams(); - - $command = $params['command']; - $users = $params['users']; - $secret = $params['secret']; - - $result = $this->getResult(); - - if( $secret != $wgWebPlatformAuthSecret ) { - $result->addValue( null, 'result', new stdClass() ); - return; - } - - $users = explode(',', $users); - $userlist = array(); - - if( $command == 'GetUsersById' ) { - $userlist = UserArray::newFromIDs($users); - } - else if( $command == 'GetUsersByName' ) { - $res = $this->getDB()->select( - 'user', - 'user_id', - array( 'user_name' => $users ) - ); - foreach ( $res as $row ) { - $userlist[] = User::newFromId($row->user_id); - } - } - - $response = array(); - foreach( $userlist as $user ) { - $response[$user->getId()] = array( - 'user_id' => $user->getId(), - 'user_name' => $user->getName(), - 'user_real_name' => $user->getRealName(), - 'user_email' => $user->getEmail(), - 'user_page_url' => $user->getUserPage()->getFullURL() - ); - } - - //In MW there is no user "0", but in Q2A - if( in_array( 0, $users ) ) { - $user = User::newFromId(1); //WikiSysop; - $response[0] = array( - 'user_id' => 0, - 'user_name' => $user->getName(), - 'user_real_name' => $user->getRealName(), - 'user_email' => $user->getEmail(), - 'user_page_url' => $user->getUserPage()->getFullURL() - ); - } - - // Open search results may be stored for a very long time - $this->getMain()->setCacheMaxAge( $wgSearchSuggestCacheExpiry ); - $this->getMain()->setCacheMode( 'public' ); - - // Set top level elements - $result->addValue( null, 'users', $response ); - } - - public function isReadMode() { //Needed to be always available, even if read-api is not allowed - return false; - } - - public function getAllowedParams() { - return array( - 'command' => null, - 'users' => null, - 'secret' => null, - ); - } - - public function getParamDescription() { - return false; - return array( - 'command' => 'GetUsersById|GetUsersByName', - 'users' => 'Comma seperated list of either user names or user ids. Depends on given command.', - ); - } - - public function getDescription() { - return false; - return 'User information provider for the webplatform.org applications'; - } - - public function getExamples() { - return false; - return array( - 'api.php?action=webplatformauth&command=GetUsersById&users=45,23,56' - ); - } - - public function getHelpUrls() { - return 'https://www.webplatform.org'; - } -} diff --git a/includes/specials/AccountsHandlerSpecialPage.php b/includes/specials/AccountsHandlerSpecialPage.php new file mode 100644 index 0000000..ad8ac9b --- /dev/null +++ b/includes/specials/AccountsHandlerSpecialPage.php @@ -0,0 +1,116 @@ +<?php + +/** + * MediaWiki SSO using Firefox Accounts + * + * Project details are available on the WebPlatform wiki + * https://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension + **/ + +// FIXME, Loader.. :( +require_once( dirname( dirname( __FILE__ ) ) . '/FirefoxAccountsManager.php' ); +require_once( dirname( dirname( __FILE__ ) ) . '/WebPlatformAuthUserFactory.php' ); + +// Guzzle Exceptions +//use Guzzle\Http\Exception\ClientErrorResponseException; + +/** + * Accounts Handler Special Page + * + * A "Controller" (but in MediaWiki) that binds MediaWiki specific + * to other decoupled moving parts. + * + * Related documentation: + * * http://www.mediawiki.org/wiki/Manual:Special_pages + */ +class AccountsHandlerSpecialPage extends UnlistedSpecialPage +{ + /** + * FxA API handler + * + * @var FirefoxAccountsManager + */ + private $apiHandler; + + /** + * Constructor following MW initialization convention + */ + public function __construct() + { + try { + $apiHandler = new FirefoxAccountsManager( $GLOBALS['wgWebPlatformAuth'] ); + } catch ( Exception $e ) { + $this->getOutput()->showErrorPage( 'error' , $e->getMessage() ); + + return; + } + + $this->apiHandler = $apiHandler; + + parent::__construct( 'AccountsHandler' ); + } + + public function execute($par) + { + $this->setHeaders(); + $this->getOutput()->setPageTitle( 'webplatformauth-main-specialpage-title' ); + + switch ( $par ) { + case 'start': + $this->_start(); + break; + case 'signup': + $this->_signup(); + break; + case 'logout': + $this->_logout(); + break; + case 'callback': // should we keep that? or have a callback url and a default? + $this->_callback(); + break; + default: + $this->_default(); + break; + } + } + + private function _callback() + { + $this->getOutput()->addWikiText('== Execution messages =='.PHP_EOL.implode(PHP_EOL.'* ', $GLOBALS['poorman_logging'])); + } + + private function _default() + { + $this->getOutput()->redirect( $this->_getRefererUri() ); + } + + private function _signup() + { + $goto = $this->apiHandler->initHandshake( $this->_getRefererUri() , true ); + $this->getOutput()->redirect( $goto ); + } + + private function _start() + { + $goto = $this->apiHandler->initHandshake( $this->_getRefererUri() ); + $this->getOutput()->redirect( $goto ); + } + + private function _logout() + { + $GLOBALS['wgUser']->logout(); + unset($_COOKIES); + $this->getOutput()->addWikiText( 'Logout method' ); + + $this->getOutput()->redirect( $this->_getRefererUri() ); + } + + private function _getRefererUri() + { + // Generally the wgArticlePagh ends with .../$1 + $path = str_replace('$1','', $GLOBALS['wgArticlePath']); + $h = $this->getRequest()->getAllHeaders(); + + return ( isset( $h['REFERER'] ) ) ? $h['REFERER'] : $path; + } +} diff --git a/includes/specials/WPA_RenewSession.php b/includes/specials/WPA_RenewSession.php deleted file mode 100644 index 3f18a80..0000000 --- a/includes/specials/WPA_RenewSession.php +++ /dev/null @@ -1,19 +0,0 @@ -<?php -class WPARenewSession extends UnlistedSpecialPage { - - /** - * Constructor - */ - function __construct() { - parent::__construct( 'RenewSession' ); - } - - function execute( $par ) { - global $wgUser; - $_SESSION['wsUserEmail'] = $wgUser->getEmail(); - $_SESSION['wsUserEffectiveGroups'] = implode(',',$wgUser->getEffectiveGroups()); - $_SESSION['wsUserPageURL'] = $wgUser->getUserPage()->getFullURL(); - - $this->getOutput()->redirect( $par ); - } -} \ No newline at end of file diff --git a/includes/specials/WebPlatformAuthLogin.php b/includes/specials/WebPlatformAuthLogin.php new file mode 100644 index 0000000..ae9df5e --- /dev/null +++ b/includes/specials/WebPlatformAuthLogin.php @@ -0,0 +1,45 @@ +<?php + +/** + * MediaWiki SSO using Firefox Accounts + * + * Project details are available on the WebPlatform wiki + * https://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension + **/ + +/** + * Extend original MediaWiki Special:UserLogin + * + * Expected result redirect to FxA appropriate pages: + * * login + * * create account + * + * Related documentation: + * * http://www.mediawiki.org/wiki/Manual:Special_pages + */ +class WebPlatformAuthLogin extends LoginForm +{ + + public function execute($subPage) + { + // Generally the wgArticlePagh ends with .../$1 + $path = str_replace( '$1' , '' , $GLOBALS['wgArticlePath'] ); + $type = $this->getRequest()->getVal( 'type' ); + + // Change AccountsHandler for better name later + $urls['signin'] = $path.'Special:AccountsHandler/start'; + $urls['signup'] = $path.'Special:AccountsHandler/signup'; + + $this->setHeaders(); + + if ( $subPage === 'signup' ) { + $selectedUri = $urls['signup']; + } elseif ( $type === 'signup' ) { + $selectedUri = $urls['signup']; + } else { + $selectedUri = $urls['signin']; + } + + $this->getOutput()->redirect( $selectedUri ); + } +} diff --git a/includes/specials/WebPlatformAuthLogout.php b/includes/specials/WebPlatformAuthLogout.php new file mode 100644 index 0000000..4c7f46c --- /dev/null +++ b/includes/specials/WebPlatformAuthLogout.php @@ -0,0 +1,33 @@ +<?php + +/** + * MediaWiki SSO using Firefox Accounts + * + * Project details are available on the WebPlatform wiki + * https://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension + **/ + +/** + * Extend original MediaWiki Special:UserLogout + * + * Expected result redirect to FxA appropriate pages: + * * logout + * + * Related documentation: + * * http://www.mediawiki.org/wiki/Manual:Special_pages + */ +class WebPlatformAuthLogout extends SpecialUserlogout +{ + public function execute($subPage) + { + // Generally the wgArticlePagh ends with .../$1 + $path = str_replace( '$1' , '' , $GLOBALS['wgArticlePath'] ); + $selectedUri = $path . 'Special:AccountsHandler/logout'; + + // TODO, delete local session! + // ... iframe with click event on #signout ? + // https://github.com/mozilla/fxa-content-server/blob/master/app/scripts/views/settings.js#L34 + + $this->getOutput()->redirect( $selectedUri ); + } +} diff --git a/includes/specials/WebPlatformAuthPassword.php b/includes/specials/WebPlatformAuthPassword.php new file mode 100644 index 0000000..a00c143 --- /dev/null +++ b/includes/specials/WebPlatformAuthPassword.php @@ -0,0 +1,24 @@ +<?php + +/** + * MediaWiki SSO using Firefox Accounts + * + * Project details are available on the WebPlatform wiki + * https://docs.webplatform.org/wiki/WPD:Projects/SSO/MediaWikiExtension + **/ + +/** + * Extend original MediaWiki Special:ChangePassword + * + * Related documentation: + * * http://www.mediawiki.org/wiki/Manual:Special_pages + */ +class WebPlatformAuthPassword extends SpecialChangePassword +{ + + public function execute($subPage) + { + // Make Non Hardcoded! #IMPROVEMENT + $this->getOutput()->redirect( 'https://accounts.webplatform.org/settings' ); + } +} diff --git a/vendor/.gitkeep b/vendor/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/vendor/.gitkeep -- To view, visit https://gerrit.wikimedia.org/r/136152 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: If5d3caec9b05b8362190f8ef30658efd135cdb4c Gerrit-PatchSet: 4 Gerrit-Project: mediawiki/extensions/WebPlatformAuth Gerrit-Branch: master Gerrit-Owner: Renoirb <ren...@w3.org> Gerrit-Reviewer: Aaron Schulz <asch...@wikimedia.org> Gerrit-Reviewer: Reedy <re...@wikimedia.org> Gerrit-Reviewer: Renoirb <ren...@w3.org> Gerrit-Reviewer: Siebrand <siebr...@kitano.nl> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits