MediaWiki REL1_30
AuthPluginPrimaryAuthenticationProvider.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Auth;
25
26use AuthPlugin;
27use User;
28
38{
39 private $auth;
40 private $hasDomain;
41 private $requestType = null;
42
49 public function __construct( AuthPlugin $auth, $requestType = null ) {
50 parent::__construct();
51
52 if ( $auth instanceof AuthManagerAuthPlugin ) {
53 throw new \InvalidArgumentException(
54 'Trying to wrap AuthManagerAuthPlugin in AuthPluginPrimaryAuthenticationProvider ' .
55 'makes no sense.'
56 );
57 }
58
59 $need = count( $auth->domainList() ) > 1
60 ? PasswordDomainAuthenticationRequest::class
61 : PasswordAuthenticationRequest::class;
62 if ( $requestType === null ) {
63 $requestType = $need;
64 } elseif ( $requestType !== $need && !is_subclass_of( $requestType, $need ) ) {
65 throw new \InvalidArgumentException( "$requestType is not a $need" );
66 }
67
68 $this->auth = $auth;
69 $this->requestType = $requestType;
70 $this->hasDomain = (
71 $requestType === PasswordDomainAuthenticationRequest::class ||
72 is_subclass_of( $requestType, PasswordDomainAuthenticationRequest::class )
73 );
74 $this->authoritative = $auth->strict();
75
76 // Registering hooks from core is unusual, but is needed here to be
77 // able to call the AuthPlugin methods those hooks replace.
78 \Hooks::register( 'UserSaveSettings', [ $this, 'onUserSaveSettings' ] );
79 \Hooks::register( 'UserGroupsChanged', [ $this, 'onUserGroupsChanged' ] );
80 \Hooks::register( 'UserLoggedIn', [ $this, 'onUserLoggedIn' ] );
81 \Hooks::register( 'LocalUserCreated', [ $this, 'onLocalUserCreated' ] );
82 }
83
88 protected function makeAuthReq() {
89 $class = $this->requestType;
90 if ( $this->hasDomain ) {
91 return new $class( $this->auth->domainList() );
92 } else {
93 return new $class();
94 }
95 }
96
101 protected function setDomain( $req ) {
102 if ( $this->hasDomain ) {
103 $domain = $req->domain;
104 } else {
105 // Just grab the first one.
106 $domainList = $this->auth->domainList();
107 $domain = reset( $domainList );
108 }
109
110 // Special:UserLogin does this. Strange.
111 if ( !$this->auth->validDomain( $domain ) ) {
112 $domain = $this->auth->getDomain();
113 }
114 $this->auth->setDomain( $domain );
115 }
116
122 public function onUserSaveSettings( $user ) {
123 // No way to know the domain, just hope the provider handles that.
124 $this->auth->updateExternalDB( $user );
125 }
126
133 public function onUserGroupsChanged( $user, $added, $removed ) {
134 // No way to know the domain, just hope the provider handles that.
135 $this->auth->updateExternalDBGroups( $user, $added, $removed );
136 }
137
142 public function onUserLoggedIn( $user ) {
143 $hookUser = $user;
144 // No way to know the domain, just hope the provider handles that.
145 $this->auth->updateUser( $hookUser );
146 if ( $hookUser !== $user ) {
147 throw new \UnexpectedValueException(
148 get_class( $this->auth ) . '::updateUser() tried to replace $user!'
149 );
150 }
151 }
152
158 public function onLocalUserCreated( $user, $autocreated ) {
159 // For $autocreated, see self::autoCreatedAccount()
160 if ( !$autocreated ) {
161 $hookUser = $user;
162 // No way to know the domain, just hope the provider handles that.
163 $this->auth->initUser( $hookUser, $autocreated );
164 if ( $hookUser !== $user ) {
165 throw new \UnexpectedValueException(
166 get_class( $this->auth ) . '::initUser() tried to replace $user!'
167 );
168 }
169 }
170 }
171
172 public function getUniqueId() {
173 return parent::getUniqueId() . ':' . get_class( $this->auth );
174 }
175
176 public function getAuthenticationRequests( $action, array $options ) {
177 switch ( $action ) {
180 return [ $this->makeAuthReq() ];
181
184 // No way to know the domain, just hope the provider handles that.
185 return $this->auth->allowPasswordChange() ? [ $this->makeAuthReq() ] : [];
186
187 default:
188 return [];
189 }
190 }
191
192 public function beginPrimaryAuthentication( array $reqs ) {
193 $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType );
194 if ( !$req || $req->username === null || $req->password === null ||
195 ( $this->hasDomain && $req->domain === null )
196 ) {
198 }
199
200 $username = User::getCanonicalName( $req->username, 'usable' );
201 if ( $username === false ) {
203 }
204
205 $this->setDomain( $req );
206 if ( $this->testUserCanAuthenticateInternal( User::newFromName( $username ) ) &&
207 $this->auth->authenticate( $username, $req->password )
208 ) {
210 } else {
211 $this->authoritative = $this->auth->strict() || $this->auth->strictUserAuth( $username );
212 return $this->failResponse( $req );
213 }
214 }
215
217 $username = User::getCanonicalName( $username, 'usable' );
218 if ( $username === false ) {
219 return false;
220 }
221
222 // We have to check every domain, because at least LdapAuthentication
223 // interprets AuthPlugin::userExists() as applying only to the current
224 // domain.
225 $curDomain = $this->auth->getDomain();
226 $domains = $this->auth->domainList() ?: [ '' ];
227 foreach ( $domains as $domain ) {
228 $this->auth->setDomain( $domain );
229 if ( $this->testUserCanAuthenticateInternal( User::newFromName( $username ) ) ) {
230 $this->auth->setDomain( $curDomain );
231 return true;
232 }
233 }
234 $this->auth->setDomain( $curDomain );
235 return false;
236 }
237
244 private function testUserCanAuthenticateInternal( $user ) {
245 if ( $this->auth->userExists( $user->getName() ) ) {
246 return !$this->auth->getUserInstance( $user )->isLocked();
247 } else {
248 return false;
249 }
250 }
251
253 $username = User::getCanonicalName( $username, 'usable' );
254 if ( $username === false ) {
255 return;
256 }
257 $user = User::newFromName( $username );
258 if ( $user ) {
259 // Reset the password on every domain.
260 $curDomain = $this->auth->getDomain();
261 $domains = $this->auth->domainList() ?: [ '' ];
262 $failed = [];
263 foreach ( $domains as $domain ) {
264 $this->auth->setDomain( $domain );
265 if ( $this->testUserCanAuthenticateInternal( $user ) &&
266 !$this->auth->setPassword( $user, null )
267 ) {
268 $failed[] = $domain === '' ? '(default)' : $domain;
269 }
270 }
271 $this->auth->setDomain( $curDomain );
272 if ( $failed ) {
273 throw new \UnexpectedValueException(
274 "AuthPlugin failed to reset password for $username in the following domains: "
275 . join( ' ', $failed )
276 );
277 }
278 }
279 }
280
281 public function testUserExists( $username, $flags = User::READ_NORMAL ) {
282 $username = User::getCanonicalName( $username, 'usable' );
283 if ( $username === false ) {
284 return false;
285 }
286
287 // We have to check every domain, because at least LdapAuthentication
288 // interprets AuthPlugin::userExists() as applying only to the current
289 // domain.
290 $curDomain = $this->auth->getDomain();
291 $domains = $this->auth->domainList() ?: [ '' ];
292 foreach ( $domains as $domain ) {
293 $this->auth->setDomain( $domain );
294 if ( $this->auth->userExists( $username ) ) {
295 $this->auth->setDomain( $curDomain );
296 return true;
297 }
298 }
299 $this->auth->setDomain( $curDomain );
300 return false;
301 }
302
304 // No way to know the domain, just hope the provider handles that.
305 return $this->auth->allowPropChange( $property );
306 }
307
309 AuthenticationRequest $req, $checkData = true
310 ) {
311 if ( get_class( $req ) !== $this->requestType ) {
312 return \StatusValue::newGood( 'ignored' );
313 }
314
315 // Hope it works, AuthPlugin gives us no way to do this.
316 $curDomain = $this->auth->getDomain();
317 $this->setDomain( $req );
318 try {
319 // If !$checkData the domain might be wrong. Nothing we can do about that.
320 if ( !$this->auth->allowPasswordChange() ) {
321 return \StatusValue::newFatal( 'authmanager-authplugin-setpass-denied' );
322 }
323
324 if ( !$checkData ) {
325 return \StatusValue::newGood();
326 }
327
328 if ( $this->hasDomain ) {
329 if ( $req->domain === null ) {
330 return \StatusValue::newGood( 'ignored' );
331 }
332 if ( !$this->auth->validDomain( $req->domain ) ) {
333 return \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' );
334 }
335 }
336
337 $username = User::getCanonicalName( $req->username, 'usable' );
338 if ( $username !== false ) {
339 $sv = \StatusValue::newGood();
340 if ( $req->password !== null ) {
341 if ( $req->password !== $req->retype ) {
342 $sv->fatal( 'badretype' );
343 } else {
344 $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
345 }
346 }
347 return $sv;
348 } else {
349 return \StatusValue::newGood( 'ignored' );
350 }
351 } finally {
352 $this->auth->setDomain( $curDomain );
353 }
354 }
355
357 if ( get_class( $req ) === $this->requestType ) {
358 $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false;
359 if ( $username === false ) {
360 return;
361 }
362
363 if ( $this->hasDomain && $req->domain === null ) {
364 return;
365 }
366
367 $this->setDomain( $req );
368 $user = User::newFromName( $username );
369 if ( !$this->auth->setPassword( $user, $req->password ) ) {
370 // This is totally unfriendly and leaves other
371 // AuthenticationProviders in an uncertain state, but what else
372 // can we do?
373 throw new \ErrorPageError(
374 'authmanager-authplugin-setpass-failed-title',
375 'authmanager-authplugin-setpass-failed-message'
376 );
377 }
378 }
379 }
380
381 public function accountCreationType() {
382 // No way to know the domain, just hope the provider handles that.
383 return $this->auth->canCreateAccounts() ? self::TYPE_CREATE : self::TYPE_NONE;
384 }
385
386 public function testForAccountCreation( $user, $creator, array $reqs ) {
387 return \StatusValue::newGood();
388 }
389
390 public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
391 if ( $this->accountCreationType() === self::TYPE_NONE ) {
392 throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
393 }
394
395 $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType );
396 if ( !$req || $req->username === null || $req->password === null ||
397 ( $this->hasDomain && $req->domain === null )
398 ) {
400 }
401
402 $username = User::getCanonicalName( $req->username, 'usable' );
403 if ( $username === false ) {
405 }
406
407 $this->setDomain( $req );
408 if ( $this->auth->addUser(
409 $user, $req->password, $user->getEmail(), $user->getRealName()
410 ) ) {
412 } else {
414 new \Message( 'authmanager-authplugin-create-fail' )
415 );
416 }
417 }
418
419 public function autoCreatedAccount( $user, $source ) {
420 $hookUser = $user;
421 // No way to know the domain, just hope the provider handles that.
422 $this->auth->initUser( $hookUser, true );
423 if ( $hookUser !== $user ) {
424 throw new \UnexpectedValueException(
425 get_class( $this->auth ) . '::initUser() tried to replace $user!'
426 );
427 }
428 }
429}
Authentication plugin interface.
Basic framework for a primary authentication provider that uses passwords.
failResponse(PasswordAuthenticationRequest $req)
Return the appropriate response for failure.
Backwards-compatibility wrapper for AuthManager via $wgAuth.
const ACTION_CHANGE
Change a user's credentials.
const ACTION_REMOVE
Remove a user's credentials.
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
const ACTION_CREATE
Create a new user.
getAuthenticationRequests( $action, array $options)
Return the applicable list of AuthenticationRequests.
providerAllowsPropertyChange( $property)
Determine whether a property can change.
onLocalUserCreated( $user, $autocreated)
Hook function to call AuthPlugin::initUser()
providerAllowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
testUserCanAuthenticate( $username)
Test whether the named user can authenticate with this provider.
onUserSaveSettings( $user)
Hook function to call AuthPlugin::updateExternalDB()
beginPrimaryAccountCreation( $user, $creator, array $reqs)
Start an account creation flow.
onUserLoggedIn( $user)
Hook function to call AuthPlugin::updateUser()
testForAccountCreation( $user, $creator, array $reqs)
Determine whether an account creation may begin.
providerChangeAuthenticationData(AuthenticationRequest $req)
Change or remove authentication data (e.g.
onUserGroupsChanged( $user, $added, $removed)
Hook function to call AuthPlugin::updateExternalDBGroups()
testUserExists( $username, $flags=User::READ_NORMAL)
Test whether the named user exists.
This is a value object for authentication requests.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
The Message class provides methods which fulfil two basic services:
Definition Message.php:159
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
this hook is for auditing only $req
Definition hooks.txt:988
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:1971
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition hooks.txt:2805
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:783
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
const TYPE_NONE
Provider cannot create or link to accounts.
$source
$property