Page MenuHomeWickedGov Phorge

SsoHookHandler.php
No OneTemporary

Size
9 KB
Referenced Files
None
Subscribers
None

SsoHookHandler.php

<?php
namespace MediaWiki\Extension\CentralAuth\Hooks\Handlers;
use LogicException;
use MediaWiki\Api\Hook\ApiCheckCanExecuteHook;
use MediaWiki\Auth\AuthenticationResponse;
use MediaWiki\Auth\AuthManager;
use MediaWiki\Auth\Hook\AuthManagerFilterProvidersHook;
use MediaWiki\Auth\Hook\AuthManagerVerifyAuthenticationHook;
use MediaWiki\Auth\LocalPasswordPrimaryAuthenticationProvider;
use MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider;
use MediaWiki\Context\RequestContext;
use MediaWiki\Extension\CentralAuth\CentralAuthRedirectingPrimaryAuthenticationProvider;
use MediaWiki\Extension\CentralAuth\FilteredRequestTracker;
use MediaWiki\Extension\CentralAuth\SharedDomainUtils;
use MediaWiki\Hook\GetLocalURLHook;
use MediaWiki\Hook\SetupAfterCacheHook;
use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook;
use MediaWiki\ResourceLoader\Hook\ResourceLoaderModifyEmbeddedSourceUrlsHook;
use MediaWiki\User\UserIdentity;
use MediaWiki\Utils\UrlUtils;
use MediaWiki\WikiMap\WikiMap;
use MobileContext;
use MWExceptionHandler;
use Wikimedia\NormalizedException\NormalizedException;
/**
* Ensure that the SSO domain cannot be used for anything that is unrelated to its purpose.
*/
class SsoHookHandler implements
GetLocalURLHook,
SetupAfterCacheHook,
GetUserPermissionsErrorsHook,
ApiCheckCanExecuteHook,
ResourceLoaderModifyEmbeddedSourceUrlsHook,
AuthManagerFilterProvidersHook,
AuthManagerVerifyAuthenticationHook
{
// Allowlists of things a user can do on the SSO domain.
// FIXME these should be configurable and/or come from extension attributes
// 'static' is WMF's custom static.php entry point, serving some files on the SSO domain (T374286)
private const ALLOWED_ENTRY_POINTS = [ 'index', 'api', 'static' ];
private const ALLOWED_SPECIAL_PAGES = [ 'Userlogin', 'Userlogout', 'CreateAccount',
'PasswordReset', 'Captcha' ];
private const ALLOWED_API_MODULES = [
// needed for allowing any query API, even if we only want meta modules; it can be
// used to check page existence (which is unwanted functionality on the SSO domain),
// which is unfortunate but permissions will still be checked, so it's not a risk.
'query',
// allow login/signup directly via the API + help for those APIs
'clientlogin', 'createaccount', 'authmanagerinfo', 'paraminfo', 'help',
// APIs used during web login
'validatepassword', 'userinfo', 'webauthn', 'fancycaptchareload',
// generic meta APIs, there's a good chance something somewhere will use them
'siteinfo', 'globaluserinfo', 'tokens',
];
// List of authentication providers which should be skipped on the local login page in
// SUL3 mode, because they will be done on the shared domain instead.
// This is somewhat fragile, e.g. in case of class renamespacing. We inherit that from
// AuthManager and can't do much about it. It fails in the safe direction, though - on
// provider key mismatch there will be unnecessary extra checks.
private const DISALLOWED_LOCAL_PROVIDERS = [
// FIXME what about preauth providers like AbuseFilter which don't generate a form
// but might prevent login and then the user ends up on a confusing login page?
// FIXME what about providers (if any) which generate a form but also do something else?
'preauth' => [
'CaptchaPreAuthenticationProvider',
],
'primaryauth' => [
TemporaryPasswordPrimaryAuthenticationProvider::class,
LocalPasswordPrimaryAuthenticationProvider::class,
],
'secondaryauth' => [
'OATHSecondaryAuthenticationProvider',
],
];
private UrlUtils $urlUtils;
private FilteredRequestTracker $filteredRequestTracker;
private SharedDomainUtils $sharedDomainUtils;
private ?MobileContext $mobileContext;
public function __construct(
UrlUtils $urlUtils,
FilteredRequestTracker $filteredRequestTracker,
SharedDomainUtils $sharedDomainUtils,
?MobileContext $mobileContext = null
) {
$this->urlUtils = $urlUtils;
$this->filteredRequestTracker = $filteredRequestTracker;
$this->sharedDomainUtils = $sharedDomainUtils;
$this->mobileContext = $mobileContext;
}
/** @inheritDoc */
public function onSetupAfterCache() {
if ( $this->sharedDomainUtils->shouldRestrictCurrentDomain() ) {
// FIXME The REST API does not provide a hook for disabling APIs. No rest APIs
// should be needed for login and signup so we can just throw unconditionally,
// but this should be improved in the future.
// FIXME should not log a production error
if ( !in_array( MW_ENTRY_POINT, self::ALLOWED_ENTRY_POINTS, true ) ) {
throw new \RuntimeException( MW_ENTRY_POINT . ' endpoint is not allowed on the SSO domain' );
}
}
}
/** @inheritDoc */
public function onGetUserPermissionsErrors( $title, $user, $action, &$result ) {
if ( $this->sharedDomainUtils->shouldRestrictCurrentDomain() ) {
if ( !$title->isSpecialPage() ) {
$result = wfMessage( 'badaccess-group0' );
return false;
}
foreach ( self::ALLOWED_SPECIAL_PAGES as $name ) {
if ( $title->isSpecial( $name ) ) {
return true;
}
}
$result = wfMessage( 'badaccess-group0' );
return false;
}
}
/** @inheritDoc */
public function onApiCheckCanExecute( $module, $user, &$message ) {
if ( $this->sharedDomainUtils->shouldRestrictCurrentDomain() ) {
if ( !in_array( $module->getModuleName(), self::ALLOWED_API_MODULES ) ) {
$message = [ 'apierror-moduledisabled', $module->getModuleName() ];
return false;
}
}
}
/**
* @inheritDoc
* @phan-param array{local:string} $urls
*/
public function onResourceLoaderModifyEmbeddedSourceUrls( array &$urls ): void {
$local = $urls['local'];
$local = $this->urlUtils->expand( $local, PROTO_CURRENT );
// reassure Phan that expand() won't return null
'@phan-var string $local';
if ( $this->mobileContext && $this->mobileContext->usingMobileDomain() ) {
$local = $this->mobileContext->getMobileUrl( $local );
}
$urls['local'] = $local;
}
/**
* If we are not on the shared domain and SUL3 is enabled, remove some authentication
* providers. They will run after the redirect on shared domain, so it's not necessary to
* run them locally, and on the local domain they would generate a login form, and we
* don't want that.
* @inheritDoc
* @note
*/
public function onAuthManagerFilterProviders( array &$providers ): void {
$request = RequestContext::getMain()->getRequest();
if ( $this->sharedDomainUtils->isSul3Enabled( $request )
&& !$this->sharedDomainUtils->isSharedDomain()
) {
// We'll rely on CentralAuthSsoPreAuthenticationProvider to make sure filtering does not
// happen at the wrong time so make sure it's in place.
if ( !isset( $providers['preauth']['CentralAuthSsoPreAuthenticationProvider'] ) ) {
throw new LogicException( 'CentralAuthSsoPreAuthenticationProvider not found during SUL3 login' );
}
foreach ( self::DISALLOWED_LOCAL_PROVIDERS as $stage => $disallowedProviders ) {
foreach ( $disallowedProviders as $disallowedProvider ) {
unset( $providers[$stage][$disallowedProvider] );
}
}
// This is security-critical code. If these providers are removed but some
// non-redirect-based login is still possible, or the providers are erroneously
// removed on the shared domain as well, that would circumvent important security
// checks. To prevent mistakes, we sync with the behavior of the
// AuthManagerVerifyAuthentication hook.
$this->filteredRequestTracker->markRequestAsFiltered( $request );
}
}
/** @inheritDoc */
public function onAuthManagerVerifyAuthentication(
?UserIdentity $user,
AuthenticationResponse &$response,
AuthManager $authManager,
array $info
) {
if ( $this->filteredRequestTracker->isCurrentAuthenticationFlowFiltered( $authManager )
&& $info['primaryId'] !== CentralAuthRedirectingPrimaryAuthenticationProvider::class
) {
// If providers were filtered, but then authentication wasn't handled by redirecting,
// report and interrupt.
MWExceptionHandler::logException( new NormalizedException(
'Providers were filtered but redirecting provider was not the primary',
[
'user' => $user->getName(),
'result' => $response->status,
] + $info
) );
$response = AuthenticationResponse::newFail( wfMessage( 'internalerror' ) );
return false;
}
}
/** @inheritDoc */
public function onGetLocalURL( $title, &$url, $query ) {
if ( $this->sharedDomainUtils->shouldRestrictCurrentDomain() ) {
// Only allow links to auth-related special pages on the SSO domain.
// Point all other links to the normal wiki domain.
foreach ( self::ALLOWED_SPECIAL_PAGES as $name ) {
if ( $title->isSpecial( $name ) ) {
return;
}
}
if ( $title->getInterwiki() !== '' ) {
return;
}
// WikiMap entry for the current wiki points to the normal wiki domain,
// even when $wgServer etc. were overridden for the SSO domain.
$currentWiki = WikiMap::getWiki( WikiMap::getCurrentWikiId() );
$url = wfAppendQuery( $currentWiki->getCanonicalUrl( $title->getPrefixedText() ), $query );
if ( $this->mobileContext && $this->mobileContext->shouldDisplayMobileView() ) {
$url = $this->mobileContext->getMobileUrl( $url );
}
}
}
}

File Metadata

Mime Type
text/x-php
Expires
Aug 18 2026, 07:47 (7 w, 3 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
bc/76/92060a23a60bd88bdf2217e09106
Default Alt Text
SsoHookHandler.php (9 KB)

Event Timeline