* In the future, it may also serve as the entry point to the authorization
* system.
*
+ * If you are looking at this because you are working on an extension that creates its own
+ * login or signup page, then 1) you really shouldn't do that, 2) if you feel you absolutely
+ * have to, subclass AuthManagerSpecialPage or build it on the client side using the clientlogin
+ * or the createaccount API. Trying to call this class directly will very likely end up in
+ * security vulnerabilities or broken UX in edge cases.
+ *
+ * If you are working on an extension that needs to integrate with the authentication system
+ * (e.g. by providing a new login method, or doing extra permission checks), you'll probably
+ * need to write an AuthenticationProvider.
+ *
+ * If you want to create a "reserved" user programmatically, User::newSystemUser() might be what
+ * you are looking for. If you want to change user data, use User::changeAuthenticationData().
+ * Code that is related to some SessionProvider or PrimaryAuthenticationProvider can
+ * create a (non-reserved) user by calling AuthManager::autoCreateUser(); it is then the provider's
+ * responsibility to ensure that the user can authenticate somehow (see especially
+ * PrimaryAuthenticationProvider::autoCreatedAccount()).
+ * If you are writing code that is not associated with such a provider and needs to create accounts
+ * programmatically for real users, you should rethink your architecture. There is no good way to
+ * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
+ * cannot provide any means for users to access the accounts it would create.
+ *
+ * The two main control flows when using this class are as follows:
+ * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
+ * the requests with data (by using them to build a HTMLForm and have the user fill it, or by
+ * exposing a form specification via the API, so that the client can build it), and pass them to
+ * the appropriate begin* method. That will return either a success/failure response, or more
+ * requests to fill (either by building a form or by redirecting the user to some external
+ * provider which will send the data back), in which case they need to be submitted to the
+ * appropriate continue* method and that step has to be repeated until the response is a success
+ * or failure response. AuthManager will use the session to maintain internal state during the
+ * process.
+ * * Code doing an authentication data change will call getAuthenticationRequests(), select
+ * a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
+ * changeAuthenticationData(). If the data change is user-initiated, the whole process needs
+ * to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
+ * a non-OK status.
+ *
* @ingroup Auth
* @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
*/
class AuthManager implements LoggerAwareInterface {
/** Log in with an existing (not necessarily local) user */
/**
* Determine whether a username can authenticate
*
- * @param string $username
+ * This is mainly for internal purposes and only takes authentication data into account,
+ * not things like blocks that can change without the authentication system being aware.
+ *
+ * @param string $username MediaWiki username
* @return bool
*/
public function userCanAuthenticate( $username ) {
* If $req was returned for AuthManager::ACTION_REMOVE, using $req should
* no longer result in a successful login.
*
+ * This method should only be called if allowsAuthenticationDataChange( $req, true )
+ * returned success.
+ *
* @param AuthenticationRequest $req
*/
public function changeAuthenticationData( AuthenticationRequest $req ) {
/**
* Determine whether a particular account can be created
- * @param string $username
+ * @param string $username MediaWiki username
* @param array $options
* - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
* - creating: (bool) For internal use only. Never specify this.
/**
* Auto-create an account, and log into that account
+ *
+ * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
+ * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
+ * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
+ * the username of a non-existing user from provideSessionInfo(). Calling this method
+ * explicitly (e.g. from a maintenance script) is also fine.
+ *
* @param User $user User to auto-create
* @param string $source What caused the auto-creation? This must be the ID
* of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
}
}
- // Ignore warnings about master connections/writes...hard to avoid here
- \Profiler::instance()->getTransactionProfiler()->resetExpectations();
-
$backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
if ( $cache->get( $backoffKey ) ) {
$this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
'from' => $from,
] );
+ // Ignore warnings about master connections/writes...hard to avoid here
+ $trxProfiler = \Profiler::instance()->getTransactionProfiler();
+ $trxProfiler->setSilenced( true );
try {
$status = $user->addToDatabase();
if ( !$status->isOk() ) {
return $status;
}
} catch ( \Exception $ex ) {
+ $trxProfiler->setSilenced( false );
$this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
'username' => $username,
'exception' => $ex,
// Update user count
\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
-
// Watch user's userpage and talk page
- $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
+ \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
+ $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
+ } );
// Log the creation
if ( $this->config->get( 'NewUserLog' ) ) {
$logEntry->setParameters( [
'4::userid' => $user->getId(),
] );
- $logid = $logEntry->insert();
+ $logEntry->insert();
}
- // Commit database changes, so even if something else later blows up
- // the newly-created user doesn't get lost.
- wfGetLBFactory()->commitMasterChanges( __METHOD__ );
+ $trxProfiler->setSilenced( false );
if ( $login ) {
$this->setSessionDataForUser( $user );
// Query them and merge results
$reqs = [];
- $allPrimaryRequired = null;
foreach ( $providers as $provider ) {
$isPrimary = $provider instanceof PrimaryAuthenticationProvider;
- $thisRequired = [];
foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
$id = $req->getUniqueId();
- // If it's from a Primary, mark it as "primary-required" but
- // track it for later.
+ // If a required request if from a Primary, mark it as "primary-required" instead
if ( $isPrimary ) {
if ( $req->required ) {
- $thisRequired[$id] = true;
$req->required = AuthenticationRequest::PRIMARY_REQUIRED;
}
}
- if ( !isset( $reqs[$id] ) || $req->required === AuthenticationRequest::REQUIRED ) {
+ if (
+ !isset( $reqs[$id] )
+ || $req->required === AuthenticationRequest::REQUIRED
+ || $reqs[$id] === AuthenticationRequest::OPTIONAL
+ ) {
$reqs[$id] = $req;
}
}
-
- // Track which requests are required by all primaries
- if ( $isPrimary ) {
- $allPrimaryRequired = $allPrimaryRequired === null
- ? $thisRequired
- : array_intersect_key( $allPrimaryRequired, $thisRequired );
- }
- }
- // Any requests that were required by all primaries are required.
- foreach ( (array)$allPrimaryRequired as $id => $dummy ) {
- $reqs[$id]->required = AuthenticationRequest::REQUIRED;
}
// AuthManager has its own req for some actions
}
/**
+ * Log the user in
* @param User $user
* @param bool|null $remember
*/
/**
* Reset the internal caching for unit testing
+ * @protected Unit tests only
*/
public static function resetCache() {
if ( !defined( 'MW_PHPUNIT_TEST' ) ) {