}
// Try bot passwords
- if ( $authRes === false && $this->getConfig()->get( 'EnableBotPasswords' ) &&
- strpos( $params['name'], BotPassword::getSeparator() ) !== false
+ if (
+ $authRes === false && $this->getConfig()->get( 'EnableBotPasswords' ) &&
+ ( $botLoginData = BotPassword::canonicalizeLoginData( $params['name'], $params['password'] ) )
) {
$status = BotPassword::login(
- $params['name'], $params['password'], $this->getRequest()
+ $botLoginData[0], $botLoginData[1], $this->getRequest()
);
if ( $status->isOK() ) {
$session = $status->getValue();
$authRes = 'Success';
$loginType = 'BotPassword';
- } else {
+ } elseif ( !$botLoginData[2] ) {
$authRes = 'Failed';
$message = $status->getMessage();
LoggerFactory::getInstance( 'authentication' )->info(
] );
if ( $this->operation === 'insert' || !empty( $data['resetPassword'] ) ) {
- $this->password = PasswordFactory::generateRandomPasswordString(
- max( 32, $this->getConfig()->get( 'MinimalPasswordLength' ) )
- );
+ $this->password = BotPassword::generatePassword( $this->getConfig() );
$passwordFactory = new PasswordFactory();
$passwordFactory->init( RequestContext::getMain()->getConfig() );
$password = $passwordFactory->newFromPlaintext( $this->password );
$out->addWikiMsg(
'botpasswords-newpassword',
htmlspecialchars( $username . $sep . $this->par ),
- htmlspecialchars( $this->password )
+ htmlspecialchars( $this->password ),
+ htmlspecialchars( $username ),
+ htmlspecialchars( $this->par . $sep . $this->password )
);
$this->password = null;
}
return (bool)$dbw->affectedRows();
}
+ /**
+ * Returns a (raw, unhashed) random password string.
+ * @param Config $config
+ * @return string
+ */
+ public static function generatePassword( $config ) {
+ return PasswordFactory::generateRandomPasswordString(
+ max( 32, $config->get( 'MinimalPasswordLength' ) ) );
+ }
+
+ /**
+ * There are two ways to login with a bot password: "username@appId", "password" and
+ * "username", "appId@password". Transform it so it is always in the first form.
+ * Returns [bot username, bot password, could be normal password?] where the last one is a flag
+ * meaning this could either be a bot password or a normal password, it cannot be decided for
+ * certain (although in such cases it almost always will be a bot password).
+ * If this cannot be a bot password login just return false.
+ * @param string $username
+ * @param string $password
+ * @return array|false
+ */
+ public static function canonicalizeLoginData( $username, $password ) {
+ $sep = BotPassword::getSeparator();
+ if ( strpos( $username, $sep ) !== false ) {
+ // the separator is not valid in usernames so this must be a bot login
+ return [ $username, $password, false ];
+ } elseif ( strlen( $password ) > 32 && strpos( $password, $sep ) !== false ) {
+ // the strlen check helps minimize the password information obtainable from timing
+ $segments = explode( $sep, $password );
+ $password = array_pop( $segments );
+ $appId = implode( $sep, $segments );
+ if ( preg_match( '/^[0-9a-w]{32,}$/', $password ) ) {
+ return [ $username . $sep . $appId, $password, true ];
+ }
+ }
+ return false;
+ }
+
/**
* Try to log the user in
* @param string $username Combined user name and app ID
"botpasswords-updated-body": "The bot password for bot name \"$1\" of user \"$2\" was updated.",
"botpasswords-deleted-title": "Bot password deleted",
"botpasswords-deleted-body": "The bot password for bot name \"$1\" of user \"$2\" was deleted.",
- "botpasswords-newpassword": "The new password to log in with <strong>$1</strong> is <strong>$2</strong>. <em>Please record this for future reference.</em>",
+ "botpasswords-newpassword": "The new password to log in with <strong>$1</strong> is <strong>$2</strong>. <em>Please record this for future reference.</em> <br> (For old bots which require the login name to be the same as the eventual username, you can also use <strong>$3</strong> as username and <strong>$4</strong> as password.)",
"botpasswords-no-provider": "BotPasswordsSessionProvider is not available.",
"botpasswords-restriction-failed": "Bot password restrictions prevent this login.",
"botpasswords-invalid-name": "The username specified does not contain the bot password separator (\"$1\").",
"botpasswords-updated-body": "Success message when a bot password is updated. Parameters:\n* $1 - Bot name\n* $2 - User name",
"botpasswords-deleted-title": "Title of the success page when a bot password is deleted.",
"botpasswords-deleted-body": "Success message when a bot password is deleted. Parameters:\n* $1 - Bot name\n* $2 - User name",
- "botpasswords-newpassword": "Success message to display the new password when a bot password is created or updated. Parameters:\n* $1 - User name to be used for login.\n* $2 - Password to be used for login.",
+ "botpasswords-newpassword": "Success message to display the new password when a bot password is created or updated. Parameters:\n* $1 - User name to be used for login.\n* $2 - Password to be used for login.\n* $3, $4 - an alternative version of the user name and password, respectively, which is less preferred, but more compatible with old bots.",
"botpasswords-no-provider": "Error message when login is attempted but the BotPasswordsSessionProvider is not included in <code>$wgSessionProviders</code>.",
"botpasswords-restriction-failed": "Error message when login is rejected because the configured restrictions were not satisfied.",
"botpasswords-invalid-name": "Error message when a username lacking the separator character is passed to BotPassword. Parameters:\n* $1 - The separator character.",
$this->assertNotNull( BotPassword::newFromCentralId( 43, 'BotPassword' ) );
}
+ /**
+ * @dataProvider provideCanonicalizeLoginData
+ */
+ public function testCanonicalizeLoginData( $username, $password, $expectedResult ) {
+ $result = BotPassword::canonicalizeLoginData( $username, $password );
+ if ( is_array( $expectedResult ) ) {
+ $this->assertArrayEquals( $expectedResult, $result, true, true );
+ } else {
+ $this->assertSame( $expectedResult, $result );
+ }
+ }
+
+ public function provideCanonicalizeLoginData() {
+ return [
+ [ 'user', 'pass', false ],
+ [ 'user', 'abc@def', false ],
+ [ 'user@bot', '12345678901234567890123456789012',
+ [ 'user@bot', '12345678901234567890123456789012', false ] ],
+ [ 'user', 'bot@12345678901234567890123456789012',
+ [ 'user@bot', '12345678901234567890123456789012', true ] ],
+ [ 'user', 'bot@12345678901234567890123456789012345',
+ [ 'user@bot', '12345678901234567890123456789012345', true ] ],
+ [ 'user', 'bot@x@12345678901234567890123456789012',
+ [ 'user@bot@x', '12345678901234567890123456789012', true ] ],
+ ];
+ }
+
public function testLogin() {
// Test failure when bot passwords aren't enabled
$this->setMwGlobals( 'wgEnableBotPasswords', false );