MediaWiki  master
LocalPasswordPrimaryAuthenticationProvider.php
Go to the documentation of this file.
1 <?php
22 namespace MediaWiki\Auth;
23 
26 use 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  [ $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.
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
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()
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
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