1<?php
2
3namespace MediaWiki\Auth;
4
5use HashConfig;
6use MediaWiki\Tests\Unit\Auth\AuthenticationProviderTestTrait;
7use MediaWikiIntegrationTestCase;
8use stdClass;
9use TestLogger;
10use Wikimedia\TestingAccessWrapper;
11
12/**
13 * @group AuthManager
14 * @group Database
15 * @covers \MediaWiki\Auth\ThrottlePreAuthenticationProvider
16 */
17class ThrottlePreAuthenticationProviderTest extends MediaWikiIntegrationTestCase {
18	use AuthenticationProviderTestTrait;
19
20	public function testConstructor() {
21		$provider = new ThrottlePreAuthenticationProvider();
22		$providerPriv = TestingAccessWrapper::newFromObject( $provider );
23		$config = new HashConfig( [
24			'AccountCreationThrottle' => [ [
25				'count' => 123,
26				'seconds' => 86400,
27			] ],
28			'PasswordAttemptThrottle' => [ [
29				'count' => 5,
30				'seconds' => 300,
31			] ],
32		] );
33		$this->initProvider( $provider, $config );
34		$this->assertSame( [
35			'accountCreationThrottle' => [ [ 'count' => 123, 'seconds' => 86400 ] ],
36			'passwordAttemptThrottle' => [ [ 'count' => 5, 'seconds' => 300 ] ]
37		], $providerPriv->throttleSettings );
38		$accountCreationThrottle = TestingAccessWrapper::newFromObject(
39			$providerPriv->accountCreationThrottle );
40		$this->assertSame( [ [ 'count' => 123, 'seconds' => 86400 ] ],
41			$accountCreationThrottle->conditions );
42		$passwordAttemptThrottle = TestingAccessWrapper::newFromObject(
43			$providerPriv->passwordAttemptThrottle );
44		$this->assertSame( [ [ 'count' => 5, 'seconds' => 300 ] ],
45			$passwordAttemptThrottle->conditions );
46
47		$provider = new ThrottlePreAuthenticationProvider( [
48			'accountCreationThrottle' => [ [ 'count' => 43, 'seconds' => 10000 ] ],
49			'passwordAttemptThrottle' => [ [ 'count' => 11, 'seconds' => 100 ] ],
50		] );
51		$providerPriv = TestingAccessWrapper::newFromObject( $provider );
52		$config = new HashConfig( [
53			'AccountCreationThrottle' => [ [
54				'count' => 123,
55				'seconds' => 86400,
56			] ],
57			'PasswordAttemptThrottle' => [ [
58				'count' => 5,
59				'seconds' => 300,
60			] ],
61		] );
62		$this->initProvider( $provider, $config );
63		$this->assertSame( [
64			'accountCreationThrottle' => [ [ 'count' => 43, 'seconds' => 10000 ] ],
65			'passwordAttemptThrottle' => [ [ 'count' => 11, 'seconds' => 100 ] ],
66		], $providerPriv->throttleSettings );
67
68		$cache = new \HashBagOStuff();
69		$provider = new ThrottlePreAuthenticationProvider( [ 'cache' => $cache ] );
70		$providerPriv = TestingAccessWrapper::newFromObject( $provider );
71		$config = new HashConfig( [
72			'AccountCreationThrottle' => [ [ 'count' => 1, 'seconds' => 1 ] ],
73			'PasswordAttemptThrottle' => [ [ 'count' => 1, 'seconds' => 1 ] ],
74		] );
75		$this->initProvider( $provider, $config );
76		$accountCreationThrottle = TestingAccessWrapper::newFromObject(
77			$providerPriv->accountCreationThrottle );
78		$this->assertSame( $cache, $accountCreationThrottle->cache );
79		$passwordAttemptThrottle = TestingAccessWrapper::newFromObject(
80			$providerPriv->passwordAttemptThrottle );
81		$this->assertSame( $cache, $passwordAttemptThrottle->cache );
82	}
83
84	public function testDisabled() {
85		$provider = new ThrottlePreAuthenticationProvider( [
86			'accountCreationThrottle' => [],
87			'passwordAttemptThrottle' => [],
88			'cache' => new \HashBagOStuff(),
89		] );
90		$this->initProvider(
91			$provider,
92			new HashConfig( [
93				'AccountCreationThrottle' => null,
94				'PasswordAttemptThrottle' => null,
95			] ),
96			null,
97			$this->getServiceContainer()->getAuthManager()
98		);
99
100		$this->assertEquals(
101			\StatusValue::newGood(),
102			$provider->testForAccountCreation(
103				\User::newFromName( 'Created' ),
104				\User::newFromName( 'Creator' ),
105				[]
106			)
107		);
108		$this->assertEquals(
109			\StatusValue::newGood(),
110			$provider->testForAuthentication( [] )
111		);
112	}
113
114	/**
115	 * @dataProvider provideTestForAccountCreation
116	 * @param string $creatorname
117	 * @param bool $succeed
118	 * @param bool $hook
119	 */
120	public function testTestForAccountCreation( $creatorname, $succeed, $hook ) {
121		$provider = new ThrottlePreAuthenticationProvider( [
122			'accountCreationThrottle' => [ [ 'count' => 2, 'seconds' => 86400 ] ],
123			'cache' => new \HashBagOStuff(),
124		] );
125		$this->initProvider(
126			$provider,
127			new HashConfig( [
128				'AccountCreationThrottle' => null,
129				'PasswordAttemptThrottle' => null,
130			] ),
131			null,
132			$this->getServiceContainer()->getAuthManager(),
133			$this->getServiceContainer()->getHookContainer()
134		);
135
136		$user = \User::newFromName( 'RandomUser' );
137		$creator = \User::newFromName( $creatorname );
138		if ( $hook ) {
139			$mock = $this->getMockBuilder( stdClass::class )
140				->addMethods( [ 'onExemptFromAccountCreationThrottle' ] )
141				->getMock();
142			$mock->method( 'onExemptFromAccountCreationThrottle' )
143				->willReturn( false );
144			$this->mergeMwGlobalArrayValue( 'wgHooks', [
145				'ExemptFromAccountCreationThrottle' => [ $mock ],
146			] );
147		}
148
149		$this->assertTrue(
150
151			$provider->testForAccountCreation( $user, $creator, [] )->isOK(),
152			'attempt #1'
153		);
154		$this->assertTrue(
155
156			$provider->testForAccountCreation( $user, $creator, [] )->isOK(),
157			'attempt #2'
158		);
159		$this->assertEquals(
160			$succeed ? true : false,
161			$provider->testForAccountCreation( $user, $creator, [] )->isOK(),
162			'attempt #3'
163		);
164	}
165
166	public static function provideTestForAccountCreation() {
167		return [
168			'Normal user' => [ 'NormalUser', false, false ],
169			'Sysop' => [ 'UTSysop', true, false ],
170			'Normal user with hook' => [ 'NormalUser', true, true ],
171		];
172	}
173
174	public function testTestForAuthentication() {
175		$provider = new ThrottlePreAuthenticationProvider( [
176			'passwordAttemptThrottle' => [ [ 'count' => 2, 'seconds' => 86400 ] ],
177			'cache' => new \HashBagOStuff(),
178		] );
179		$this->initProvider(
180			$provider,
181			new HashConfig( [
182				'AccountCreationThrottle' => null,
183				'PasswordAttemptThrottle' => null,
184			] ),
185			null,
186			$this->getServiceContainer()->getAuthManager()
187		);
188
189		$req = new UsernameAuthenticationRequest;
190		$req->username = 'SomeUser';
191		for ( $i = 1; $i <= 3; $i++ ) {
192			$status = $provider->testForAuthentication( [ $req ] );
193			$this->assertEquals( $i < 3, $status->isGood(), "attempt #$i" );
194		}
195		$this->assertCount( 1, $status->getErrors() );
196		$msg = new \Message( $status->getErrors()[0]['message'], $status->getErrors()[0]['params'] );
197		$this->assertEquals( 'login-throttled', $msg->getKey() );
198
199		$provider->postAuthentication( \User::newFromName( 'SomeUser' ),
200			AuthenticationResponse::newFail( wfMessage( 'foo' ) ) );
201		$this->assertFalse( $provider->testForAuthentication( [ $req ] )->isGood(), 'after FAIL' );
202
203		$provider->postAuthentication( \User::newFromName( 'SomeUser' ),
204			AuthenticationResponse::newPass() );
205		$this->assertTrue( $provider->testForAuthentication( [ $req ] )->isGood(), 'after PASS' );
206
207		$req1 = new UsernameAuthenticationRequest;
208		$req1->username = 'foo';
209		$req2 = new UsernameAuthenticationRequest;
210		$req2->username = 'bar';
211		$this->assertTrue( $provider->testForAuthentication( [ $req1, $req2 ] )->isGood() );
212
213		$req = new UsernameAuthenticationRequest;
214		$req->username = 'Some user';
215		$provider->testForAuthentication( [ $req ] );
216		$req->username = 'Some_user';
217		$provider->testForAuthentication( [ $req ] );
218		$req->username = 'some user';
219		$status = $provider->testForAuthentication( [ $req ] );
220		$this->assertFalse( $status->isGood(), 'denormalized usernames are normalized' );
221	}
222
223	public function testPostAuthentication() {
224		$provider = new ThrottlePreAuthenticationProvider( [
225			'passwordAttemptThrottle' => [],
226			'cache' => new \HashBagOStuff(),
227		] );
228		$this->initProvider(
229			$provider,
230			new HashConfig( [
231				'AccountCreationThrottle' => null,
232				'PasswordAttemptThrottle' => null,
233			] ),
234			null,
235			$this->getServiceContainer()->getAuthManager()
236		);
237		$provider->postAuthentication( \User::newFromName( 'SomeUser' ),
238			AuthenticationResponse::newPass() );
239
240		$provider = new ThrottlePreAuthenticationProvider( [
241			'passwordAttemptThrottle' => [ [ 'count' => 2, 'seconds' => 86400 ] ],
242			'cache' => new \HashBagOStuff(),
243		] );
244		$logger = new TestLogger( true );
245		$this->initProvider(
246			$provider,
247			new HashConfig( [
248				'AccountCreationThrottle' => null,
249				'PasswordAttemptThrottle' => null,
250			] ),
251			$logger,
252			$this->getServiceContainer()->getAuthManager()
253		);
254		$provider->postAuthentication( \User::newFromName( 'SomeUser' ),
255			AuthenticationResponse::newPass() );
256		$this->assertSame( [
257			[ \Psr\Log\LogLevel::INFO, 'throttler data not found for {user}' ],
258		], $logger->getBuffer() );
259	}
260}
261