Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
69 / 69 |
|
100.00% |
5 / 5 |
CRAP | |
100.00% |
1 / 1 |
ThrottlePreAuthenticationProvider | |
100.00% |
69 / 69 |
|
100.00% |
5 / 5 |
24 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
postInitSetup | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
4 | |||
testForAccountCreation | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
testForAuthentication | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
9 | |||
postAuthentication | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | * @ingroup Auth |
20 | */ |
21 | |
22 | namespace MediaWiki\Auth; |
23 | |
24 | use BagOStuff; |
25 | use MediaWiki\MainConfigNames; |
26 | use MediaWiki\User\User; |
27 | |
28 | /** |
29 | * A pre-authentication provider to throttle authentication actions. |
30 | * |
31 | * Adding this provider will throttle account creations and primary authentication attempts |
32 | * (more specifically, any authentication that returns FAIL on failure). Secondary authentication |
33 | * cannot be easily throttled on a framework level (since it would typically return UI on failure); |
34 | * secondary providers are expected to do their own throttling. |
35 | * @ingroup Auth |
36 | * @since 1.27 |
37 | */ |
38 | class ThrottlePreAuthenticationProvider extends AbstractPreAuthenticationProvider { |
39 | /** @var array */ |
40 | protected $throttleSettings; |
41 | |
42 | /** @var Throttler */ |
43 | protected $accountCreationThrottle; |
44 | |
45 | /** @var Throttler */ |
46 | protected $passwordAttemptThrottle; |
47 | |
48 | /** @var BagOStuff */ |
49 | protected $cache; |
50 | |
51 | /** |
52 | * @param array $params |
53 | * - accountCreationThrottle: (array) Condition array for the account creation throttle; an array |
54 | * of arrays in a format like $wgPasswordAttemptThrottle, passed to the Throttler constructor. |
55 | * - passwordAttemptThrottle: (array) Condition array for the password attempt throttle, in the |
56 | * same format as accountCreationThrottle. |
57 | * - cache: (BagOStuff) Where to store the throttle, defaults to the local cluster instance. |
58 | */ |
59 | public function __construct( $params = [] ) { |
60 | $this->throttleSettings = array_intersect_key( $params, |
61 | [ 'accountCreationThrottle' => true, 'passwordAttemptThrottle' => true ] ); |
62 | $this->cache = $params['cache'] ?? \ObjectCache::getLocalClusterInstance(); |
63 | } |
64 | |
65 | protected function postInitSetup() { |
66 | $accountCreationThrottle = $this->config->get( MainConfigNames::AccountCreationThrottle ); |
67 | // Handle old $wgAccountCreationThrottle format (number of attempts per 24 hours) |
68 | if ( !is_array( $accountCreationThrottle ) ) { |
69 | $accountCreationThrottle = [ [ |
70 | 'count' => $accountCreationThrottle, |
71 | 'seconds' => 86400, |
72 | ] ]; |
73 | } |
74 | |
75 | // @codeCoverageIgnoreStart |
76 | $this->throttleSettings += [ |
77 | // @codeCoverageIgnoreEnd |
78 | 'accountCreationThrottle' => $accountCreationThrottle, |
79 | 'passwordAttemptThrottle' => |
80 | $this->config->get( MainConfigNames::PasswordAttemptThrottle ), |
81 | ]; |
82 | |
83 | if ( !empty( $this->throttleSettings['accountCreationThrottle'] ) ) { |
84 | $this->accountCreationThrottle = new Throttler( |
85 | $this->throttleSettings['accountCreationThrottle'], [ |
86 | 'type' => 'acctcreate', |
87 | 'cache' => $this->cache, |
88 | ] |
89 | ); |
90 | } |
91 | if ( !empty( $this->throttleSettings['passwordAttemptThrottle'] ) ) { |
92 | $this->passwordAttemptThrottle = new Throttler( |
93 | $this->throttleSettings['passwordAttemptThrottle'], [ |
94 | 'type' => 'password', |
95 | 'cache' => $this->cache, |
96 | ] |
97 | ); |
98 | } |
99 | } |
100 | |
101 | public function testForAccountCreation( $user, $creator, array $reqs ) { |
102 | if ( !$this->accountCreationThrottle || !$creator->isPingLimitable() ) { |
103 | return \StatusValue::newGood(); |
104 | } |
105 | |
106 | $ip = $this->manager->getRequest()->getIP(); |
107 | |
108 | if ( !$this->getHookRunner()->onExemptFromAccountCreationThrottle( $ip ) ) { |
109 | $this->logger->debug( __METHOD__ . ": a hook allowed account creation w/o throttle" ); |
110 | return \StatusValue::newGood(); |
111 | } |
112 | |
113 | $result = $this->accountCreationThrottle->increase( null, $ip, __METHOD__ ); |
114 | if ( $result ) { |
115 | $message = wfMessage( 'acct_creation_throttle_hit' )->params( $result['count'] ) |
116 | ->durationParams( $result['wait'] ); |
117 | return \StatusValue::newFatal( $message ); |
118 | } |
119 | |
120 | return \StatusValue::newGood(); |
121 | } |
122 | |
123 | public function testForAuthentication( array $reqs ) { |
124 | if ( !$this->passwordAttemptThrottle ) { |
125 | return \StatusValue::newGood(); |
126 | } |
127 | |
128 | $ip = $this->manager->getRequest()->getIP(); |
129 | try { |
130 | $username = AuthenticationRequest::getUsernameFromRequests( $reqs ); |
131 | } catch ( \UnexpectedValueException $e ) { |
132 | $username = null; |
133 | } |
134 | |
135 | // Get everything this username could normalize to, and throttle each one individually. |
136 | // If nothing uses usernames, just throttle by IP. |
137 | if ( $username !== null ) { |
138 | $usernames = $this->manager->normalizeUsername( $username ); |
139 | } else { |
140 | $usernames = [ null ]; |
141 | } |
142 | $result = false; |
143 | foreach ( $usernames as $name ) { |
144 | $r = $this->passwordAttemptThrottle->increase( $name, $ip, __METHOD__ ); |
145 | if ( $r && ( !$result || $result['wait'] < $r['wait'] ) ) { |
146 | $result = $r; |
147 | } |
148 | } |
149 | |
150 | if ( $result ) { |
151 | $message = wfMessage( 'login-throttled' )->durationParams( $result['wait'] ); |
152 | return \StatusValue::newFatal( $message ); |
153 | } else { |
154 | $this->manager->setAuthenticationSessionData( 'LoginThrottle', |
155 | [ 'users' => $usernames, 'ip' => $ip ] ); |
156 | return \StatusValue::newGood(); |
157 | } |
158 | } |
159 | |
160 | /** |
161 | * @param null|User $user |
162 | * @param AuthenticationResponse $response |
163 | */ |
164 | public function postAuthentication( $user, AuthenticationResponse $response ) { |
165 | if ( $response->status !== AuthenticationResponse::PASS ) { |
166 | return; |
167 | } elseif ( !$this->passwordAttemptThrottle ) { |
168 | return; |
169 | } |
170 | |
171 | $data = $this->manager->getAuthenticationSessionData( 'LoginThrottle' ); |
172 | if ( !$data ) { |
173 | // this can occur when login is happening via AuthenticationRequest::$loginRequest |
174 | // so testForAuthentication is skipped |
175 | $this->logger->info( 'throttler data not found for {user}', [ 'user' => $user->getName() ] ); |
176 | return; |
177 | } |
178 | |
179 | foreach ( $data['users'] as $name ) { |
180 | $this->passwordAttemptThrottle->clear( $name, $data['ip'] ); |
181 | } |
182 | } |
183 | } |