MediaWiki  master
LocalPasswordPrimaryAuthenticationProvider.php
Go to the documentation of this file.
1 <?php
22 namespace MediaWiki\Auth;
23 
28 
36 {
37 
39  protected $loginOnly = false;
40 
42  private $dbProvider;
43 
51  public function __construct( IConnectionProvider $dbProvider, $params = [] ) {
52  parent::__construct( $params );
53  $this->loginOnly = !empty( $params['loginOnly'] );
54  $this->dbProvider = $dbProvider;
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' => \MediaWiki\Status\Status::newFatal( 'resetpass-expired' )->getMessage(),
76  ];
77  } else {
78  $data = [
79  'hard' => false,
80  'msg' => \MediaWiki\Status\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 || $req->username === null || $req->password === null ) {
91  }
92 
93  $username = $this->userNameUtils->getCanonical(
94  $req->username, UserRigorOptions::RIGOR_USABLE );
95  if ( $username === false ) {
97  }
98 
99  $row = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
100  ->select( [ 'user_id', 'user_password', 'user_password_expires' ] )
101  ->from( 'user' )
102  ->where( [ 'user_name' => $username ] )
103  ->caller( __METHOD__ )->fetchRow();
104  if ( !$row ) {
105  // Do not reveal whether its bad username or
106  // bad password to prevent username enumeration
107  // on private wikis. (T134100)
108  return $this->failResponse( $req );
109  }
110 
111  $oldRow = clone $row;
112  // Check for *really* old password hashes that don't even have a type
113  // The old hash format was just an md5 hex hash, with no type information
114  if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
115  $row->user_password = ":B:{$row->user_id}:{$row->user_password}";
116  }
117 
118  $status = $this->checkPasswordValidity( $username, $req->password );
119  if ( !$status->isOK() ) {
120  return $this->getFatalPasswordErrorResponse( $username, $status );
121  }
122 
123  $pwhash = $this->getPassword( $row->user_password );
124  if ( !$pwhash->verify( $req->password ) ) {
125  if ( $this->config->get( MainConfigNames::LegacyEncoding ) ) {
126  // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
127  // Check for this with iconv
128  $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $req->password );
129  if ( $cp1252Password === $req->password || !$pwhash->verify( $cp1252Password ) ) {
130  return $this->failResponse( $req );
131  }
132  } else {
133  return $this->failResponse( $req );
134  }
135  }
136 
137  // @codeCoverageIgnoreStart
138  if ( $this->getPasswordFactory()->needsUpdate( $pwhash ) ) {
139  $newHash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
140  $fname = __METHOD__;
141  \DeferredUpdates::addCallableUpdate( function () use ( $newHash, $oldRow, $fname ) {
142  $dbw = $this->dbProvider->getPrimaryDatabase();
143  $dbw->newUpdateQueryBuilder()
144  ->update( 'user' )
145  ->set( [ 'user_password' => $newHash->toString() ] )
146  ->where( [
147  'user_id' => $oldRow->user_id,
148  'user_password' => $oldRow->user_password,
149  ] )
150  ->caller( $fname )->execute();
151  } );
152  }
153  // @codeCoverageIgnoreEnd
154 
155  $this->setPasswordResetFlag( $username, $status, $row );
156 
157  return AuthenticationResponse::newPass( $username );
158  }
159 
160  public function testUserCanAuthenticate( $username ) {
161  $username = $this->userNameUtils->getCanonical(
162  $username, UserRigorOptions::RIGOR_USABLE );
163  if ( $username === false ) {
164  return false;
165  }
166 
167  $row = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
168  ->select( [ 'user_password' ] )
169  ->from( 'user' )
170  ->where( [ 'user_name' => $username ] )
171  ->caller( __METHOD__ )->fetchRow();
172  if ( !$row ) {
173  return false;
174  }
175 
176  // Check for *really* old password hashes that don't even have a type
177  // The old hash format was just an md5 hex hash, with no type information
178  if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
179  return true;
180  }
181 
182  return !$this->getPassword( $row->user_password ) instanceof \InvalidPassword;
183  }
184 
185  public function testUserExists( $username, $flags = User::READ_NORMAL ) {
186  $username = $this->userNameUtils->getCanonical(
187  $username, UserRigorOptions::RIGOR_USABLE );
188  if ( $username === false ) {
189  return false;
190  }
191 
192  [ $mode, $options ] = \DBAccessObjectUtils::getDBOptions( $flags );
193  $db = \DBAccessObjectUtils::getDBFromIndex( $this->dbProvider, $mode );
194  return (bool)$db->newSelectQueryBuilder()
195  ->select( [ 'user_id' ] )
196  ->from( 'user' )
197  ->where( [ 'user_name' => $username ] )
198  ->options( $options )
199  ->caller( __METHOD__ )->fetchField();
200  }
201 
203  AuthenticationRequest $req, $checkData = true
204  ) {
205  // We only want to blank the password if something else will accept the
206  // new authentication data, so return 'ignore' here.
207  if ( $this->loginOnly ) {
208  return \StatusValue::newGood( 'ignored' );
209  }
210 
211  if ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
212  if ( !$checkData ) {
213  return \StatusValue::newGood();
214  }
215 
216  $username = $this->userNameUtils->getCanonical( $req->username,
217  UserRigorOptions::RIGOR_USABLE );
218  if ( $username !== false ) {
219  $row = $this->dbProvider->getPrimaryDatabase()->newSelectQueryBuilder()
220  ->select( [ 'user_id' ] )
221  ->from( 'user' )
222  ->where( [ 'user_name' => $username ] )
223  ->caller( __METHOD__ )->fetchRow();
224  if ( $row ) {
225  $sv = \StatusValue::newGood();
226  if ( $req->password !== null ) {
227  if ( $req->password !== $req->retype ) {
228  $sv->fatal( 'badretype' );
229  } else {
230  $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
231  }
232  }
233  return $sv;
234  }
235  }
236  }
237 
238  return \StatusValue::newGood( 'ignored' );
239  }
240 
242  $username = $req->username !== null ?
243  $this->userNameUtils->getCanonical( $req->username, UserRigorOptions::RIGOR_USABLE )
244  : false;
245  if ( $username === false ) {
246  return;
247  }
248 
249  $pwhash = null;
250 
251  if ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
252  if ( $this->loginOnly ) {
253  $pwhash = $this->getPasswordFactory()->newFromCiphertext( null );
254  $expiry = null;
255  } else {
256  $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
257  $expiry = $this->getNewPasswordExpiry( $username );
258  }
259  }
260 
261  if ( $pwhash ) {
262  $dbw = $this->dbProvider->getPrimaryDatabase();
263  $dbw->newUpdateQueryBuilder()
264  ->update( 'user' )
265  ->set( [
266  'user_password' => $pwhash->toString(),
267  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable expiry is set together with pwhash
268  'user_password_expires' => $dbw->timestampOrNull( $expiry ),
269  ] )
270  ->where( [ 'user_name' => $username ] )
271  ->caller( __METHOD__ )->execute();
272  }
273  }
274 
275  public function accountCreationType() {
276  return $this->loginOnly ? self::TYPE_NONE : self::TYPE_CREATE;
277  }
278 
279  public function testForAccountCreation( $user, $creator, array $reqs ) {
280  $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
281 
282  $ret = \StatusValue::newGood();
283  if ( !$this->loginOnly && $req && $req->username !== null && $req->password !== null ) {
284  if ( $req->password !== $req->retype ) {
285  $ret->fatal( 'badretype' );
286  } else {
287  $ret->merge(
288  $this->checkPasswordValidity( $user->getName(), $req->password )
289  );
290  }
291  }
292  return $ret;
293  }
294 
295  public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
296  if ( $this->accountCreationType() === self::TYPE_NONE ) {
297  throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
298  }
299 
300  $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
301  if ( $req && $req->username !== null && $req->password !== null ) {
302  // Nothing we can do besides claim it, because the user isn't in
303  // the DB yet
304  if ( $req->username !== $user->getName() ) {
305  $req = clone $req;
306  $req->username = $user->getName();
307  }
308  $ret = AuthenticationResponse::newPass( $req->username );
309  $ret->createRequest = $req;
310  return $ret;
311  }
313  }
314 
315  public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
316  if ( $this->accountCreationType() === self::TYPE_NONE ) {
317  throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
318  }
319 
320  // Now that the user is in the DB, set the password on it.
321  $this->providerChangeAuthenticationData( $res->createRequest );
322 
323  return null;
324  }
325 }
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 getDBFromIndex(IConnectionProvider $dbProvider, int $index)
Takes $index from ::getDBOptions() and return proper Database object.
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.
getFatalPasswordErrorResponse(string $username, Status $status)
Adds user-friendly description to a fatal password validity check error.
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.
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:98
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
const TYPE_NONE
Provider cannot create or link to accounts.
Shared interface for rigor levels when dealing with User methods.
Provide primary and replica IDatabase connections.