MediaWiki REL1_39
LocalPasswordPrimaryAuthenticationProvider.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Auth;
23
26use User;
28
36{
37
39 protected $loginOnly = false;
40
42 private $loadBalancer;
43
51 public function __construct( ILoadBalancer $loadBalancer, $params = [] ) {
52 parent::__construct( $params );
53 $this->loginOnly = !empty( $params['loginOnly'] );
54 $this->loadBalancer = $loadBalancer;
55 }
56
64 protected function getPasswordResetData( $username, $row ) {
65 $now = (int)wfTimestamp();
66 $expiration = wfTimestampOrNull( TS_UNIX, $row->user_password_expires );
67 if ( $expiration === null || (int)$expiration >= $now ) {
68 return null;
69 }
70
71 $grace = $this->config->get( MainConfigNames::PasswordExpireGrace );
72 if ( (int)$expiration + $grace < $now ) {
73 $data = [
74 'hard' => true,
75 'msg' => \Status::newFatal( 'resetpass-expired' )->getMessage(),
76 ];
77 } else {
78 $data = [
79 'hard' => false,
80 'msg' => \Status::newFatal( 'resetpass-expired-soft' )->getMessage(),
81 ];
82 }
83
84 return (object)$data;
85 }
86
87 public function beginPrimaryAuthentication( array $reqs ) {
88 $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
89 if ( !$req ) {
91 }
92
93 if ( $req->username === null || $req->password === null ) {
95 }
96
97 $username = $this->userNameUtils->getCanonical(
98 $req->username, UserRigorOptions::RIGOR_USABLE );
99 if ( $username === false ) {
101 }
102
103 $fields = [
104 'user_id', 'user_password', 'user_password_expires',
105 ];
106
107 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
108 $row = $dbr->selectRow(
109 'user',
110 $fields,
111 [ 'user_name' => $username ],
112 __METHOD__
113 );
114 if ( !$row ) {
115 // Do not reveal whether its bad username or
116 // bad password to prevent username enumeration
117 // on private wikis. (T134100)
118 return $this->failResponse( $req );
119 }
120
121 $oldRow = clone $row;
122 // Check for *really* old password hashes that don't even have a type
123 // The old hash format was just an md5 hex hash, with no type information
124 if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
125 $row->user_password = ":B:{$row->user_id}:{$row->user_password}";
126 }
127
128 $status = $this->checkPasswordValidity( $username, $req->password );
129 if ( !$status->isOK() ) {
130 // Fatal, can't log in
131 return AuthenticationResponse::newFail( $status->getMessage() );
132 }
133
134 $pwhash = $this->getPassword( $row->user_password );
135 if ( !$pwhash->verify( $req->password ) ) {
136 if ( $this->config->get( MainConfigNames::LegacyEncoding ) ) {
137 // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
138 // Check for this with iconv
139 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $req->password );
140 if ( $cp1252Password === $req->password || !$pwhash->verify( $cp1252Password ) ) {
141 return $this->failResponse( $req );
142 }
143 } else {
144 return $this->failResponse( $req );
145 }
146 }
147
148 // @codeCoverageIgnoreStart
149 if ( $this->getPasswordFactory()->needsUpdate( $pwhash ) ) {
150 $newHash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
151 $fname = __METHOD__;
152 \DeferredUpdates::addCallableUpdate( function () use ( $newHash, $oldRow, $fname ) {
153 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
154 $dbw->update(
155 'user',
156 [ 'user_password' => $newHash->toString() ],
157 [
158 'user_id' => $oldRow->user_id,
159 'user_password' => $oldRow->user_password
160 ],
161 $fname
162 );
163 } );
164 }
165 // @codeCoverageIgnoreEnd
166
167 $this->setPasswordResetFlag( $username, $status, $row );
168
169 return AuthenticationResponse::newPass( $username );
170 }
171
172 public function testUserCanAuthenticate( $username ) {
173 $username = $this->userNameUtils->getCanonical(
174 $username, UserRigorOptions::RIGOR_USABLE );
175 if ( $username === false ) {
176 return false;
177 }
178
179 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
180 $row = $dbr->selectRow(
181 'user',
182 [ 'user_password' ],
183 [ 'user_name' => $username ],
184 __METHOD__
185 );
186 if ( !$row ) {
187 return false;
188 }
189
190 // Check for *really* old password hashes that don't even have a type
191 // The old hash format was just an md5 hex hash, with no type information
192 if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
193 return true;
194 }
195
196 return !$this->getPassword( $row->user_password ) instanceof \InvalidPassword;
197 }
198
199 public function testUserExists( $username, $flags = User::READ_NORMAL ) {
200 $username = $this->userNameUtils->getCanonical(
201 $username, UserRigorOptions::RIGOR_USABLE );
202 if ( $username === false ) {
203 return false;
204 }
205
206 list( $db, $options ) = \DBAccessObjectUtils::getDBOptions( $flags );
207 return (bool)$this->loadBalancer->getConnectionRef( $db )->selectField(
208 [ 'user' ],
209 'user_id',
210 [ 'user_name' => $username ],
211 __METHOD__,
212 $options
213 );
214 }
215
217 AuthenticationRequest $req, $checkData = true
218 ) {
219 // We only want to blank the password if something else will accept the
220 // new authentication data, so return 'ignore' here.
221 if ( $this->loginOnly ) {
222 return \StatusValue::newGood( 'ignored' );
223 }
224
225 if ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
226 if ( !$checkData ) {
227 return \StatusValue::newGood();
228 }
229
230 $username = $this->userNameUtils->getCanonical( $req->username,
231 UserRigorOptions::RIGOR_USABLE );
232 if ( $username !== false ) {
233 $row = $this->loadBalancer->getConnectionRef( DB_PRIMARY )->selectRow(
234 'user',
235 [ 'user_id' ],
236 [ 'user_name' => $username ],
237 __METHOD__
238 );
239 if ( $row ) {
240 $sv = \StatusValue::newGood();
241 if ( $req->password !== null ) {
242 if ( $req->password !== $req->retype ) {
243 $sv->fatal( 'badretype' );
244 } else {
245 $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
246 }
247 }
248 return $sv;
249 }
250 }
251 }
252
253 return \StatusValue::newGood( 'ignored' );
254 }
255
257 $username = $req->username !== null ?
258 $this->userNameUtils->getCanonical( $req->username, UserRigorOptions::RIGOR_USABLE )
259 : false;
260 if ( $username === false ) {
261 return;
262 }
263
264 $pwhash = null;
265
266 if ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
267 if ( $this->loginOnly ) {
268 $pwhash = $this->getPasswordFactory()->newFromCiphertext( null );
269 $expiry = null;
270 } else {
271 $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
272 $expiry = $this->getNewPasswordExpiry( $username );
273 }
274 }
275
276 if ( $pwhash ) {
277 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
278 $dbw->update(
279 'user',
280 [
281 'user_password' => $pwhash->toString(),
282 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable expiry is set together with pwhash
283 'user_password_expires' => $dbw->timestampOrNull( $expiry ),
284 ],
285 [ 'user_name' => $username ],
286 __METHOD__
287 );
288 }
289 }
290
291 public function accountCreationType() {
292 return $this->loginOnly ? self::TYPE_NONE : self::TYPE_CREATE;
293 }
294
295 public function testForAccountCreation( $user, $creator, array $reqs ) {
296 $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
297
298 $ret = \StatusValue::newGood();
299 if ( !$this->loginOnly && $req && $req->username !== null && $req->password !== null ) {
300 if ( $req->password !== $req->retype ) {
301 $ret->fatal( 'badretype' );
302 } else {
303 $ret->merge(
304 $this->checkPasswordValidity( $user->getName(), $req->password )
305 );
306 }
307 }
308 return $ret;
309 }
310
311 public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
312 if ( $this->accountCreationType() === self::TYPE_NONE ) {
313 throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
314 }
315
316 $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
317 if ( $req && $req->username !== null && $req->password !== null ) {
318 // Nothing we can do besides claim it, because the user isn't in
319 // the DB yet
320 if ( $req->username !== $user->getName() ) {
321 $req = clone $req;
322 $req->username = $user->getName();
323 }
324 $ret = AuthenticationResponse::newPass( $req->username );
325 $ret->createRequest = $req;
326 return $ret;
327 }
329 }
330
331 public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
332 if ( $this->accountCreationType() === self::TYPE_NONE ) {
333 throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
334 }
335
336 // Now that the user is in the DB, set the password on it.
337 $this->providerChangeAuthenticationData( $res->createRequest );
338
339 return null;
340 }
341}
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Basic framework for a primary authentication provider that uses passwords.
failResponse(PasswordAuthenticationRequest $req)
Return the appropriate response for failure.
setPasswordResetFlag( $username, Status $status, $data=null)
Check if the password should be reset.
getNewPasswordExpiry( $username)
Get expiration date for a new password, if any.
This is a value object for authentication requests.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
This is a value object to hold authentication response data.
static newFail(Message $msg, array $failReasons=[])
A primary authentication provider that uses the password field in the 'user' table.
testUserExists( $username, $flags=User::READ_NORMAL)
Test whether the named user exists.
beginPrimaryAccountCreation( $user, $creator, array $reqs)
Start an account creation flow.
testUserCanAuthenticate( $username)
Test whether the named user can authenticate with this provider.Should return true if the provider ha...
providerAllowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
providerChangeAuthenticationData(AuthenticationRequest $req)
Change or remove authentication data (e.g.
finishAccountCreation( $user, $creator, AuthenticationResponse $res)
Post-creation callback.Called after the user is added to the database, before secondary authenticatio...
testForAccountCreation( $user, $creator, array $reqs)
Determine whether an account creation may begin.Called from AuthManager::beginAccountCreation()No nee...
getPasswordResetData( $username, $row)
Check if the password has expired and needs a reset.
A class containing constants representing the names of configuration variables.
const LegacyEncoding
Name constant for the LegacyEncoding setting, for use with Config::get()
const PasswordExpireGrace
Name constant for the PasswordExpireGrace setting, for use with Config::get()
internal since 1.36
Definition User.php:70
const TYPE_NONE
Provider cannot create or link to accounts.
Shared interface for rigor levels when dealing with User methods.
Create and track the database connections and transactions for a given database cluster.
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28