MediaWiki  master
LocalPasswordPrimaryAuthenticationProvider.php
Go to the documentation of this file.
1 <?php
22 namespace MediaWiki\Auth;
23 
24 use User;
25 
33 {
34 
36  protected $loginOnly = false;
37 
44  public function __construct( $params = [] ) {
45  parent::__construct( $params );
46  $this->loginOnly = !empty( $params['loginOnly'] );
47  }
48 
56  protected function getPasswordResetData( $username, $row ) {
57  $now = wfTimestamp();
58  $expiration = wfTimestampOrNull( TS_UNIX, $row->user_password_expires );
59  if ( $expiration === null || $expiration >= $now ) {
60  return null;
61  }
62 
63  $grace = $this->config->get( 'PasswordExpireGrace' );
64  if ( $expiration + $grace < $now ) {
65  $data = [
66  'hard' => true,
67  'msg' => \Status::newFatal( 'resetpass-expired' )->getMessage(),
68  ];
69  } else {
70  $data = [
71  'hard' => false,
72  'msg' => \Status::newFatal( 'resetpass-expired-soft' )->getMessage(),
73  ];
74  }
75 
76  return (object)$data;
77  }
78 
79  public function beginPrimaryAuthentication( array $reqs ) {
80  $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
81  if ( !$req ) {
83  }
84 
85  if ( $req->username === null || $req->password === null ) {
87  }
88 
89  $username = User::getCanonicalName( $req->username, 'usable' );
90  if ( $username === false ) {
92  }
93 
94  $fields = [
95  'user_id', 'user_password', 'user_password_expires',
96  ];
97 
98  $dbr = wfGetDB( DB_REPLICA );
99  $row = $dbr->selectRow(
100  'user',
101  $fields,
102  [ 'user_name' => $username ],
103  __METHOD__
104  );
105  if ( !$row ) {
106  // Do not reveal whether its bad username or
107  // bad password to prevent username enumeration
108  // on private wikis. (T134100)
109  return $this->failResponse( $req );
110  }
111 
112  $oldRow = clone $row;
113  // Check for *really* old password hashes that don't even have a type
114  // The old hash format was just an md5 hex hash, with no type information
115  if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
116  $row->user_password = ":B:{$row->user_id}:{$row->user_password}";
117  }
118 
119  $status = $this->checkPasswordValidity( $username, $req->password );
120  if ( !$status->isOK() ) {
121  // Fatal, can't log in
122  return AuthenticationResponse::newFail( $status->getMessage() );
123  }
124 
125  $pwhash = $this->getPassword( $row->user_password );
126  if ( !$pwhash->verify( $req->password ) ) {
127  if ( $this->config->get( 'LegacyEncoding' ) ) {
128  // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
129  // Check for this with iconv
130  $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $req->password );
131  if ( $cp1252Password === $req->password || !$pwhash->verify( $cp1252Password ) ) {
132  return $this->failResponse( $req );
133  }
134  } else {
135  return $this->failResponse( $req );
136  }
137  }
138 
139  // @codeCoverageIgnoreStart
140  if ( $this->getPasswordFactory()->needsUpdate( $pwhash ) ) {
141  $newHash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
142  $fname = __METHOD__;
143  \DeferredUpdates::addCallableUpdate( function () use ( $newHash, $oldRow, $fname ) {
144  $dbw = wfGetDB( DB_MASTER );
145  $dbw->update(
146  'user',
147  [ 'user_password' => $newHash->toString() ],
148  [
149  'user_id' => $oldRow->user_id,
150  'user_password' => $oldRow->user_password
151  ],
152  $fname
153  );
154  } );
155  }
156  // @codeCoverageIgnoreEnd
157 
158  $this->setPasswordResetFlag( $username, $status, $row );
159 
160  return AuthenticationResponse::newPass( $username );
161  }
162 
163  public function testUserCanAuthenticate( $username ) {
164  $username = User::getCanonicalName( $username, 'usable' );
165  if ( $username === false ) {
166  return false;
167  }
168 
169  $dbr = wfGetDB( DB_REPLICA );
170  $row = $dbr->selectRow(
171  'user',
172  [ 'user_password' ],
173  [ 'user_name' => $username ],
174  __METHOD__
175  );
176  if ( !$row ) {
177  return false;
178  }
179 
180  // Check for *really* old password hashes that don't even have a type
181  // The old hash format was just an md5 hex hash, with no type information
182  if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
183  return true;
184  }
185 
186  return !$this->getPassword( $row->user_password ) instanceof \InvalidPassword;
187  }
188 
189  public function testUserExists( $username, $flags = User::READ_NORMAL ) {
190  $username = User::getCanonicalName( $username, 'usable' );
191  if ( $username === false ) {
192  return false;
193  }
194 
195  list( $db, $options ) = \DBAccessObjectUtils::getDBOptions( $flags );
196  return (bool)wfGetDB( $db )->selectField(
197  [ 'user' ],
198  'user_id',
199  [ 'user_name' => $username ],
200  __METHOD__,
201  $options
202  );
203  }
204 
206  AuthenticationRequest $req, $checkData = true
207  ) {
208  // We only want to blank the password if something else will accept the
209  // new authentication data, so return 'ignore' here.
210  if ( $this->loginOnly ) {
211  return \StatusValue::newGood( 'ignored' );
212  }
213 
214  if ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
215  if ( !$checkData ) {
216  return \StatusValue::newGood();
217  }
218 
219  $username = User::getCanonicalName( $req->username, 'usable' );
220  if ( $username !== false ) {
221  $row = wfGetDB( DB_MASTER )->selectRow(
222  'user',
223  [ 'user_id' ],
224  [ 'user_name' => $username ],
225  __METHOD__
226  );
227  if ( $row ) {
228  $sv = \StatusValue::newGood();
229  if ( $req->password !== null ) {
230  if ( $req->password !== $req->retype ) {
231  $sv->fatal( 'badretype' );
232  } else {
233  $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
234  }
235  }
236  return $sv;
237  }
238  }
239  }
240 
241  return \StatusValue::newGood( 'ignored' );
242  }
243 
245  $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false;
246  if ( $username === false ) {
247  return;
248  }
249 
250  $pwhash = null;
251 
252  if ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
253  if ( $this->loginOnly ) {
254  $pwhash = $this->getPasswordFactory()->newFromCiphertext( null );
255  $expiry = null;
256  } else {
257  $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
258  $expiry = $this->getNewPasswordExpiry( $username );
259  }
260  }
261 
262  if ( $pwhash ) {
263  $dbw = wfGetDB( DB_MASTER );
264  $dbw->update(
265  'user',
266  [
267  'user_password' => $pwhash->toString(),
268  'user_password_expires' => $dbw->timestampOrNull( $expiry ),
269  ],
270  [ 'user_name' => $username ],
271  __METHOD__
272  );
273  }
274  }
275 
276  public function accountCreationType() {
277  return $this->loginOnly ? self::TYPE_NONE : self::TYPE_CREATE;
278  }
279 
280  public function testForAccountCreation( $user, $creator, array $reqs ) {
281  $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
282 
283  $ret = \StatusValue::newGood();
284  if ( !$this->loginOnly && $req && $req->username !== null && $req->password !== null ) {
285  if ( $req->password !== $req->retype ) {
286  $ret->fatal( 'badretype' );
287  } else {
288  $ret->merge(
289  $this->checkPasswordValidity( $user->getName(), $req->password )
290  );
291  }
292  }
293  return $ret;
294  }
295 
296  public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
297  if ( $this->accountCreationType() === self::TYPE_NONE ) {
298  throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
299  }
300 
301  $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
302  if ( $req && $req->username !== null && $req->password !== null ) {
303  // Nothing we can do besides claim it, because the user isn't in
304  // the DB yet
305  if ( $req->username !== $user->getName() ) {
306  $req = clone $req;
307  $req->username = $user->getName();
308  }
309  $ret = AuthenticationResponse::newPass( $req->username );
310  $ret->createRequest = $req;
311  return $ret;
312  }
314  }
315 
316  public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
317  if ( $this->accountCreationType() === self::TYPE_NONE ) {
318  throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
319  }
320 
321  // Now that the user is in the DB, set the password on it.
322  $this->providerChangeAuthenticationData( $res->createRequest );
323 
324  return null;
325  }
326 }
beginPrimaryAccountCreation( $user, $creator, array $reqs)
Start an account creation flow.
getPasswordResetData( $username, $row)
Check if the password has expired and needs a reset.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
checkPasswordValidity( $username, $password)
Check that the password is valid.
testUserExists( $username, $flags=User::READ_NORMAL)
Test whether the named user exists.
providerAllowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
testForAccountCreation( $user, $creator, array $reqs)
Determine whether an account creation may begin.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
A primary authentication provider that uses the password field in the &#39;user&#39; table.
finishAccountCreation( $user, $creator, AuthenticationResponse $res)
Post-creation callback.
getNewPasswordExpiry( $username)
Get expiration date for a new password, if any.
This is a value object to hold authentication response data.
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
const DB_MASTER
Definition: defines.php:26
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
setPasswordResetFlag( $username, Status $status, $data=null)
Check if the password should be reset.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
static getCanonicalName( $name, $validate='valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid...
Definition: User.php:1139
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
testUserCanAuthenticate( $username)
Test whether the named user can authenticate with this provider.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
Basic framework for a primary authentication provider that uses passwords.
providerChangeAuthenticationData(AuthenticationRequest $req)
Change or remove authentication data (e.g.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
const DB_REPLICA
Definition: defines.php:25
failResponse(PasswordAuthenticationRequest $req)
Return the appropriate response for failure.
This is a value object for authentication requests.