Page MenuHomeWickedGov Phorge

CaptchaPreAuthenticationProvider.php
No OneTemporary

Size
8 KB
Referenced Files
None
Subscribers
None

CaptchaPreAuthenticationProvider.php

<?php
namespace MediaWiki\Extension\ConfirmEdit\Auth;
use MediaWiki\Auth\AbstractPreAuthenticationProvider;
use MediaWiki\Auth\AuthenticationRequest;
use MediaWiki\Auth\AuthenticationResponse;
use MediaWiki\Auth\AuthManager;
use MediaWiki\Extension\ConfirmEdit\CaptchaTriggers;
use MediaWiki\Extension\ConfirmEdit\Hooks;
use MediaWiki\Extension\ConfirmEdit\SimpleCaptcha\SimpleCaptcha;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Status\Status;
use MediaWiki\User\User;
class CaptchaPreAuthenticationProvider extends AbstractPreAuthenticationProvider {
/**
* @inheritDoc
*/
public function getAuthenticationRequests( $action, array $options ) {
$captcha = Hooks::getInstance();
$user = User::newFromName( $options['username'] );
$needed = false;
switch ( $action ) {
case AuthManager::ACTION_CREATE:
$u = $user ?: new User();
$needed = $captcha->needCreateAccountCaptcha( $u );
if ( $needed ) {
$captcha->setAction( 'accountcreate' );
// This is debug level simply because generally
// captchas are either always or never triggered on
// view of create account, so it gets pretty noisy
LoggerFactory::getInstance( 'captcha' )
->debug( 'Captcha shown on account creation for {user}', [
'event' => 'captcha.display',
'eventType' => 'accountcreation',
'user' => $u->getName()
] );
}
break;
case AuthManager::ACTION_LOGIN:
// Captcha is shown on login when there were too many failed attempts from the current IP
// or using a given username, or if a hook handler says that a CAPTCHA should be shown.
// The varying on username is a bit awkward because we don't know the
// username yet. The username from the last successful login is stored in a cookie,
// but we still must make sure to not lock out other usernames, so after the first
// failed login attempt using a username that needs a captcha, set a session flag
// to display a captcha on login from that point on. This will result in confusing
// error messages if the browser cannot persist the session (because we'll never show
// a required captcha field), but then login would be impossible anyway so no big deal.
// If the username ends to be one that does not trigger the captcha, that will
// result in weird behavior (if the user leaves the captcha field empty, they get
// a required field error; if they fill it with an invalid answer, it will pass)
// - again, not a huge deal.
$session = $this->manager->getRequest()->getSession();
$suggestedUsername = $session->suggestLoginUsername();
if ( $captcha->triggersCaptcha( CaptchaTriggers::LOGIN_ATTEMPT ) ) {
$captcha->setAction( 'loginattempt' );
LoggerFactory::getInstance( 'captcha' )
->info( 'Captcha shown on login attempt by {clientip} for {suggestedUser}', [
'event' => 'captcha.display',
'eventType' => 'loginattempt',
'suggestedUser' => $suggestedUsername,
'clientip' => $this->manager->getRequest()->getIP(),
'ua' => $this->manager->getRequest()->getHeader( 'User-Agent' )
] );
$needed = true;
break;
}
$loginCounter = $this->getLoginAttemptCounter( $captcha );
$userProbablyNeedsCaptcha = $session->get( 'ConfirmEdit:loginCaptchaPerUserTriggered' );
if (
$loginCounter->isBadLoginTriggered()
|| $userProbablyNeedsCaptcha
|| ( $suggestedUsername && $loginCounter->isBadLoginPerUserTriggered( $suggestedUsername ) )
) {
$needed = true;
$captcha->setAction( 'badlogin' );
LoggerFactory::getInstance( 'captcha' )
->info( 'Captcha shown on login by {clientip} for {suggestedUser}', [
'event' => 'captcha.display',
'eventType' => 'badlogin',
'suggestedUser' => $suggestedUsername,
'clientip' => $this->manager->getRequest()->getIP(),
'ua' => $this->manager->getRequest()->getHeader( 'User-Agent' )
] );
break;
}
break;
}
if ( $needed ) {
return [ $captcha->createAuthenticationRequest() ];
} else {
return [];
}
}
/**
* @inheritDoc
*/
public function testForAuthentication( array $reqs ) {
$captcha = Hooks::getInstance();
$username = AuthenticationRequest::getUsernameFromRequests( $reqs );
$loginCounter = $this->getLoginAttemptCounter( $captcha );
$success = true;
$isBadLoginPerUserTriggered = $username ?
$loginCounter->isBadLoginPerUserTriggered( $username ) : false;
$loginTriggersCaptcha = $captcha->triggersCaptcha( CaptchaTriggers::LOGIN_ATTEMPT );
if (
$isBadLoginPerUserTriggered ||
$loginCounter->isBadLoginTriggered() ||
$loginTriggersCaptcha
) {
if ( $loginTriggersCaptcha ) {
$captcha->setAction( 'loginattempt' );
$captcha->setTrigger( "loginattempt login '$username'" );
} else {
$captcha->setAction( 'badlogin' );
$captcha->setTrigger( "post-badlogin login '$username'" );
}
$success = $this->verifyCaptcha( $captcha, $reqs, new User() );
$ip = $this->manager->getRequest()->getIP();
$action = $loginTriggersCaptcha ? 'login page' : 'bad login';
LoggerFactory::getInstance( 'captcha' )->info( "Captcha shown on $action for {user}", [
'event' => 'captcha.submit',
'eventType' => $loginTriggersCaptcha ? 'loginattempt' : 'badlogin',
'successful' => $success,
'user' => $username ?? 'unknown',
'clientip' => $ip,
'ua' => $this->manager->getRequest()->getHeader( 'User-Agent' )
] );
}
if ( $isBadLoginPerUserTriggered ) {
$session = $this->manager->getRequest()->getSession();
// A captcha is needed to log in with this username, so display it on the next attempt.
$session->set( 'ConfirmEdit:loginCaptchaPerUserTriggered', true );
}
// Make brute force attacks harder by not telling whether the password or the
// captcha failed.
return $success ? Status::newGood() : $this->makeError( 'wrongpassword', $captcha );
}
/**
* @inheritDoc
*/
public function testForAccountCreation( $user, $creator, array $reqs ) {
$captcha = Hooks::getInstance();
if ( $captcha->needCreateAccountCaptcha( $creator ) ) {
$username = $user->getName();
$captcha->setAction( 'accountcreate' );
$captcha->setTrigger( "new account '$username'" );
$success = $this->verifyCaptcha( $captcha, $reqs, $user );
$ip = $this->manager->getRequest()->getIP();
LoggerFactory::getInstance( 'captcha' )->info(
'Captcha submitted on account creation for {user}', [
'event' => 'captcha.submit',
'eventType' => 'accountcreation',
'successful' => $success,
'user' => $username,
'clientip' => $ip,
'ua' => $this->manager->getRequest()->getHeader( 'User-Agent' )
]
);
if ( !$success ) {
return $this->makeError( 'captcha-createaccount-fail', $captcha );
}
}
return Status::newGood();
}
/**
* @inheritDoc
*/
public function postAuthentication( $user, AuthenticationResponse $response ) {
$captcha = Hooks::getInstance();
$loginCounter = $this->getLoginAttemptCounter( $captcha );
switch ( $response->status ) {
case AuthenticationResponse::PASS:
case AuthenticationResponse::RESTART:
$session = $this->manager->getRequest()->getSession();
$session->remove( 'ConfirmEdit:loginCaptchaPerUserTriggered' );
$loginCounter->resetBadLoginCounter( $user ? $user->getName() : null );
break;
case AuthenticationResponse::FAIL:
$loginCounter->increaseBadLoginCounter( $user ? $user->getName() : null );
break;
}
}
/**
* Verify submitted captcha.
* Assumes that the user has to pass the capctha (permission checks are caller's responsibility).
* @param SimpleCaptcha $captcha
* @param AuthenticationRequest[] $reqs
* @param User $user
* @return bool
*/
protected function verifyCaptcha( SimpleCaptcha $captcha, array $reqs, User $user ) {
/** @var CaptchaAuthenticationRequest $req */
$req = AuthenticationRequest::getRequestByClass( $reqs,
CaptchaAuthenticationRequest::class, true );
if ( !$req ) {
return false;
}
return $captcha->passCaptchaLimited( $req->captchaId, $req->captchaWord, $user );
}
/**
* @param string $message Message key
* @param SimpleCaptcha $captcha
* @return Status
*/
protected function makeError( $message, SimpleCaptcha $captcha ) {
$error = $captcha->getError();
if ( $error ) {
return Status::newFatal( wfMessage( 'captcha-error', $error ) );
}
return Status::newFatal( $message );
}
protected function getLoginAttemptCounter( SimpleCaptcha $captcha ): LoginAttemptCounter {
// Overridable for testing
return new LoginAttemptCounter( $captcha );
}
}

File Metadata

Mime Type
text/x-php
Expires
Aug 16 2026, 11:48 (12 w, 5 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
f0/8b/6cd21cca97c6838e19f4059097a3
Default Alt Text
CaptchaPreAuthenticationProvider.php (8 KB)

Event Timeline