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