jenkins-bot has submitted this change and it was merged.
Change subject: Add JavaScript login check against the central wiki
......................................................................
Add JavaScript login check against the central wiki
The ability for CentralAuth to log the user in everywhere via a "push"
model is going away, as browsers tighten their security on third-party
cookies.
This changes things to a "pull" model instead. If the user is logged
out, a request is sent to the central wiki, which puts the necessary
data in memcache so we can set the necessary cookies as first-party
instead. To avoid doing this with every page view, it is instead done
just once and a cookie or localStorage token is set to remember this.
Note that, to allow Internet Explorer to be able to send and receive the
cookies via CORS or the iframe, a P3P header must be included when the
cookies are set on the central domain. The default policy used is
clearly invalid: it has no valid tokens at all, while a real policy must
contain at least an "access" token and one statement, which would
contain either the NID token or at least one "purpose" token, one
"recipient" token, and one "retention" token.
Change-Id: Ib6e9cce4fa4c5f1482c59bfac28087f558786efe
---
M CentralAuth.alias.php
M CentralAuth.i18n.php
M CentralAuth.php
M CentralAuthHooks.php
M CentralAuthUser.php
A modules/ext.centralauth.centralautologin.js
A specials/SpecialCentralAutoLogin.php
M specials/SpecialCentralLogin.php
8 files changed, 794 insertions(+), 6 deletions(-)
Approvals:
Tim Starling: Looks good to me, approved
jenkins-bot: Verified
diff --git a/CentralAuth.alias.php b/CentralAuth.alias.php
index 6944c7f..71b4140 100644
--- a/CentralAuth.alias.php
+++ b/CentralAuth.alias.php
@@ -12,6 +12,7 @@
$specialPageAliases['en'] = array(
'CentralAuth' => array( 'CentralAuth' ),
'AutoLogin' => array( 'AutoLogin' ),
+ 'CentralAutoLogin' => array( 'CentralAutoLogin' ),
'MergeAccount' => array( 'MergeAccount' ),
'GlobalGroupMembership' => array( 'GlobalUserRights',
'GlobalGroupMembership' ),
'GlobalGroupPermissions' => array( 'GlobalGroupPermissions' ),
@@ -786,4 +787,4 @@
/** Chinese (Hong Kong) (中文(香港)) */
$specialPageAliases['zh-hk'] = array(
'GlobalGroupMembership' => array( '全域用戶權限' ),
-);
\ No newline at end of file
+);
diff --git a/CentralAuth.i18n.php b/CentralAuth.i18n.php
index 5e5a76c..7d661f9 100644
--- a/CentralAuth.i18n.php
+++ b/CentralAuth.i18n.php
@@ -303,6 +303,25 @@
When you [[Special:UserLogin|log in]], the central login system instructs your
browser to request this page from all linked domains, using image links.
You have requested this page without providing any authentication data, so it
does nothing.',
+ // Central login
+ 'centralautologin' => 'Central login',
+ 'centralauth-centralautologin-desc' => 'This special page is used
internally by MediaWiki.
+When you visit a linked domain while not logged in, the central login system
uses this page to determine whether you are logged in to the central domain.
+You have requested this page without providing any authentication data, so it
does nothing.',
+ 'centralauth-centralautologin-alreadyloggedinlocally' => 'You are
already logged in locally',
+ 'centralauth-centralautologin-badparams' => 'Authentication parameters
specified were invalid',
+ 'centralauth-centralautologin-lostsession' => 'Session data was lost',
+ 'centralauth-centralautologin-badstate' => 'Invalid state "$1"',
+ 'centralauth-centralautologin-notposted' => 'Central login form must be
posted',
+ 'centralauth-centralautologin-badstate-central' => 'State "$1" is not
valid on the central wiki',
+ 'centralauth-centralautologin-badstate-local' => 'State "$1" is not
valid on the local wiki',
+ 'centralauth-centralautologin-badwiki' => 'The wiki "$1" is not valid
for central login',
+ 'centralauth-centralautologin-corsfail' => 'CORS origin check failed',
+ 'centralauth-centralautologin-p3p-explanation' => 'Certain browsers
require a P3P compact policy for cookies to be sent or received in some cases,
including the situation involved in the SUL login check. This is a needless
hoop to jump through in this situation, considering that all pages involved are
in the same wiki group and P3P itself is mostly obsolete and abandoned;
fortunately, the invalid policy linking to this page is currently considered
"good enough" by these browsers\' default configurations.
+
+Please review this site\'s human-readable privacy policy to determine how any
information collected might be used.',
+ 'centralauth-centralautologin-logged-in' => 'You are centrally logged
in as $1. Reload the page to apply your user settings.',
+
// Global group membership
'globalgroupmembership' => 'Membership in global groups',
@@ -818,6 +837,26 @@
'autologin' => '{{doc-special|AutoLogin|unlisted=1}}
See example: [[w:Special:Autologin]].',
'centralauth-autologin-desc' => 'This is the text shown on
[[Special:AutoLogin]] when this page is requested by a user, not by the
automatic global login system.',
+ 'centralautologin' => '{{doc-special|CentralAutoLogin|unlisted=1}}
+See example: [[w:Special:CentralAutoLogin]].',
+ 'centralauth-centralautologin-desc' => 'This is the text shown on
[[Special:CentralAutoLogin]] when this page is requested by a user, not by the
automatic global login system.',
+ 'centralauth-centralautologin-alreadyloggedinlocally' => 'Error message
shown when [[Special:CentralAutoLogin]] is called on the local wiki while the
user is already logged in.',
+ 'centralauth-centralautologin-badparams' => 'Error message when the
required authentication parameters are missing or invalid',
+ 'centralauth-centralautologin-lostsession' => 'Error message when the
session data is lost or overwritten',
+ 'centralauth-centralautologin-badstate' => 'Error message shown when an
invalid state is given for [[Special:CentralAutoLogin]].
+* $1 - Name of the state',
+ 'centralauth-centralautologin-notposted' => 'Error message shown when
[[Special:CentralAutoLogin]] is called in form mode with an HTTP method other
than POST',
+ 'centralauth-centralautologin-badstate-central' => 'Error message shown
when [[Special:CentralAutoLogin]] is called on the central wiki with a state
intended for use on the local wiki.
+* $1 - Name of the state',
+ 'centralauth-centralautologin-badstate-local' => 'Error message shown
when [[Special:CentralAutoLogin]] is called on the local wiki with a state
intended for use on the central wiki.
+* $1 - Name of the state',
+ 'centralauth-centralautologin-badwiki' => 'Error message shown when an
unacceptable wiki ID is given to [[Special:CentralAutoLogin]].
+* $1 - The wiki ID',
+ 'centralauth-centralautologin-corsfail' => 'Error message shown when
the CORS origin check fails.',
+ 'centralauth-centralautologin-p3p-explanation' => 'Explanation for the
P3P hack that allows IE to set cookies.',
+ 'centralauth-centralautologin-logged-in' => 'Message displayed by
JavaScript when the auto-login succeeds.
+* $1 - User name
+* $2 - User gender code {{gender}}',
'globalgroupmembership' => '{{doc-special|GlobalGroupMembership}}',
'globalgrouppermissions' => '{{doc-special|GlobalGroupPermissions}}
See example: [[w:Special:GlobalGroupPermissions]] and
[[w:Special:SpecialPages]]',
diff --git a/CentralAuth.php b/CentralAuth.php
index d30a947..3c108a8 100644
--- a/CentralAuth.php
+++ b/CentralAuth.php
@@ -73,7 +73,7 @@
* login wiki will use a session/cookie to handle unified login sessions
across wikis.
*
* On login, users will be redirected to the login wiki's
Special:CentralLogin/login
- * page and then redirected to Special:CentralAutoLogin back on the
originating wiki.
+ * page and then redirected to Special:CentralLogin back on the originating
wiki.
* In the process, the central login wiki cookie and session will be set.
* As the user accesses other wikis, the login wiki will be checked via
JavaScript
* to check login status and set the local session and cookies.
@@ -122,6 +122,25 @@
* Should be a 20x20px PNG.
*/
$wgCentralAuthLoginIcon = false;
+
+/**
+ * Specify a P3P header value to be used when setting CentralAuth cookies on
+ * the login wiki ($wgCentralAuthLoginWiki).
+ *
+ * When set true, a invalid policy (lacking all required tokens) will be sent
+ * that none the less serves to allow current versions of IE with the default
+ * privacy settings to see the cookies in the auto-login check.
+ *
+ * Set false to disable sending the P3P header altogether. Note this will
+ * likely break the auto-login check in IE, unless the header is being set
+ * globally elsewhere (e.g. in the webserver).
+ *
+ * Otherwise, whatever string is assigned here will be sent as the value of the
+ * P3P header.
+ *
+ * @var bool|string
+ */
+$wgCentralAuthCookiesP3P = true;
/**
* If true, local accounts will be created for active global sessions
@@ -177,6 +196,7 @@
$wgAutoloadClasses['CentralAuthSuppressUserJob'] =
"$caBase/SuppressUserJob.php";
$wgAutoloadClasses['WikiSet'] = "$caBase/WikiSet.php";
$wgAutoloadClasses['SpecialAutoLogin'] =
"$caBase/specials/SpecialAutoLogin.php";
+$wgAutoloadClasses['SpecialCentralAutoLogin'] =
"$caBase/specials/SpecialCentralAutoLogin.php";
$wgAutoloadClasses['CentralAuthUserArray'] =
"$caBase/CentralAuthUserArray.php";
$wgAutoloadClasses['CentralAuthUserArrayFromResult'] =
"$caBase/CentralAuthUserArray.php";
$wgAutoloadClasses['SpecialGlobalGroupMembership'] =
"$caBase/specials/SpecialGlobalGroupMembership.php";
@@ -218,6 +238,8 @@
$wgHooks['MakeGlobalVariablesScript'][] =
'CentralAuthHooks::onMakeGlobalVariablesScript';
$wgHooks['SpecialPasswordResetOnSubmit'][] =
'CentralAuthHooks::onSpecialPasswordResetOnSubmit';
$wgHooks['OtherBlockLogLink'][] = 'CentralAuthHooks::getBlockLogLink';
+$wgHooks['BeforePageDisplay'][] = 'CentralAuthHooks::onBeforePageDisplay';
+$wgHooks['ResourceLoaderGetConfigVars'][] =
'CentralAuthHooks::onResourceLoaderGetConfigVars';
$wgHooks['ApiTokensGetTokenTypes'][] =
'ApiDeleteGlobalAccount::injectTokenFunction';
$wgHooks['ApiTokensGetTokenTypes'][] =
'ApiSetGlobalAccountStatus::injectTokenFunction';
@@ -250,6 +272,7 @@
$wgSpecialPages['CentralAuth'] = 'SpecialCentralAuth';
$wgSpecialPages['CentralLogin'] = 'SpecialCentralLogin';
$wgSpecialPages['AutoLogin'] = 'SpecialAutoLogin';
+$wgSpecialPages['CentralAutoLogin'] = 'SpecialCentralAutoLogin';
$wgSpecialPages['MergeAccount'] = 'SpecialMergeAccount';
$wgSpecialPages['GlobalGroupMembership'] = 'SpecialGlobalGroupMembership';
$wgSpecialPages['GlobalGroupPermissions'] = 'SpecialGlobalGroupPermissions';
@@ -327,6 +350,18 @@
'centralauth-completelogin-finishing'
),
) + $commonModuleInfo;
+$wgResourceModules['ext.centralauth.centralautologin'] = array(
+ 'scripts' => 'ext.centralauth.centralautologin.js',
+ 'position' => 'top',
+ 'dependencies' => array(
+ 'mediawiki.notify',
+ 'mediawiki.jqueryMsg',
+ ),
+ 'messages' => array(
+ 'centralautologin',
+ 'centralauth-centralautologin-logged-in',
+ ),
+) + $commonModuleInfo;
$wgResourceModules['ext.centralauth.noflash'] = array(
'styles' => 'ext.centralauth.noflash.css',
diff --git a/CentralAuthHooks.php b/CentralAuthHooks.php
index 78e0e4b..36cbd03 100644
--- a/CentralAuthHooks.php
+++ b/CentralAuthHooks.php
@@ -881,6 +881,32 @@
}
/**
+ * @param &$out OutputPage
+ * @param &$skin Skin
+ * @return bool
+ */
+ static function onBeforePageDisplay( &$out, &$skin ) {
+ global $wgCentralAuthLoginWiki;
+ if ( $wgCentralAuthLoginWiki && wfWikiID() !==
$wgCentralAuthLoginWiki && $out->getUser()->isAnon() ) {
+ $out->addModules( 'ext.centralauth.centralautologin' );
+ }
+ return true;
+ }
+
+ /**
+ * @param &$vars
+ * @return bool
+ */
+ static function onResourceLoaderGetConfigVars( &$vars ) {
+ global $wgUser, $wgCentralAuthLoginWiki;
+ $vars['wgCentralAuthWikiID'] = wfWikiID();
+ $vars['wgCentralAuthCentralAutoLoginEndpoint'] = wfExpandUrl(
WikiMap::getForeignURL(
+ $wgCentralAuthLoginWiki, 'Special:CentralAutoLogin/$1'
+ ), PROTO_HTTPS );
+ return true;
+ }
+
+ /**
* @param $auth
* @param $user User
* @param $params
diff --git a/CentralAuthUser.php b/CentralAuthUser.php
index 2ec4d0f..5592b0c 100644
--- a/CentralAuthUser.php
+++ b/CentralAuthUser.php
@@ -2002,6 +2002,23 @@
$remember = $remember->getOption( 'rememberpassword' );
}
+ // IE requires that a P3P header be provided for the cookies to
be
+ // visible to the js auto-login check.
+ global $wgCentralAuthCookiesP3P, $wgCentralAuthLoginWiki;
+ if ( $wgCentralAuthLoginWiki === wfWikiID() ) {
+ if ( $wgCentralAuthCookiesP3P === true ) {
+ // Note this policy is not valid: it has no
valid tokens, while
+ // a valid policy would contain an "access"
token and at least
+ // one statement, which would contain either
the NID token or
+ // at least one "purpose" token, one
"recipient" token, and one
+ // "retention" token.
+ $url = Title::makeTitle( NS_SPECIAL,
'CentralAutoLogin/P3P' )->getCanonicalURL();
+ header( "P3P: CP=\"This is not a P3P policy!
See $url for more info.\"", true );
+ } elseif ( $wgCentralAuthCookiesP3P ) {
+ header( "P3P: $wgCentralAuthCookiesP3P", true );
+ }
+ }
+
$session = array();
$session['user'] = $this->mName;
self::setCookie( 'User', $this->mName );
diff --git a/modules/ext.centralauth.centralautologin.js
b/modules/ext.centralauth.centralautologin.js
new file mode 100644
index 0000000..b154b7a
--- /dev/null
+++ b/modules/ext.centralauth.centralautologin.js
@@ -0,0 +1,169 @@
+( function ( mw, $ ) {
+ // Are we already logged in?
+ if ( mw.config.get( 'wgUserName' ) !== null ) {
+ return;
+ }
+
+ // Do we already know we're logged out centrally?
+ if ( mw.config.get( 'wgCanonicalSpecialPageName' ) !== 'Userlogin' ) {
+ if ( 'localStorage' in window && +localStorage.getItem(
'CentralAuthAnon' ) > new Date().getTime() ) {
+ return;
+ }
+
+ // Can't use $.cookie(), because we want to check this at the
top of
+ // the page and that isn't loaded until the bottom.
+ if ( /(^|; )CentralAuthAnon=1/.test( document.cookie ) ) {
+ return;
+ }
+ }
+
+ var centralEndpoint = mw.config.get(
'wgCentralAuthCentralAutoLoginEndpoint' ),
+ localEndpoint = mw.config.get( 'wgArticlePath' ).replace( '$1',
'Special:CentralAutoLogin/$1' ),
+ wikiId = mw.config.get( 'wgCentralAuthWikiID' );
+
+ function getReturnToParams() {
+ var params = location.search.slice( 1 ).split( '&' ),
+ len = params.length,
+ param, i,
+ ret = [];
+ for ( i = 0; i < len; i++ ) {
+ param = params[i].split( '=' );
+ param = decodeURIComponent( param[0] );
+ if ( param === 'returnto' || param === 'returntoquery'
) {
+ ret.push( params[i] );
+ }
+ }
+ return ret.join( '&' );
+ }
+
+ // These functions should be declared in the "if ( jQuery.support.cors
)"
+ // where they're actually needed, but jshint insists they be placed up
here
+ // instead.
+ // Documentation: [[mw:Auth systems/SUL2#Local wiki Javascript Auth
check]]
+
+ // State C1
+ function fetchCentralUserId() {
+ // Also, disable this brain-damaged warning. We're not going to
rewrite
+ // all the PHP code because it doesn't like "ret.gu_id".
+ /*jshint camelcase: false */
+ $.ajax( {
+ url: centralEndpoint.replace( '$1', 'C1' ),
+ dataType: 'json',
+ type: 'POST',
+ data: { wikiid: wikiId },
+ xhrFields: {
+ withCredentials: true
+ }
+ } ).done( function ( ret ) {
+ /*jshint camelcase: false */
+ if ( ret.status === 'ok' ) {
+ if ( +ret.gu_id <= 0 ) {
+ var t = new Date();
+ t.setTime( t.getTime() + 86400000 );
+ if ( 'localStorage' in window ) {
+ localStorage.setItem(
'CentralAuthAnon', t.getTime() );
+ } else {
+ document.cookie =
'CentralAuthAnon=1; expires=' + t.toGMTString() + '; path=/';
+ }
+ } else {
+ if ( 'localStorage' in window ) {
+ localStorage.removeItem(
'CentralAuthAnon' );
+ }
+ if ( /(^|; )CentralAuthAnon=/.test(
document.cookie ) ) {
+ document.cookie =
'CentralAuthAnon=0; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/';
+ }
+ createLocalSessionForCentralLogin(
ret.gu_id );
+ }
+ }
+ } );
+ }
+
+ // State L1
+ function createLocalSessionForCentralLogin( guid ) {
+ /*jshint camelcase: false */
+ $.ajax( {
+ url: localEndpoint.replace( '$1', 'L1' ),
+ dataType: 'json',
+ type: 'POST',
+ data: { gu_id: guid }
+ } ).done( function ( ret ) {
+ if ( ret.status === 'ok' ) {
+ haveCentralWikiPutLoginDataInMemcached(
ret.token );
+ }
+ } );
+ }
+
+ // State C2
+ function haveCentralWikiPutLoginDataInMemcached( token ) {
+ $.ajax( {
+ url: centralEndpoint.replace( '$1', 'C2' ),
+ dataType: 'json',
+ type: 'POST',
+ data: { token: token, wikiid: wikiId },
+ xhrFields: {
+ withCredentials: true
+ }
+ } ).done( function ( ret ) {
+ if ( ret.status === 'ok' ) {
+ checkMemcachedDataAndSetCentralAuthCookies();
+ }
+ } );
+ }
+
+ // State L2
+ function checkMemcachedDataAndSetCentralAuthCookies() {
+ $.ajax( {
+ url: localEndpoint.replace( '$1', 'L2' ),
+ dataType: 'json',
+ type: 'POST',
+ data: {}
+ } ).done( function ( ret ) {
+ if ( ret.status === 'ok' ) {
+ if ( mw.config.get(
'wgCanonicalSpecialPageName' ) === 'Userlogin' ) {
+ var url = mw.config.get(
'wgArticlePath' ).replace( '$1', 'Special:CentralLogin/Status' );
+ url += ( url.indexOf( '?' ) < 0 ? '?' :
'&' ) + getReturnToParams();
+ location.href = url;
+ } else {
+ mw.notify(
+ mw.message(
'centralauth-centralautologin-logged-in',
+ ret.userName,
ret.userGender
+ ), {
+ title: mw.message(
'centralautologin' ).plain(),
+ autoHide: false,
+ tag: 'CentralAutoLogin'
+ }
+ );
+ }
+ }
+ } );
+ }
+
+ if ( jQuery.support.cors ) {
+ // We can do AJAX calls using CORS.
+
+ // Start by calling state C1
+ fetchCentralUserId();
+ } else {
+ // We have to do it with an iframe. State "L0" returns the first
+ // auto-posting form for us, and the protocol proceeds from
there
+ // without our intervention.
+ $( function () {
+ var url = localEndpoint.replace( '$1', 'L0' );
+ if ( mw.config.get( 'wgCanonicalSpecialPageName' ) ===
'Userlogin' ) {
+ url += ( url.indexOf( '?' ) < 0 ? '?' : '&' ) +
'oncomplete=status';
+ url += '&' + getReturnToParams();
+ }
+ $( '<iframe>' )
+ .css( {
+ position: 'absolute',
+ top: 0,
+ left: '-10px',
+ width: '1px',
+ height: '1px'
+ } )
+ .attr( 'src', url )
+ .appendTo( document.body );
+ } );
+ }
+
+}( mediaWiki, jQuery ) );
diff --git a/specials/SpecialCentralAutoLogin.php
b/specials/SpecialCentralAutoLogin.php
new file mode 100644
index 0000000..6d89a13
--- /dev/null
+++ b/specials/SpecialCentralAutoLogin.php
@@ -0,0 +1,501 @@
+<?php
+
+/**
+ * Unlisted Special page to set requisite cookies for being logged into this
wiki.
+ *
+ * @ingroup Extensions
+ */
+class SpecialCentralAutoLogin extends UnlistedSpecialPage {
+ private $isForm = false;
+
+ function __construct() {
+ parent::__construct( 'CentralAutoLogin' );
+ }
+
+ function execute( $par ) {
+ global $wgMemc, $wgUser;
+
+ switch ( strval( $par ) ) {
+ case '': // also null and false
+ $this->setHeaders();
+ $this->getOutput()->addWikiMsg(
'centralauth-centralautologin-desc' );
+ return;
+
+ case 'P3P': // Explain the bogus P3P header
+ $this->setHeaders();
+ $this->getOutput()->addWikiMsg(
'centralauth-centralautologin-p3p-explanation' );
+ return;
+
+ case 'L0': // Output form for C1
+ $this->isForm = 1;
+ if ( !$wgUser->isAnon() ) {
+ $data = array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-alreadyloggedinlocally' ),
+ );
+ break;
+ }
+ $data = array(
+ 'status' => 'ok',
+ 'nextState' => 'C1',
+ 'params' => array(
+ 'wikiid' => wfWikiID(),
+ ),
+ );
+ break;
+
+ case 'C1': // Query gu_id
+ $data = $this->checkInputState( $par, true );
+ if ( $data ) {
+ break;
+ }
+
+ global $wgUser;
+ $centralUser = CentralAuthUser::getInstance( $wgUser );
+ $data = array(
+ 'status' => 'ok',
+ 'nextState' => 'L1',
+ 'params' => array(
+ 'gu_id' => $centralUser ?
$centralUser->getId() : 0,
+ )
+ );
+ break;
+
+ case 'L1': // Start session for gu_id
+ $data = $this->checkInputState( $par, false );
+ if ( $data ) {
+ break;
+ }
+
+ $gu_id = +$this->getRequest()->getVal( 'gu_id', 0 );
+ if ( $gu_id <= 0 ) {
+ // Should only get here for iframe mode
+ $script = "var t = new Date();\n" .
+ "t.setTime( t.getTime() + 86400000
);\n" .
+ "if ( 'localStorage' in window ) {\n" .
+ "\tlocalStorage.setItem(
'CentralAuthAnon', t.getTime() );\n" .
+ "} else {\n" .
+ "\tdocument.cookie =
'CentralAuthAnon=1; expires=' + t.toGMTString() + '; path=/';\n" .
+ "}\n";
+ $data = array(
+ 'status' => 'script',
+ 'script' => $script,
+ );
+ break;
+ }
+
+ $script = "if ( 'localStorage' in window ) {\n" .
+ "\tlocalStorage.removeItem( 'CentralAuthAnon'
);\n" .
+ "}\n" .
+ "if ( /(^|; )CentralAuthAnon=/.test(
document.cookie ) ) {\n" .
+ "\tdocument.cookie = 'CentralAuthAnon=0;
expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/';\n" .
+ "}\n";
+
+ // Ensure that a session exists
+ if ( session_id() == '' ) {
+ wfSetupSession();
+ }
+
+ // Create memc token
+ $wikiid = wfWikiID();
+ $memcData = array(
+ 'gu_id' => $gu_id,
+ 'wikiid' => $wikiid,
+ );
+ do {
+ $token = MWCryptRand::generateHex( 32 );
+ $key = CentralAuthUser::memcKey(
'centralautologin-token', $token, $wikiid );
+ } while ( !$wgMemc->add( $key, $memcData, 10 ) );
+
+ // Save memc token for L2
+ $this->getRequest()->setSessionData(
'centralautologin-token', $token );
+
+ $data = array(
+ 'status' => 'ok',
+ 'nextState' => 'C2',
+ 'script' => $script,
+ 'params' => array(
+ 'token' => $token,
+ 'wikiid' => wfWikiID(),
+ )
+ );
+ break;
+
+ case 'C2': // Complete session for memc token
+ $data = $this->checkInputState( $par, true );
+ if ( $data ) {
+ break;
+ }
+
+ // Validate params
+ $wikiid = $this->getRequest()->getVal( 'wikiid', '' );
+ $token = $this->getRequest()->getVal( 'token', '' );
+ if ( $token === '' || $wikiid === '' ) {
+ $data = array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-badparams' )
+ );
+ break;
+ }
+
+ // Load memc data
+ $key = CentralAuthUser::memcKey(
'centralautologin-token', $token, $wikiid );
+ $memcData = $wgMemc->get( $key );
+ $wgMemc->delete( $key );
+
+ // Check memc data
+ global $wgUser;
+ $centralUser = CentralAuthUser::getInstance( $wgUser );
+ if ( !$memcData ||
+ $memcData['wikiid'] !== $wikiid ||
+ !$centralUser ||
+ !$centralUser->getId() ||
+ $memcData['gu_id'] != $centralUser->getId()
+ ) {
+ $data = array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-badparams' )
+ );
+ break;
+ }
+
+ // Write info for session creation into memc
+ $memcData += array(
+ 'userName' => $centralUser->getName(),
+ 'token' => $centralUser->getAuthToken(),
+ );
+ $wgMemc->set( $key, $memcData, 10 );
+
+ $data = array(
+ 'status' => 'ok',
+ 'nextState' => 'L2',
+ 'params' => array(),
+ );
+ break;
+
+ case 'L2': // Set cookies for session in memc
+ $data = $this->checkInputState( $par, false );
+ if ( $data ) {
+ break;
+ }
+
+ // Check saved memc token
+ $token = $this->getRequest()->getSessionData(
'centralautologin-token' );
+ if ( $token === null ) {
+ $data = array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-lostsession' ),
+ );
+ break;
+ }
+
+ // Load memc data
+ $wikiid = wfWikiID();
+ $key = CentralAuthUser::memcKey(
'centralautologin-token', $token, $wikiid );
+ $memcData = $wgMemc->get( $key );
+ $wgMemc->delete( $key );
+
+ // Check memc data
+ if ( !$memcData ||
+ $memcData['wikiid'] !== $wikiid ||
+ !isset( $memcData['userName'] ) ||
+ !isset( $memcData['token'] )
+ ) {
+ $data = array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-lostsession' ),
+ );
+ break;
+ }
+
+ // Load and check CentralAuthUser
+ $centralUser = new CentralAuthUser(
$memcData['userName'] );
+ if ( !$centralUser->getId() || $centralUser->getId() !=
$memcData['gu_id'] ) {
+ $msg = "Wrong user: expected
{$memcData['gu_id']}, got {$centralUser->getId()}";
+ wfDebug( __METHOD__ . ": $msg\n" );
+ $data = array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-lostsession' ),
+ );
+ break;
+ }
+ $loginResult = $centralUser->authenticateWithToken(
$memcData['token'] );
+ if ( $loginResult != 'ok' ) {
+ $msg = "Bad token: $loginResult";
+ wfDebug( __METHOD__ . ": $msg\n" );
+ $data = array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-lostsession' ),
+ );
+ break;
+ }
+
+ // Set a new session cookie, Just In Case™
+ // If either we don't trust PHP's entropy, or if we
need to change
+ // cookie settings when logging in, then change the
session ID
+ // manually.
+ global $wgCookieSecure;
+ $cookieParams = session_get_cookie_params();
+ if ( wfCheckEntropy() && $wgCookieSecure ==
$cookieParams['secure'] ) {
+ session_regenerate_id( false );
+ } else {
+ $tmp = $_SESSION;
+ session_destroy();
+ wfSetupSession( MWCryptRand::generateHex( 32 )
);
+ $_SESSION = $tmp;
+ }
+
+ // Set central cookies too
+ $centralUser->setGlobalCookies( false );
+
+ $data = array(
+ 'status' => 'ok',
+ 'params' => array(),
+ );
+
+ switch ( $this->getRequest()->getVal( 'oncomplete',
'mw.notify' ) ) {
+ case 'mw.notify':
+ $user = User::newFromName(
$centralUser->getName() );
+ $gender = $user->getOption( 'gender' );
+ if ( strval( $gender ) === '' ) {
+ $gender = 'unknown';
+ }
+ $data['script'] = Xml::encodeJsCall(
'top.mw.notify', array(
+ new XmlJsCode( trim( Xml::encodeJsCall(
'top.mw.message', array(
+
'centralauth-centralautologin-logged-in',
+ $user->getName(),
+ $gender
+ ) ), ';' ) ),
+ array(
+ 'title' => wfMessage(
'centralautologin' )->plain(),
+ 'autoHide' => false,
+ 'tag' => 'CentralAutoLogin',
+ ),
+ ) );
+ $data['params']['userName'] = $user->getName();
+ $data['params']['userGender'] = $gender;
+ break;
+
+ case 'status':
+ $request = $this->getRequest();
+ $url = Title::newFromText(
'Special:CentralLogin/Status' )->getFullUrl( array(
+ 'returnto' => $request->getVal(
'returnto' ),
+ 'returntoquery' => $request->getVal(
'returntoquery' ),
+ ) );
+ $data['script'] = 'top.location.href = ' .
Xml::encodeJsVar( $url ) . ';';
+ break;
+ }
+ break;
+
+ default:
+ $data = array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-badstate', $par ),
+ );
+ break;
+ }
+
+ $this->outputData( $data );
+ }
+
+ private function checkInputState( $par, $central ) {
+ global $wgCentralAuthLoginWiki;
+
+ $request = $this->getRequest();
+ $this->isForm = $request->getBool( 'form' );
+
+ // Make sure it was posted.
+ if ( !$request->wasPosted() && $request->getMethod() !==
'OPTIONS' ) {
+ return array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-notposted' ),
+ );
+ }
+
+ // Validate the state for this wiki
+ if ( $central ) {
+ if ( wfWikiID() !== $wgCentralAuthLoginWiki ) {
+ return array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-badstate-central', $par ),
+ );
+ }
+
+ $wikiId = $request->getVal( 'wikiid' );
+ if ( $wikiId === $wgCentralAuthLoginWiki ) {
+ return array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-badwiki', $wikiId ),
+ );
+ }
+ $wiki = WikiMap::getWiki( $wikiId );
+ if ( !$wiki ) {
+ return array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-badwiki', $wikiId ),
+ );
+ }
+
+ // CORS request, validate origin and set CORS headers
+ if ( !$this->isForm ) {
+ $response = $request->response();
+
+ $originHeader = $request->getHeader( 'Origin' );
+ if ( $originHeader === false ) {
+ $origins = array();
+ } else {
+ $origins = explode( ' ', $originHeader
);
+ }
+
+ $wikiOrigin = 'http://' . strtolower(
$wiki->getHostname() );
+ $ok = false;
+ foreach ( $origins as $origin ) {
+ if ( $wikiOrigin === str_replace(
'https://', 'http://', strtolower( $origin ) ) ) {
+ $wikiOrigin = $origin;
+ $ok = true;
+ break;
+ }
+ }
+ if ( !$ok ) {
+ $message = HttpStatus::getMessage( 403
);
+ $response->header( "HTTP/1.1 403
$message", true, 403 );
+ return array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-corsfail' )
+ );
+ }
+
+ $response->header(
"Access-Control-Allow-Origin: $wikiOrigin" );
+ $response->header(
'Access-Control-Allow-Credentials: true' );
+ $this->getOutput()->addVaryHeader( 'Origin' );
+ }
+ } else {
+ if ( wfWikiID() === $wgCentralAuthLoginWiki ) {
+ return array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-badstate-local', $par ),
+ );
+ }
+
+ global $wgUser;
+ if ( !$wgUser->isAnon() ) {
+ return array(
+ 'status' => 'error',
+ 'msg' => array(
'centralauth-centralautologin-alreadyloggedinlocally' ),
+ );
+ }
+ }
+
+ if ( $request->getMethod() === 'OPTIONS' ) {
+ return array(
+ 'status' => 'cors'
+ );
+ }
+
+ return null;
+ }
+
+ private function outputData( $data ) {
+ global $wgMimeType, $wgLanguageCode;
+
+ $output = $this->getOutput();
+ $output->enableClientCache( false );
+ $output->sendCacheControl();
+ $output->disable();
+ $response = $this->getRequest()->response();
+
+ if ( !$this->isForm ) {
+ $frameOptions = $output->getFrameOptions();
+ if ( $frameOptions ) {
+ $response->header( "X-Frame-Options:
$frameOptions" );
+ }
+ }
+
+ if ( $this->isForm ) {
+ $script='';
+ $bodyParams = array();
+ $body='';
+ switch ( $data['status'] ) {
+ case 'script':
+ $script = $data['script'];
+ break;
+
+ case 'ok':
+ if ( isset( $data['script'] ) ) {
+ $script = $data['script'];
+ }
+
+ if ( isset( $data['nextState'] ) ) {
+ $script .= "\n\nfunction doSubmit()
{\n" .
+ "\tif ( document.forms[0] )
{\n" .
+
"\t\tdocument.forms[0].submit();\n" .
+ "\t}\n" .
+ "}";
+ $bodyParams['onload'] = 'doSubmit()';
+
+ switch ( substr( $data['nextState'], 0,
1 ) ) {
+ case 'C':
+ global $wgCentralAuthLoginWiki;
+ $target =
$wgCentralAuthLoginWiki;
+
+ default:
+ $target =
$this->getRequest()->getVal( 'wikiid', wfWikiID() );
+ break;
+ }
+ $body .= "\n" . Html::openElement(
'form', array(
+ 'method' => 'POST',
+ 'action' =>
WikiMap::getForeignURL( $target, 'Special:CentralAutoLogin/' .
$data['nextState'] ),
+ ) ) . "\n";
+ $body .= Html::hidden( 'form', '1' ) .
"\n";
+ foreach ( array( 'oncomplete',
'returnto', 'returntoquery' ) as $k ) {
+ $v =
$this->getRequest()->getVal( $k );
+ if ( $v !== null ) {
+ $body .= Html::hidden(
$k, $v ) . "\n";
+ }
+ }
+ foreach ( $data['params'] as $k => $v )
{
+ $body .= Html::hidden( $k, $v )
. "\n";
+ }
+ $body .= Html::closeElement( 'form' );
+ }
+ break;
+
+ case 'error':
+ $params = $data['msg'];
+ $key = array_shift( $params );
+ $body = wfMessage( $key, $params )->escaped();
+ break;
+ }
+
+ $response->header( "Content-type: $wgMimeType;
charset=UTF-8" );
+ $response->header( 'Content-language: ' .
$wgLanguageCode );
+ print Html::htmlHeader();
+ print Html::openElement( 'head' );
+ print Html::element( 'title', null,
$output->getHTMLTitle() );
+ if ( $script !== '' ) {
+ print Html::inlineScript( $script );
+ }
+ print Html::closeElement( 'head' );
+ print Html::openElement( 'body', $bodyParams );
+ print $body;
+ print Html::closeElement( 'body' );
+ print Html::closeElement( 'html' );
+ } else {
+ $response->header( "Content-type: application/json;
charset=UTF-8" );
+ $response->header( 'Content-language: ' .
$wgLanguageCode );
+ switch ( $data['status'] ) {
+ case 'cors':
+ break;
+
+ case 'ok':
+ $data['params']['status'] = 'ok';
+ print FormatJson::encode( $data['params'] );
+ break;
+
+ default:
+ print FormatJson::encode( $data );
+ break;
+ }
+ }
+ }
+}
diff --git a/specials/SpecialCentralLogin.php b/specials/SpecialCentralLogin.php
index b4d1ab6..f01ce22 100644
--- a/specials/SpecialCentralLogin.php
+++ b/specials/SpecialCentralLogin.php
@@ -185,8 +185,8 @@
// The user wants an HTTP redirect link (as well as
other links) and
// this is on HTTPS, so send a redirect to the success
page in HTTP.
$query = array(
- 'returnTo' => $attempt['returnTo'],
- 'returnToQuery' => $attempt['returnToQuery']
+ 'returnto' => $attempt['returnTo'],
+ 'returntoquery' => $attempt['returnToQuery']
);
$url = $this->getFullTitle()->getFullUrl( $query,
false, PROTO_HTTP );
$this->getOutput()->redirect( $url );
@@ -212,8 +212,8 @@
// Show the login success page
$form = new LoginForm( new FauxRequest() );
$form->showReturnToPage( 'success',
- $this->getRequest()->getVal( 'returnTo', '' ),
- $this->getRequest()->getVal( 'returnToQuery', '' )
+ $this->getRequest()->getVal( 'returnto', '' ),
+ $this->getRequest()->getVal( 'returntoquery', '' )
);
$this->getOutput()->setPageTitle( $this->msg(
'centralloginsuccesful' ) );
// Show any icons that trigger cross-domain cookies
--
To view, visit https://gerrit.wikimedia.org/r/58924
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Ib6e9cce4fa4c5f1482c59bfac28087f558786efe
Gerrit-PatchSet: 17
Gerrit-Project: mediawiki/extensions/CentralAuth
Gerrit-Branch: master
Gerrit-Owner: Anomie <[email protected]>
Gerrit-Reviewer: Aaron Schulz <[email protected]>
Gerrit-Reviewer: Alex Monk <[email protected]>
Gerrit-Reviewer: Anomie <[email protected]>
Gerrit-Reviewer: CSteipp <[email protected]>
Gerrit-Reviewer: Hoo man <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: Tim Starling <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits