Page MenuHomeWickedGov Phorge

CentralAuthSessionProvider.php
No OneTemporary

Size
19 KB
Referenced Files
None
Subscribers
None

CentralAuthSessionProvider.php

<?php
use MediaWiki\Context\RequestContext;
use MediaWiki\Extension\CentralAuth\CentralAuthSessionManager;
use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
use MediaWiki\MainConfigNames;
use MediaWiki\Password\InvalidPassword;
use MediaWiki\Password\PasswordError;
use MediaWiki\Password\PasswordFactory;
use MediaWiki\Request\FauxRequest;
use MediaWiki\Request\WebRequest;
use MediaWiki\Session\CookieSessionProvider;
use MediaWiki\Session\SessionBackend;
use MediaWiki\Session\SessionInfo;
use MediaWiki\Session\SessionManager;
use MediaWiki\Session\UserInfo;
use MediaWiki\User\TempUser\TempUserConfig;
use MediaWiki\User\User;
use MediaWiki\User\UserIdentityLookup;
use MediaWiki\User\UserRigorOptions;
use Wikimedia\Rdbms\IDBAccessObject;
/**
* CentralAuth cookie-based sessions.
*
* This is intended to completely replace the core CookieSessionProvider.
*
* @warning Due to the complicated way CentralAuth has historically interacted with core
* sessions, this is somewhat complicated and is probably not a good example to
* copy if you're writing your own SessionProvider.
*/
class CentralAuthSessionProvider extends CookieSessionProvider {
/** @var bool */
protected $enable = false;
/** @var array */
protected $centralCookieOptions = [];
private UserIdentityLookup $userIdentityLookup;
private CentralAuthSessionManager $sessionManager;
private TempUserConfig $tempUserConfig;
/**
* @param TempUserConfig $tempUserConfig
* @param UserIdentityLookup $userIdentityLookup
* @param CentralAuthSessionManager $sessionManager
* @param array $params In addition to the parameters for
* CookieSessionProvider, the following are
* recognized:
* - enable: Whether to set CentralAuth-specific features. Defaults to
* $wgCentralAuthCookies.
* - centralSessionName: Central session cookie name. Defaults to
* centralCookieOptions['prefix'] . 'Session'. Note this does not
* replace the parent class's 'sessionName', it's a different cookie.
* - centralCookieOptions: Settings for central cookies
* - prefix: Cookie prefix, defaults to $wgCentralAuthCookiePrefix
* - path: Cookie path, defaults to $wgCentralAuthCookiePath
* - domain: Cookie domain, defaults to $wgCentralAuthCookieDomain
* - secure: Cookie secure flag, defaults to $wgCookieSecure
* - httpOnly: Cookie httpOnly flag, defaults to $wgCookieHttpOnly
* - sameSite: Cookie SameSite attribute, defaults to $wgCookieSameSite
*/
public function __construct(
TempUserConfig $tempUserConfig,
UserIdentityLookup $userIdentityLookup,
CentralAuthSessionManager $sessionManager,
$params = []
) {
$this->userIdentityLookup = $userIdentityLookup;
$this->sessionManager = $sessionManager;
$this->tempUserConfig = $tempUserConfig;
$params += [
'centralCookieOptions' => [],
];
if ( !is_array( $params['centralCookieOptions'] ) ) {
throw new \InvalidArgumentException(
__METHOD__ . ': centralCookieOptions must be an array'
);
}
$this->centralCookieOptions = $params['centralCookieOptions'];
unset( $params['centralCookieOptions'] );
parent::__construct( $params );
}
protected function postInitSetup() {
parent::postInitSetup();
$this->centralCookieOptions += [
'prefix' => $this->getConfig()->get( 'CentralAuthCookiePrefix' ),
'path' => $this->getConfig()->get( 'CentralAuthCookiePath' ),
'domain' => $this->getConfig()->get( 'CentralAuthCookieDomain' ),
'secure' => $this->getConfig()->get( 'CookieSecure' ) || $this->getConfig()->get( 'ForceHTTPS' ),
'httpOnly' => $this->getConfig()->get( 'CookieHttpOnly' ),
'sameSite' => $this->getConfig()->get( 'CookieSameSite' )
];
$params = [
'enable' => $this->getConfig()->get( 'CentralAuthCookies' ),
'centralSessionName' => $this->centralCookieOptions['prefix'] . 'Session',
];
$this->params += $params;
$this->enable = (bool)$params['enable'];
}
/**
* Get the local session info, with CentralAuthSource metadata.
*
* @param WebRequest $request
* @param bool $forceEmptyPersist If this is true and there is no local
* session, return a persistent empty local session to override the
* non-functional CentralAuth one. This prevents the deletion of global
* cookies, allowing the user to retain their central login elsewhere
* in the same cookie domain. It makes sense to do this for problems with
* the central session that are specific to the local wiki. (T342475)
* @return SessionInfo|null
*/
private function returnParentSessionInfo( WebRequest $request, $forceEmptyPersist = false ) {
$info = parent::provideSessionInfo( $request );
if ( $info ) {
return new SessionInfo( $info->getPriority(), [
'copyFrom' => $info,
'metadata' => [
'CentralAuthSource' => 'Local',
],
] );
}
if ( $forceEmptyPersist ) {
return new SessionInfo( $this->priority, [
'id' => null,
'provider' => $this,
'idIsSafe' => true,
'persisted' => true,
'metadata' => [
'CentralAuthSource' => 'Local',
],
] );
}
return null;
}
/**
* Determine whether $request has a valid CentralAuth session.
*
* The following requests are considered to have a valid CentralAuth session:
* - A request with a centralauth_User and centralauth_Token cookie (prefix is configurable)
* that match the data in the globaluser table.
* - A request with a centralauth_Session cookie where the referenced data exists in the
* central session store, and the username and token in the central session object match
* the data in the globaluser table.
*
* The following requests are considered to have a valid local session (marked with
* CentralAuthSource => Local in the session metadata):
* - A request with a centralauth_Session cookie where the referenced data exists in the
* central session store, but is a stub (has pending_name or pending_guid fields).
* - Anything that CookieSessionProvider considers valid, if the session is anonymous or for
* a local user that's not attached to the global CentralAuth user.
* - When $wgCentralAuthCookies is disabled, falls back to CookieSessionProvider entirely.
*
* @param WebRequest $request
* @return SessionInfo|null
*/
public function provideSessionInfo( WebRequest $request ) {
if ( !$this->enable ) {
$this->logger->debug( __METHOD__ . ': Not enabled, falling back to core sessions' );
return $this->returnParentSessionInfo( $request );
}
$info = [
'id' => $this->getCookie( $request, $this->params['sessionName'], '' )
];
if ( !SessionManager::validateSessionId( $info['id'] ) ) {
unset( $info['id'] );
}
$userName = null;
$token = null;
$from = null;
$prefix = $this->centralCookieOptions['prefix'];
$userCookie = $this->getCookie( $request, 'User', $prefix );
$tokenCookie = $this->getCookie( $request, 'Token', $prefix );
if ( $userCookie !== null && $tokenCookie !== null ) {
$userName = $userCookie;
$token = $tokenCookie;
$from = 'cookies';
} else {
$id = $this->getCookie( $request, $this->params['centralSessionName'], '' );
if ( $id !== null ) {
$data = $this->sessionManager->getCentralSessionById( $id );
if ( isset( $data['pending_name'] ) || isset( $data['pending_guid'] ) ) {
$this->logger->debug( __METHOD__ . ': uninitialized session' );
} elseif ( isset( $data['token'] ) && isset( $data['user'] ) ) {
$token = $data['token'];
$userName = $data['user'];
$from = 'session';
} else {
$this->logger->debug( __METHOD__ . ': uninitialized session' );
}
}
}
if ( $userName === null || $token === null ) {
return $this->returnParentSessionInfo( $request );
}
// Check to avoid session ID collisions, as reported on T21158
if ( $userCookie === null ) {
$this->logger->debug(
__METHOD__ . ': no User cookie, so unable to check for session mismatch'
);
return $this->returnParentSessionInfo( $request );
}
if ( $userCookie != $userName ) {
$this->logger->debug(
__METHOD__ . ': Session ID/User mismatch. Possible session collision. ' .
"Expected: $userName; actual: $userCookie"
);
return $this->returnParentSessionInfo( $request );
}
// Clean up username
$userName = $this->userNameUtils->getCanonical( $userName );
if ( !$userName ) {
$this->logger->debug( __METHOD__ . ': invalid username' );
return $this->returnParentSessionInfo( $request );
}
if ( !$this->userNameUtils->isUsable( $userName ) ) {
// Log a warning if the username is not usable, except if the name is reserved by the temporary account
// system to avoid spamming the logs (T373827).
if ( !$this->tempUserConfig->isReservedName( $userName ) ) {
$this->logger->warning(
__METHOD__ . ': username {username} is not usable on this wiki', [
'username' => $userName,
]
);
}
return $this->returnParentSessionInfo( $request, true );
}
// Try the central user
$centralUser = CentralAuthUser::getInstanceByName( $userName );
// Skip if they're being renamed
if ( $centralUser->renameInProgress() ) {
$this->logger->debug( __METHOD__ . ': rename in progress' );
// No fallback here, just fail it because our SessionCheckMetadata
// hook will do so anyway.
return null;
}
if ( !$centralUser->exists() ) {
$this->logger->debug( __METHOD__ . ': global account doesn\'t exist' );
return $this->returnParentSessionInfo( $request );
}
if ( !$centralUser->isAttached() ) {
$userIdentity = $this->userIdentityLookup->getUserIdentityByName( $userName );
if ( $userIdentity && $userIdentity->isRegistered() ) {
$this->logger->debug( __METHOD__ . ': not attached and local account exists' );
return $this->returnParentSessionInfo( $request, true );
}
}
if ( $centralUser->authenticateWithToken( $token ) != 'ok' ) {
$this->logger->debug( __METHOD__ . ': token mismatch' );
// At this point, don't log in with a local session anymore
return null;
}
$this->logger->debug( __METHOD__ . ": logged in from $from" );
$info += [
'userInfo' => UserInfo::newFromName( $userName, true ),
'provider' => $this,
// CA sessions are always persistent
'persisted' => true,
'remembered' => $tokenCookie !== null,
'metadata' => [
'CentralAuthSource' => 'CentralAuth',
],
];
return new SessionInfo( $this->priority, $info );
}
/** @inheritDoc */
public function refreshSessionInfo( SessionInfo $info, WebRequest $request, &$metadata ) {
// Check on the metadata, to avoid T124409
if ( isset( $metadata['CentralAuthSource'] ) ) {
$name = $info->getUserInfo()->getName();
if ( $name === null ) {
return true;
}
$source = 'Local';
if ( $this->enable ) {
$centralUser = CentralAuthUser::getInstanceByName( $name );
$centralUserExists = $centralUser->exists();
if ( $centralUserExists && $centralUser->isAttached() ) {
$source = 'CentralAuth';
} elseif ( $centralUserExists ) {
$userIdentity = $this->userIdentityLookup->getUserIdentityByName(
$name,
IDBAccessObject::READ_LATEST
);
if ( !$userIdentity || !$userIdentity->isRegistered() ) {
$source = 'CentralAuth';
}
}
}
if ( $metadata['CentralAuthSource'] !== $source ) {
$this->logger->warning(
'Session "{session}": CentralAuth saved source {saved} != expected source {expected}', [
'session' => $info->__toString(),
'saved' => $metadata['CentralAuthSource'],
'expected' => $source,
]
);
return false;
}
}
return true;
}
/** @inheritDoc */
public function sessionIdWasReset( SessionBackend $session, $oldId ) {
if ( !$this->enable ) {
return;
}
// We need a Session to pass to CentralAuthSessionManager::setCentralSession()
// to reset the session ID, so create one on a new FauxRequest.
$s = $session->getSession( new FauxRequest() );
// We also need to fetch the current central data to pass to
// CentralAuthSessionManager::setCentralSession() when resetting the ID.
$data = $this->sessionManager->getCentralSession( $s );
$this->sessionManager->setCentralSession( $data, true, $s );
}
/** @inheritDoc */
protected function sessionDataToExport( $user ) {
$data = parent::sessionDataToExport( $user );
// CentralAuth needs to prevent core login-from-session to
// avoid bugs like T124409
$centralUser = CentralAuthUser::getInstance( $user );
if ( $centralUser->isAttached() ) {
unset( $data['wsToken'] );
}
return $data;
}
/** @inheritDoc */
protected function cookieDataToExport( $user, $remember ) {
// If we're going to set CA cookies, don't remember in core cookies.
if ( $this->enable && $remember ) {
$centralUser = CentralAuthUser::getInstance( $user );
$remember = !$centralUser->isAttached();
}
return parent::cookieDataToExport( $user, $remember );
}
/**
* Sets CentralAuth and core authentication cookies.
*
* - For authenticated sessions where the local account is owned by the corresponding central
* account, the core <wiki>UserToken cookie is not set, the centralauth_Session,
* centralauth_User, and (if the session is remembered) centralauth_Token cookies are set,
* and a new central session object is created in the central session store if there wasn't
* already a centralauth_Session cookie pointing at a valid object.
* - Otherwise (including stub central sessions), behavior is identical to CookieSessionProvider.
*
* @param SessionBackend $session
* @param WebRequest $request
*/
public function persistSession( SessionBackend $session, WebRequest $request ) {
parent::persistSession( $session, $request );
if ( !$this->enable ) {
return;
}
$response = $request->response();
if ( $response->headersSent() ) {
// Can't do anything now
return;
}
$s = $session->getSession( $request );
$user = $session->getUser();
$centralUser = CentralAuthUser::getInstance( $user );
if ( $centralUser->exists() && ( $centralUser->isAttached() || !$user->getId() ) ) {
// CentralAuth needs to prevent core login-from-session to
// avoid bugs like T124409
$data = &$session->getData();
if ( array_key_exists( 'wsToken', $data ) ) {
unset( $data['wsToken'] );
$session->dirty();
}
unset( $data );
$metadata = $session->getProviderMetadata();
$metadata['CentralAuthSource'] = 'CentralAuth';
$session->setProviderMetadata( $metadata );
$remember = $session->shouldRememberUser();
$options = $this->centralCookieOptions;
// We only save the user into the central session if it's not a
// "pending" session, but we still need the ID to set the cookie.
$data = $this->sessionManager->getCentralSession( $s );
if ( isset( $data['pending_name'] ) ) {
$remember = false;
} else {
$data['user'] = $centralUser->getName();
$data['token'] = $centralUser->getAuthToken();
$data['remember'] = $remember;
}
$centralSessionId = $this->sessionManager->setCentralSession( $data, false, $s );
$cookies = [
'User' => (string)$centralUser->getName(),
'Token' => $remember ? (string)$centralUser->getAuthToken() : false,
];
foreach ( $cookies as $name => $value ) {
if ( $value === false ) {
$response->clearCookie( $name, $options );
} else {
$expirationDuration = $this->getLoginCookieExpiration( $name, $remember );
$expiration = $expirationDuration ? $expirationDuration + time() : null;
$response->setCookie( $name, (string)$value, $expiration, $options );
}
}
$response->setCookie( $this->params['centralSessionName'], $centralSessionId, null,
[ 'prefix' => '' ] + $options );
} else {
$metadata = $session->getProviderMetadata();
$metadata['CentralAuthSource'] = 'Local';
$session->setProviderMetadata( $metadata );
}
}
/**
* Deletes CentralAuth and core authentication cookies.
*
* @param WebRequest $request
*/
public function unpersistSession( WebRequest $request ) {
parent::unpersistSession( $request );
if ( !$this->enable ) {
return;
}
$response = $request->response();
if ( $response->headersSent() ) {
// Can't do anything now
$this->logger->debug( __METHOD__ . ': Headers already sent' );
return;
}
$expiry = time() - 86400;
$response->clearCookie( 'User', $this->centralCookieOptions );
$response->clearCookie( 'Token', $this->centralCookieOptions );
$response->clearCookie( $this->params['centralSessionName'],
[ 'prefix' => '' ] + $this->centralCookieOptions );
}
/** @inheritDoc */
public function invalidateSessionsForUser( User $user ) {
$centralUser = CentralAuthUser::getPrimaryInstance( $user );
if ( $centralUser->exists() && ( $centralUser->isAttached() || !$user->isRegistered() ) ) {
$centralUser->resetAuthToken();
}
}
/** @inheritDoc */
public function preventSessionsForUser( $username ) {
$username = $this->userNameUtils->getCanonical( $username );
if ( !$username ) {
return;
}
$centralUser = CentralAuthUser::getPrimaryInstanceByName( $username );
if ( !$centralUser->exists() ) {
return;
}
// Reset the user's password to something invalid and reset the token
// if it's not already invalid.
$config = RequestContext::getMain()->getConfig();
$passwordFactory = new PasswordFactory(
$config->get( MainConfigNames::PasswordConfig ),
$config->get( MainConfigNames::PasswordDefault )
);
try {
$password = $passwordFactory->newFromCiphertext( $centralUser->getPassword() );
} catch ( PasswordError $e ) {
return;
}
if ( !$password instanceof InvalidPassword ) {
$centralUser->setPassword( null );
}
}
/**
* @inheritDoc
*/
protected function setForceHTTPSCookie( $set, ?SessionBackend $backend, WebRequest $request ) {
// Do nothing. We don't support mixed-protocol HTTP/HTTPS wikis in CentralAuth,
// so this cookie is not needed.
}
/** @inheritDoc */
protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
if ( $loggedOut + 86400 > time() &&
$loggedOut !== (int)$this->getCookie(
$request, 'LoggedOut', $this->centralCookieOptions['prefix'] )
) {
$request->response()->setCookie( 'LoggedOut', (string)$loggedOut, $loggedOut + 86400,
$this->centralCookieOptions );
}
}
/**
* @return string[]
*/
public function getVaryCookies() {
$cookies = parent::getVaryCookies();
if ( $this->enable ) {
$prefix = $this->centralCookieOptions['prefix'];
$cookies[] = $prefix . 'Token';
$cookies[] = $prefix . 'LoggedOut';
$cookies[] = $this->params['centralSessionName'];
}
return $cookies;
}
/** @inheritDoc */
public function suggestLoginUsername( WebRequest $request ) {
$name = $this->getCookie( $request, 'User', $this->centralCookieOptions['prefix'] );
if ( $name !== null ) {
if ( $this->userNameUtils->isTemp( $name ) ) {
$name = false;
} else {
$name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_USABLE );
}
}
return ( $name === false || $name === null )
? parent::suggestLoginUsername( $request )
: $name;
}
/**
* Fetch the central cookie domain
* @return string
*/
public function getCentralCookieDomain() {
return $this->centralCookieOptions['domain'];
}
/** @inheritDoc */
protected function getExtendedLoginCookies() {
$cookies = parent::getExtendedLoginCookies();
$cookies[] = 'User';
return $cookies;
}
/** @inheritDoc */
public function getRememberUserDuration() {
// CentralAuth needs User and Token cookies to remember the user. The fallback to
// sessions needs UserID as well, so if that one has shorter expiration, the remember
// duration will depend on whether the account is attached; let's return the shorter
// duration in that case.
return min(
$this->getLoginCookieExpiration( 'User', true ),
$this->getLoginCookieExpiration( 'Token', true ),
$this->getLoginCookieExpiration( 'UserID', true )
) ?: null;
}
}

File Metadata

Mime Type
text/x-php
Expires
Tue, Aug 18, 18:00 (3 w, 3 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
9f/42/f2360cc32c54484d3183e092110b
Default Alt Text
CentralAuthSessionProvider.php (19 KB)

Event Timeline