Add $wgAuthenticationTokenVersion
authorBrad Jorsch <bjorsch@wikimedia.org>
Mon, 1 Feb 2016 20:07:09 +0000 (15:07 -0500)
committerBryanDavis <bdavis@wikimedia.org>
Tue, 2 Feb 2016 19:21:52 +0000 (19:21 +0000)
This allows for quickly invalidating everyone's session all at once by
changing a single value.

As a side effect, setting this also stops the user_token field from
the database from being served to the user as a cookie.

This mitigates but doesn't completely solve T49490, as it allows for
invalidating all existing sessions and token-cookies but does not help
if the user_token field in the database was leaked.

Bug: T49490
Change-Id: I9d316a6bbb36278d138f39a89125ebb8cc71b28f

includes/DefaultSettings.php
includes/user/User.php

index f30854a..2b3e6e2 100644 (file)
@@ -4633,6 +4633,18 @@ $wgUserrightsInterwikiDelimiter = '@';
  */
 $wgSecureLogin = false;
 
+/**
+ * Versioning for authentication tokens.
+ *
+ * If non-null, this is combined with the user's secret (the user_token field
+ * in the DB) to generate the token cookie. Changing this will invalidate all
+ * active sessions (i.e. it will log everyone out).
+ *
+ * @since 1.27
+ * @var string|null
+ */
+$wgAuthenticationTokenVersion = null;
+
 /** @} */ # end user accounts }
 
 /************************************************************************//**
index d71e5e1..ac78abe 100644 (file)
@@ -1184,7 +1184,7 @@ class User implements IDBAccessObject {
 
                if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
                        $this->loadFromUserObject( $proposedUser );
-                       $request->setSessionData( 'wsToken', $this->mToken );
+                       $request->setSessionData( 'wsToken', $this->getToken( false ) );
                        wfDebug( "User: logged in from $from\n" );
                        return true;
                } else {
@@ -2457,14 +2457,33 @@ class User implements IDBAccessObject {
         * Get the user's current token.
         * @param bool $forceCreation Force the generation of a new token if the
         *   user doesn't have one (default=true for backwards compatibility).
-        * @return string Token
+        * @return string|null Token
         */
        public function getToken( $forceCreation = true ) {
+               global $wgAuthenticationTokenVersion;
+
                $this->load();
                if ( !$this->mToken && $forceCreation ) {
                        $this->setToken();
                }
-               return $this->mToken;
+
+               // If the user doesn't have a token, return null to indicate that.
+               // Otherwise, hmac the version with the secret if we have a version.
+               if ( !$this->mToken ) {
+                       return null;
+               } elseif ( $wgAuthenticationTokenVersion === null ) {
+                       return $this->mToken;
+               } else {
+                       $ret = MWCryptHash::hmac( $wgAuthenticationTokenVersion, $this->mToken, false );
+
+                       // The raw hash can be overly long. Shorten it up.
+                       $len = max( 32, self::TOKEN_LENGTH );
+                       if ( strlen( $ret ) < $len ) {
+                               // Should never happen, even md5 is 128 bits
+                               throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' );
+                       }
+                       return substr( $ret, -$len );
+               }
        }
 
        /**
@@ -3595,7 +3614,7 @@ class User implements IDBAccessObject {
                }
                $session = array(
                        'wsUserID' => $this->mId,
-                       'wsToken' => $this->mToken,
+                       'wsToken' => $this->getToken( false ),
                        'wsUserName' => $this->getName()
                );
                $cookies = array(
@@ -3603,7 +3622,7 @@ class User implements IDBAccessObject {
                        'UserName' => $this->getName(),
                );
                if ( $rememberMe ) {
-                       $cookies['Token'] = $this->mToken;
+                       $cookies['Token'] = $this->getToken( false );
                } else {
                        $cookies['Token'] = false;
                }