Merge pull request #19154 from MauricioFauth/auth-plugin-response-handling

Remove ResponseRenderer::callExit() calls from auth plugins
This commit is contained in:
Maurício Meneghini Fauth 2024-05-11 17:55:00 -03:00 committed by GitHub
commit b93d1b9b52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 473 additions and 375 deletions

View File

@ -2235,9 +2235,10 @@ logs. Currently there are two variables available:
User name of currently active user (they do not have to be logged in).
``userStatus``
Status of currently active user, one of ``ok`` (user is logged in),
``mysql-denied`` (MySQL denied user login), ``allow-denied`` (user denied
``server-denied`` (database server denied user login), ``allow-denied`` (user denied
by allow/deny rules), ``root-denied`` (root is denied in configuration),
``empty-denied`` (empty password is denied).
``empty-denied`` (empty password is denied),
``no-activity`` (automatically logged out due to inactivity).
``LogFormat`` directive for Apache can look like following:

View File

@ -7672,6 +7672,7 @@
<code><![CDATA[$password]]></code>
</PossiblyInvalidPropertyAssignmentValue>
<PossiblyUnusedReturnValue>
<code><![CDATA[Response]]></code>
<code><![CDATA[bool]]></code>
</PossiblyUnusedReturnValue>
<RedundantCast>
@ -7761,6 +7762,9 @@
<code><![CDATA[$config->selectedServer]]></code>
<code><![CDATA[array_merge($config->selectedServer, $singleSignonCfgUpdate)]]></code>
</MixedPropertyTypeCoercion>
<PossiblyUnusedReturnValue>
<code><![CDATA[Response]]></code>
</PossiblyUnusedReturnValue>
<RiskyTruthyFalsyComparison>
<code><![CDATA[empty($config->selectedServer['SignonURL'])]]></code>
</RiskyTruthyFalsyComparison>
@ -14310,6 +14314,7 @@
<code><![CDATA[$config->settings]]></code>
<code><![CDATA[$config->settings]]></code>
<code><![CDATA[$config->settings]]></code>
<code><![CDATA[$config->settings]]></code>
</PropertyTypeCoercion>
<TypeDoesNotContainType>
<code><![CDATA[assertSame]]></code>

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Exceptions;
use RuntimeException;
use Throwable;
use function __;
final class AuthenticationFailure extends RuntimeException
{
public const SERVER_DENIED = 'server-denied';
public const ALLOW_DENIED = 'allow-denied';
public const ROOT_DENIED = 'root-denied';
public const EMPTY_DENIED = 'empty-denied';
public const NO_ACTIVITY = 'no-activity';
/** @psalm-param self::* $failureType */
public function __construct(
public readonly string $failureType,
string $message = '',
int $code = 0,
Throwable|null $previous = null,
) {
parent::__construct($message, $code, $previous);
}
/**
* Database server denied user login
*/
public static function deniedByDatabaseServer(): self
{
return new self(self::SERVER_DENIED, __('Cannot log in to the database server.'));
}
/**
* User denied by allow/deny rules
*/
public static function deniedByAllowDenyRules(): self
{
return new self(self::ALLOW_DENIED, __('Access denied!'));
}
/**
* User 'root' is denied in configuration
*/
public static function rootDeniedByConfiguration(): self
{
return new self(self::ROOT_DENIED, __('Access denied!'));
}
/**
* Empty password is denied
*/
public static function emptyPasswordDeniedByConfiguration(): self
{
return new self(
self::EMPTY_DENIED,
__('Login without a password is forbidden by configuration (see AllowNoPassword).'),
);
}
/**
* Automatically logged out due to inactivity
*/
public static function loggedOutDueToInactivity(): self
{
return new self(
self::NO_ACTIVITY,
__(
'You have been automatically logged out due to inactivity of %s seconds.'
. ' Once you log in again, you should be able to resume the work where you left off.',
),
);
}
}

View File

@ -11,13 +11,13 @@ use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Container\ContainerBuilder;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Dbal\ConnectionType;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PhpMyAdmin\Exceptions\AuthenticationPluginException;
use PhpMyAdmin\Exceptions\ExitException;
use PhpMyAdmin\Http\Factory\ResponseFactory;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\LanguageManager;
use PhpMyAdmin\Logging;
use PhpMyAdmin\Plugins\AuthenticationPlugin;
use PhpMyAdmin\Plugins\AuthenticationPluginFactory;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
@ -26,6 +26,7 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use function assert;
use function define;
@ -60,7 +61,23 @@ final class Authentication implements MiddlewareInterface
}
try {
$authPlugin->authenticate();
try {
$response = $authPlugin->authenticate();
if ($response !== null) {
return $response;
}
} catch (AuthenticationFailure $exception) {
return $authPlugin->showFailure($exception);
} catch (Throwable $exception) {
$response = $this->responseFactory->createResponse(StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
return $response->write($this->template->render('error/generic', [
'lang' => $GLOBALS['lang'] ?? 'en',
'dir' => LanguageManager::$textDir,
'error_message' => $exception->getMessage(),
]));
}
$currentServer = new Server(Config::getInstance()->selectedServer);
/* Enable LOAD DATA LOCAL INFILE for LDI plugin */
@ -71,7 +88,11 @@ final class Authentication implements MiddlewareInterface
// phpcs:enable
}
$this->connectToDatabaseServer(DatabaseInterface::getInstance(), $authPlugin, $currentServer);
try {
$this->connectToDatabaseServer(DatabaseInterface::getInstance(), $currentServer);
} catch (AuthenticationFailure $exception) {
return $authPlugin->showFailure($exception);
}
// Relation should only be initialized after the connection is successful
/** @var Relation $relation */
@ -81,9 +102,16 @@ final class Authentication implements MiddlewareInterface
// Tracker can only be activated after the relation has been initialized
Tracker::enable();
$authPlugin->rememberCredentials();
$response = $authPlugin->rememberCredentials();
if ($response !== null) {
return $response;
}
assert($request instanceof ServerRequest);
$authPlugin->checkTwoFactor($request);
$response = $authPlugin->checkTwoFactor($request);
if ($response !== null) {
return $response;
}
} catch (ExitException) {
return ResponseRenderer::getInstance()->response();
}
@ -94,11 +122,9 @@ final class Authentication implements MiddlewareInterface
return $handler->handle($request);
}
private function connectToDatabaseServer(
DatabaseInterface $dbi,
AuthenticationPlugin $auth,
Server $currentServer,
): void {
/** @throws AuthenticationFailure */
private function connectToDatabaseServer(DatabaseInterface $dbi, Server $currentServer): void
{
/**
* Try to connect MySQL with the control user profile (will be used to get the privileges list for the current
* user but the true user link must be open after this one, so it would be default one for all the scripts).
@ -111,7 +137,7 @@ final class Authentication implements MiddlewareInterface
// Connects to the server (validates user's login)
$userConnection = $dbi->connect($currentServer, ConnectionType::User);
if ($userConnection === null) {
$auth->showFailure('mysql-denied');
throw AuthenticationFailure::deniedByDatabaseServer();
}
if ($controlConnection !== null) {

View File

@ -10,7 +10,9 @@ namespace PhpMyAdmin\Plugins\Auth;
use PhpMyAdmin\Config;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Error\ErrorHandler;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Http\Response;
use PhpMyAdmin\Plugins\AuthenticationPlugin;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Server\Select;
@ -18,6 +20,8 @@ use PhpMyAdmin\Util;
use function __;
use function count;
use function ob_get_clean;
use function ob_start;
use function sprintf;
use function trigger_error;
@ -32,17 +36,18 @@ class AuthenticationConfig extends AuthenticationPlugin
/**
* Displays authentication form
*/
public function showLoginForm(): void
public function showLoginForm(): Response|null
{
$response = ResponseRenderer::getInstance();
if (! $response->isAjax()) {
return;
$responseRenderer = ResponseRenderer::getInstance();
if (! $responseRenderer->isAjax()) {
return null;
}
$response->setRequestStatus(false);
$responseRenderer->setRequestStatus(false);
// reload_flag removes the token parameter from the URL and reloads
$response->addJSON('reload_flag', '1');
$response->callExit();
$responseRenderer->addJSON('reload_flag', '1');
return $responseRenderer->response();
}
/**
@ -65,12 +70,10 @@ class AuthenticationConfig extends AuthenticationPlugin
/**
* User is not allowed to login to MySQL -> authentication failed
*
* @param string $failure String describing why authentication has failed
*/
public function showFailure(string $failure): never
public function showFailure(AuthenticationFailure $failure): Response
{
parent::showFailure($failure);
$this->logFailure($failure);
$connError = DatabaseInterface::getInstance()->getError();
if ($connError === '' || $connError === '0') {
@ -78,12 +81,14 @@ class AuthenticationConfig extends AuthenticationPlugin
}
/* HTML header */
$response = ResponseRenderer::getInstance();
$response->setMinimalFooter();
$header = $response->getHeader();
$responseRenderer = ResponseRenderer::getInstance();
$responseRenderer->setMinimalFooter();
$header = $responseRenderer->getHeader();
$header->setBodyId('loginform');
$header->setTitle(__('Access denied!'));
$header->disableMenuAndConsole();
ob_start();
echo '<br><br>
<div class="text-center">
<h1>';
@ -95,8 +100,8 @@ class AuthenticationConfig extends AuthenticationPlugin
<tr>
<td>';
$config = Config::getInstance();
if ($failure === 'allow-denied') {
trigger_error(__('Access denied!'), E_USER_NOTICE);
if ($failure->failureType === AuthenticationFailure::ALLOW_DENIED) {
trigger_error($failure->getMessage(), E_USER_NOTICE);
} else {
// Check whether user has configured something
if ($config->sourceMtime == 0) {
@ -158,6 +163,9 @@ class AuthenticationConfig extends AuthenticationPlugin
}
echo '</table>' , "\n";
$response->callExit();
$responseRenderer->addHTML((string) ob_get_clean());
return $responseRenderer->response();
}
}

View File

@ -11,14 +11,15 @@ use PhpMyAdmin\Config;
use PhpMyAdmin\Core;
use PhpMyAdmin\Current;
use PhpMyAdmin\Error\ErrorHandler;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PhpMyAdmin\Exceptions\SessionHandlerException;
use PhpMyAdmin\Http\Response;
use PhpMyAdmin\LanguageManager;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins\AuthenticationPlugin;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Server\Select;
use PhpMyAdmin\Session;
use PhpMyAdmin\Template;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use PhpMyAdmin\Utils\SessionCache;
@ -63,11 +64,11 @@ class AuthenticationCookie extends AuthenticationPlugin
*
* @global string $conn_error the last connection error
*/
public function showLoginForm(): never
public function showLoginForm(): Response
{
$GLOBALS['conn_error'] ??= null;
$response = ResponseRenderer::getInstance();
$responseRenderer = ResponseRenderer::getInstance();
/**
* When sending login modal after session has expired, send the
@ -75,8 +76,8 @@ class AuthenticationCookie extends AuthenticationPlugin
* in all the forms having a hidden token.
*/
$sessionExpired = isset($_REQUEST['check_timeout']) || isset($_REQUEST['session_timedout']);
if (! $sessionExpired && $response->loginPage()) {
$response->callExit();
if (! $sessionExpired && $responseRenderer->loginPage()) {
return $responseRenderer->response();
}
/**
@ -85,8 +86,8 @@ class AuthenticationCookie extends AuthenticationPlugin
* in all the forms having a hidden token.
*/
if ($sessionExpired) {
$response->setRequestStatus(false);
$response->addJSON('new_token', $_SESSION[' PMA_token ']);
$responseRenderer->setRequestStatus(false);
$responseRenderer->addJSON('new_token', $_SESSION[' PMA_token ']);
}
/**
@ -94,7 +95,7 @@ class AuthenticationCookie extends AuthenticationPlugin
* using the modal was successful after session expiration.
*/
if (isset($_REQUEST['session_timedout'])) {
$response->addJSON('logged_in', 0);
$responseRenderer->addJSON('logged_in', 0);
}
$config = Config::getInstance();
@ -159,7 +160,7 @@ class AuthenticationCookie extends AuthenticationPlugin
$configFooter = Config::renderFooter();
$response->addHTML($this->template->render('login/form', [
$responseRenderer->addHTML($this->template->render('login/form', [
'login_header' => $loginHeader,
'is_demo' => $config->config->debug->demo,
'error_messages' => $errorMessages,
@ -190,7 +191,7 @@ class AuthenticationCookie extends AuthenticationPlugin
'config_footer' => $configFooter,
]));
$response->callExit();
return $responseRenderer->response();
}
/**
@ -206,6 +207,9 @@ class AuthenticationCookie extends AuthenticationPlugin
* it returns true if all seems ok which usually leads to auth_set_user()
*
* it directly switches to showFailure() if user inactivity timeout is reached
*
* @throws AuthenticationFailure
* @throws SessionHandlerException
*/
public function readCredentials(): bool
{
@ -309,19 +313,8 @@ class AuthenticationCookie extends AuthenticationPlugin
$GLOBALS['pma_auth_server'] = Core::sanitizeMySQLHost($_REQUEST['pma_servername']);
}
try {
/* Secure current session on login to avoid session fixation */
Session::secure();
} catch (SessionHandlerException $exception) {
$responseRenderer = ResponseRenderer::getInstance();
$responseRenderer->addHTML((new Template())->render('error/generic', [
'lang' => $GLOBALS['lang'] ?? 'en',
'dir' => LanguageManager::$textDir,
'error_message' => $exception->getMessage(),
]));
$responseRenderer->callExit();
}
/* Secure current session on login to avoid session fixation */
Session::secure();
return true;
}
@ -371,7 +364,7 @@ class AuthenticationCookie extends AuthenticationPlugin
SessionCache::remove('table_priv');
SessionCache::remove('proc_priv');
$this->showFailure('no-activity');
throw AuthenticationFailure::loggedOutDueToInactivity();
}
// check password cookie
@ -440,7 +433,7 @@ class AuthenticationCookie extends AuthenticationPlugin
/**
* Stores user credentials after successful login.
*/
public function rememberCredentials(): void
public function rememberCredentials(): Response|null
{
// Name and password cookies need to be refreshed each time
// Duration = one month for username
@ -465,18 +458,18 @@ class AuthenticationCookie extends AuthenticationPlugin
// user logged in successfully after session expiration
if (isset($_REQUEST['session_timedout'])) {
$response = ResponseRenderer::getInstance();
$response->addJSON('logged_in', 1);
$response->addJSON('success', 1);
$response->addJSON('new_token', $_SESSION[' PMA_token ']);
$responseRenderer = ResponseRenderer::getInstance();
$responseRenderer->addJSON('logged_in', 1);
$responseRenderer->addJSON('success', 1);
$responseRenderer->addJSON('new_token', $_SESSION[' PMA_token ']);
$response->callExit();
return $responseRenderer->response();
}
// Set server cookies if required (once per session) and, in this case,
// force reload to ensure the client accepts cookies
if ($GLOBALS['from_cookie']) {
return;
return null;
}
/**
@ -484,11 +477,11 @@ class AuthenticationCookie extends AuthenticationPlugin
*/
Util::clearUserCache();
$response = ResponseRenderer::getInstance();
$response->disable();
$response->redirect('./index.php?route=/' . Url::getCommonRaw($urlParams, '&'));
$responseRenderer = ResponseRenderer::getInstance();
$responseRenderer->disable();
$responseRenderer->redirect('./index.php?route=/' . Url::getCommonRaw($urlParams, '&'));
$response->callExit();
return $responseRenderer->response();
}
/**
@ -539,27 +532,23 @@ class AuthenticationCookie extends AuthenticationPlugin
*
* prepares error message and switches to showLoginForm() which display the error
* and the login form
*
* @param string $failure String describing why authentication has failed
*/
public function showFailure(string $failure): never
public function showFailure(AuthenticationFailure $failure): Response
{
$GLOBALS['conn_error'] ??= null;
parent::showFailure($failure);
$this->logFailure($failure);
// Deletes password cookie and displays the login form
Config::getInstance()->removeCookie('pmaAuth-' . Current::$server);
$GLOBALS['conn_error'] = $this->getErrorMessage($failure);
$response = ResponseRenderer::getInstance();
$responseRenderer = ResponseRenderer::getInstance();
// needed for PHP-CGI (not need for FastCGI or mod-php)
$response->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
$response->addHeader('Pragma', 'no-cache');
$responseRenderer->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
$responseRenderer->addHeader('Pragma', 'no-cache');
$this->showLoginForm();
return $this->showLoginForm();
}
/**

View File

@ -12,6 +12,8 @@ use Fig\Http\Message\StatusCodeInterface;
use PhpMyAdmin\Config;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PhpMyAdmin\Http\Response;
use PhpMyAdmin\LanguageManager;
use PhpMyAdmin\Message;
use PhpMyAdmin\Plugins\AuthenticationPlugin;
@ -34,23 +36,24 @@ class AuthenticationHttp extends AuthenticationPlugin
/**
* Displays authentication form and redirect as necessary
*/
public function showLoginForm(): never
public function showLoginForm(): Response
{
$response = ResponseRenderer::getInstance();
if ($response->isAjax()) {
$response->setRequestStatus(false);
$responseRenderer = ResponseRenderer::getInstance();
if ($responseRenderer->isAjax()) {
$responseRenderer->setRequestStatus(false);
// reload_flag removes the token parameter from the URL and reloads
$response->addJSON('reload_flag', '1');
$response->callExit();
$responseRenderer->addJSON('reload_flag', '1');
return $responseRenderer->response();
}
$this->authForm();
return $this->authForm();
}
/**
* Displays authentication form
*/
public function authForm(): never
public function authForm(): Response
{
$config = Config::getInstance();
if (empty($config->selectedServer['auth_http_realm'])) {
@ -92,7 +95,7 @@ class AuthenticationHttp extends AuthenticationPlugin
$response->addHTML(Config::renderFooter());
$response->callExit();
return $response->response();
}
/**
@ -178,25 +181,24 @@ class AuthenticationHttp extends AuthenticationPlugin
/**
* User is not allowed to login to MySQL -> authentication failed
*
* @param string $failure String describing why authentication has failed
*/
public function showFailure(string $failure): never
public function showFailure(AuthenticationFailure $failure): Response
{
parent::showFailure($failure);
$this->logFailure($failure);
$error = DatabaseInterface::getInstance()->getError();
if ($error && $GLOBALS['errno'] != 1045) {
echo $this->template->render('error/generic', [
$responseRenderer = ResponseRenderer::getInstance();
$responseRenderer->addHTML($this->template->render('error/generic', [
'lang' => $GLOBALS['lang'] ?? 'en',
'dir' => LanguageManager::$textDir,
'error_message' => $error,
]);
]));
ResponseRenderer::getInstance()->callExit();
return $responseRenderer->response();
}
$this->authForm();
return $this->authForm();
}
/**

View File

@ -8,10 +8,13 @@ declare(strict_types=1);
namespace PhpMyAdmin\Plugins\Auth;
use PhpMyAdmin\Config;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PhpMyAdmin\Http\Response;
use PhpMyAdmin\LanguageManager;
use PhpMyAdmin\Plugins\AuthenticationPlugin;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Util;
use RuntimeException;
use function __;
use function array_merge;
@ -24,6 +27,7 @@ use function session_name;
use function session_set_cookie_params;
use function session_start;
use function session_write_close;
use function sprintf;
/**
* Handles the SignOn authentication method
@ -33,25 +37,25 @@ class AuthenticationSignon extends AuthenticationPlugin
/**
* Displays authentication form
*/
public function showLoginForm(): never
public function showLoginForm(): Response
{
$response = ResponseRenderer::getInstance();
$response->disable();
$responseRenderer = ResponseRenderer::getInstance();
$responseRenderer->disable();
unset($_SESSION['LAST_SIGNON_URL']);
$config = Config::getInstance();
if (empty($config->selectedServer['SignonURL'])) {
echo $this->template->render('error/generic', [
$responseRenderer->addHTML($this->template->render('error/generic', [
'lang' => $GLOBALS['lang'] ?? 'en',
'dir' => LanguageManager::$textDir,
'error_message' => 'You must set SignonURL!',
]);
]));
$response->callExit();
} else {
$response->redirect($config->selectedServer['SignonURL']);
return $responseRenderer->response();
}
$response->callExit();
$responseRenderer->redirect($config->selectedServer['SignonURL']);
return $responseRenderer->response();
}
/**
@ -90,6 +94,8 @@ class AuthenticationSignon extends AuthenticationPlugin
/**
* Gets authentication credentials
*
* @throws RuntimeException
*/
public function readCredentials(): bool
{
@ -118,13 +124,10 @@ class AuthenticationSignon extends AuthenticationPlugin
/* Handle script based auth */
if ($scriptName !== '') {
if (! @file_exists($scriptName)) {
echo $this->template->render('error/generic', [
'lang' => $GLOBALS['lang'] ?? 'en',
'dir' => LanguageManager::$textDir,
'error_message' => __('Can not find signon authentication script:') . ' ' . $scriptName,
]);
ResponseRenderer::getInstance()->callExit();
throw new RuntimeException(sprintf(
__('Can not find signon authentication script: %s'),
'$cfg[\'Servers\'][$i][\'SignonScript\']',
));
}
include $scriptName;
@ -233,12 +236,10 @@ class AuthenticationSignon extends AuthenticationPlugin
/**
* User is not allowed to login to MySQL -> authentication failed
*
* @param string $failure String describing why authentication has failed
*/
public function showFailure(string $failure): never
public function showFailure(AuthenticationFailure $failure): Response
{
parent::showFailure($failure);
$this->logFailure($failure);
/* Session name */
$sessionName = Config::getInstance()->selectedServer['SignonSession'];
@ -260,7 +261,7 @@ class AuthenticationSignon extends AuthenticationPlugin
$_SESSION['PMA_single_signon_error_message'] = $this->getErrorMessage($failure);
}
$this->showLoginForm();
return $this->showLoginForm();
}
/**

View File

@ -1,19 +1,16 @@
<?php
/**
* Abstract class for the authentication plugins
*/
declare(strict_types=1);
namespace PhpMyAdmin\Plugins;
use Exception;
use PhpMyAdmin\Config;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Exceptions\ExitException;
use PhpMyAdmin\Exceptions\SessionHandlerException;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PhpMyAdmin\Http\Response;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\IpAllowDeny;
use PhpMyAdmin\LanguageManager;
use PhpMyAdmin\Logging;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
@ -35,8 +32,7 @@ use function sprintf;
use function time;
/**
* Provides a common interface that will have to be implemented by all of the
* authentication plugins.
* Provides a common interface that will have to be implemented by all the authentication plugins.
*/
abstract class AuthenticationPlugin
{
@ -63,10 +59,13 @@ abstract class AuthenticationPlugin
/**
* Displays authentication form
*/
abstract public function showLoginForm(): void;
abstract public function showLoginForm(): Response|null;
/**
* Gets authentication credentials
*
* @throws AuthenticationFailure
* @throws Exception
*/
abstract public function readCredentials(): bool;
@ -87,18 +86,19 @@ abstract class AuthenticationPlugin
/**
* Stores user credentials after successful login.
*/
public function rememberCredentials(): void
public function rememberCredentials(): Response|null
{
return null;
}
/**
* User is not allowed to login to MySQL -> authentication failed
*
* @param string $failure String describing why authentication has failed
*/
public function showFailure(string $failure): void
abstract public function showFailure(AuthenticationFailure $failure): Response;
protected function logFailure(AuthenticationFailure $failure): void
{
Logging::logUser(Config::getInstance(), $this->user, $failure);
Logging::logUser(Config::getInstance(), $this->user, $failure->failureType);
}
/**
@ -157,38 +157,25 @@ abstract class AuthenticationPlugin
/**
* Returns error message for failed authentication.
*
* @param string $failure String describing why authentication has failed
*/
public function getErrorMessage(string $failure): string
public function getErrorMessage(AuthenticationFailure $failure): string
{
if ($failure === 'empty-denied') {
return __('Login without a password is forbidden by configuration (see AllowNoPassword)');
if ($failure->failureType === AuthenticationFailure::NO_ACTIVITY) {
return sprintf($failure->getMessage(), (int) Config::getInstance()->settings['LoginCookieValidity']);
}
if ($failure === 'root-denied' || $failure === 'allow-denied') {
return __('Access denied!');
if ($failure->failureType === AuthenticationFailure::SERVER_DENIED) {
$dbiError = DatabaseInterface::getInstance()->getError();
if ($dbiError !== '') {
return htmlspecialchars($dbiError);
}
if (isset($GLOBALS['errno'])) {
return '#' . $GLOBALS['errno'] . ' ' . $failure->getMessage();
}
}
if ($failure === 'no-activity') {
return sprintf(
__('You have been automatically logged out due to inactivity of %s seconds.'
. ' Once you log in again, you should be able to resume the work where you left off.'),
(int) Config::getInstance()->settings['LoginCookieValidity'],
);
}
$dbiError = DatabaseInterface::getInstance()->getError();
if ($dbiError !== '') {
return htmlspecialchars($dbiError);
}
if (isset($GLOBALS['errno'])) {
return '#' . $GLOBALS['errno'] . ' '
. __('Cannot log in to the MySQL server');
}
return __('Cannot log in to the MySQL server');
return $failure->getMessage();
}
/**
@ -235,28 +222,23 @@ abstract class AuthenticationPlugin
* High level authentication interface
*
* Gets the credentials or shows login form if necessary
*
* @throws AuthenticationFailure
* @throws Exception
*/
public function authenticate(): void
public function authenticate(): Response|null
{
$success = $this->readCredentials();
/* Show login form (this exits) */
if (! $success) {
/* Force generating of new session */
try {
Session::secure();
} catch (SessionHandlerException $exception) {
$responseRenderer = ResponseRenderer::getInstance();
$responseRenderer->addHTML((new Template())->render('error/generic', [
'lang' => $GLOBALS['lang'] ?? 'en',
'dir' => LanguageManager::$textDir,
'error_message' => $exception->getMessage(),
]));
Session::secure();
$responseRenderer->callExit();
$response = $this->showLoginForm();
if ($response !== null) {
return $response;
}
$this->showLoginForm();
}
/* Store credentials (eg. in cookies) */
@ -265,10 +247,14 @@ abstract class AuthenticationPlugin
$this->checkRules();
/* clear user cache */
Util::clearUserCache();
return null;
}
/**
* Check configuration defined restrictions for authentication
*
* @throws AuthenticationFailure
*/
public function checkRules(): void
{
@ -287,13 +273,13 @@ abstract class AuthenticationPlugin
// Ejects the user if banished
if ($allowDenyForbidden) {
$this->showFailure('allow-denied');
throw AuthenticationFailure::deniedByAllowDenyRules();
}
}
// is root allowed?
if (! $config->selectedServer['AllowRoot'] && $config->selectedServer['user'] === 'root') {
$this->showFailure('root-denied');
throw AuthenticationFailure::rootDeniedByConfiguration();
}
// is a login without password allowed?
@ -301,39 +287,37 @@ abstract class AuthenticationPlugin
return;
}
$this->showFailure('empty-denied');
throw AuthenticationFailure::emptyPasswordDeniedByConfiguration();
}
/**
* Checks whether two factor authentication is active
* for given user and performs it.
*
* @throws ExitException
* Checks whether two-factor authentication is active for given user and performs it.
*/
public function checkTwoFactor(ServerRequest $request): void
public function checkTwoFactor(ServerRequest $request): Response|null
{
$twofactor = new TwoFactor($this->user);
/* Do we need to show the form? */
if ($twofactor->check($request)) {
return;
return null;
}
$response = ResponseRenderer::getInstance();
if ($response->loginPage()) {
$response->callExit();
$responseRenderer = ResponseRenderer::getInstance();
if ($responseRenderer->loginPage()) {
return $responseRenderer->response();
}
$response->addHTML($this->template->render('login/header', ['session_expired' => false]));
$response->addHTML(Message::rawNotice(
$responseRenderer->addHTML($this->template->render('login/header', ['session_expired' => false]));
$responseRenderer->addHTML(Message::rawNotice(
__('You have enabled two factor authentication, please confirm your login.'),
)->getDisplay());
$response->addHTML($this->template->render('login/twofactor', [
$responseRenderer->addHTML($this->template->render('login/twofactor', [
'form' => $twofactor->render($request),
'show_submit' => $twofactor->showSubmit(),
]));
$response->addHTML($this->template->render('login/footer'));
$response->addHTML(Config::renderFooter());
$response->callExit();
$responseRenderer->addHTML($this->template->render('login/footer'));
$responseRenderer->addHTML(Config::renderFooter());
return $responseRenderer->response();
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Tests\Exceptions;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(AuthenticationFailure::class)]
final class AuthenticationFailureTest extends TestCase
{
public function testAllowDenied(): void
{
$exception = AuthenticationFailure::deniedByAllowDenyRules();
self::assertSame('allow-denied', $exception->failureType);
self::assertSame('Access denied!', $exception->getMessage());
}
public function testEmptyDenied(): void
{
$exception = AuthenticationFailure::emptyPasswordDeniedByConfiguration();
self::assertSame('empty-denied', $exception->failureType);
self::assertSame(
'Login without a password is forbidden by configuration (see AllowNoPassword).',
$exception->getMessage(),
);
}
public function testNoActivity(): void
{
$exception = AuthenticationFailure::loggedOutDueToInactivity();
self::assertSame('no-activity', $exception->failureType);
self::assertSame(
'You have been automatically logged out due to inactivity of %s seconds.'
. ' Once you log in again, you should be able to resume the work where you left off.',
$exception->getMessage(),
);
}
public function testRootDenied(): void
{
$exception = AuthenticationFailure::rootDeniedByConfiguration();
self::assertSame('root-denied', $exception->failureType);
self::assertSame('Access denied!', $exception->getMessage());
}
public function testServerDenied(): void
{
$exception = AuthenticationFailure::deniedByDatabaseServer();
self::assertSame('server-denied', $exception->failureType);
self::assertSame('Cannot log in to the database server.', $exception->getMessage());
}
}

View File

@ -7,17 +7,15 @@ namespace PhpMyAdmin\Tests\Plugins\Auth;
use PhpMyAdmin\Config;
use PhpMyAdmin\Current;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Exceptions\ExitException;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PhpMyAdmin\Plugins\Auth\AuthenticationConfig;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Tests\AbstractTestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Medium;
use ReflectionProperty;
use Throwable;
use function ob_get_clean;
use function ob_start;
use function json_decode;
#[CoversClass(AuthenticationConfig::class)]
#[Medium]
@ -55,12 +53,25 @@ class AuthenticationConfigTest extends AbstractTestCase
unset($this->object);
}
public function testAuth(): void
public function testShowLoginFormWithoutAjax(): void
{
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
ResponseRenderer::getInstance()->setAjax(false);
self::assertNull($this->object->showLoginForm());
}
public function testShowLoginFormWithAjax(): void
{
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
ResponseRenderer::getInstance()->setAjax(true);
$this->expectException(ExitException::class);
$this->object->showLoginForm();
$response = $this->object->showLoginForm();
self::assertNotNull($response);
$body = (string) $response->getBody();
self::assertJson($body);
$json = json_decode($body, true);
self::assertIsArray($json);
self::assertArrayHasKey('reload_flag', $json);
self::assertSame('1', $json['reload_flag']);
}
public function testAuthCheck(): void
@ -89,17 +100,9 @@ class AuthenticationConfigTest extends AbstractTestCase
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
ob_start();
try {
$this->object->showFailure('');
} catch (Throwable $throwable) {
}
$response = $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
$html = ob_get_clean();
self::assertInstanceOf(ExitException::class, $throwable);
self::assertIsString($html);
$html = (string) $response->getBody();
self::assertStringContainsString(
'You probably did not create a configuration file. You might want ' .

View File

@ -4,10 +4,11 @@ declare(strict_types=1);
namespace PhpMyAdmin\Tests\Plugins\Auth;
use Fig\Http\Message\StatusCodeInterface;
use PhpMyAdmin\Config;
use PhpMyAdmin\Current;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Error\ErrorHandler;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PhpMyAdmin\Exceptions\ExitException;
use PhpMyAdmin\Plugins\Auth\AuthenticationCookie;
use PhpMyAdmin\ResponseRenderer;
@ -24,6 +25,7 @@ use Throwable;
use function base64_decode;
use function base64_encode;
use function is_readable;
use function json_decode;
use function json_encode;
use function mb_strlen;
use function ob_get_clean;
@ -76,37 +78,21 @@ class AuthenticationCookieTest extends AbstractTestCase
{
$GLOBALS['conn_error'] = true;
$responseStub = new ResponseRendererStub();
$responseStub->setAjax(true);
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
$responseRenderer = ResponseRenderer::getInstance();
$responseRenderer->setAjax(true);
try {
$this->object->showLoginForm();
} catch (Throwable $throwable) {
}
$response = $this->object->showLoginForm();
self::assertInstanceOf(ExitException::class, $throwable);
$response = $responseStub->getResponse();
self::assertSame(200, $response->getStatusCode());
self::assertFalse($responseStub->hasSuccessState());
self::assertSame(['redirect_flag' => '1'], $responseStub->getJSONResult());
}
private function getAuthErrorMockResponse(): void
{
// mock error handler
$mockErrorHandler = $this->getMockBuilder(ErrorHandler::class)
->disableOriginalConstructor()
->onlyMethods(['hasDisplayErrors'])
->getMock();
$mockErrorHandler->expects(self::once())
->method('hasDisplayErrors')
->with()
->willReturn(true);
ErrorHandler::$instance = $mockErrorHandler;
self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode());
$body = (string) $response->getBody();
self::assertJson($body);
$json = json_decode($body, true);
self::assertIsArray($json);
self::assertArrayHasKey('success', $json);
self::assertFalse($json['success']);
self::assertArrayHasKey('redirect_flag', $json);
self::assertSame('1', $json['redirect_flag']);
}
public function testAuthError(): void
@ -132,17 +118,11 @@ class AuthenticationCookieTest extends AbstractTestCase
Current::$table = 'testTable';
$config->settings['Servers'] = [1, 2];
$responseStub = new ResponseRendererStub();
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
try {
$this->object->showLoginForm();
} catch (Throwable $throwable) {
}
$response = $this->object->showLoginForm();
$result = $responseStub->getHTMLResult();
self::assertInstanceOf(ExitException::class, $throwable);
$result = (string) $response->getBody();
self::assertStringContainsString(' id="imLogo"', $result);
@ -202,14 +182,9 @@ class AuthenticationCookieTest extends AbstractTestCase
$responseStub = new ResponseRendererStub();
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
try {
$this->object->showLoginForm();
} catch (Throwable $throwable) {
}
$response = $this->object->showLoginForm();
$result = $responseStub->getHTMLResult();
self::assertInstanceOf(ExitException::class, $throwable);
$result = (string) $response->getBody();
self::assertStringContainsString('id="imLogo"', $result);
@ -263,14 +238,9 @@ class AuthenticationCookieTest extends AbstractTestCase
$responseStub = new ResponseRendererStub();
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
try {
$this->object->showLoginForm();
} catch (Throwable $throwable) {
}
$response = $this->object->showLoginForm();
$result = $responseStub->getHTMLResult();
self::assertInstanceOf(ExitException::class, $throwable);
$result = (string) $response->getBody();
self::assertStringContainsString('id="imLogo"', $result);
@ -569,18 +539,14 @@ class AuthenticationCookieTest extends AbstractTestCase
// mock for blowfish function
$this->object = $this->getMockBuilder(AuthenticationCookie::class)
->disableOriginalConstructor()
->onlyMethods(['showFailure', 'cookieDecrypt'])
->onlyMethods(['cookieDecrypt'])
->getMock();
$this->object->expects(self::once())
->method('cookieDecrypt')
->willReturn('testBF');
$this->object->expects(self::once())
->method('showFailure')
->willThrowException(new ExitException());
$this->expectException(ExitException::class);
$this->expectExceptionObject(AuthenticationFailure::loggedOutDueToInactivity());
$this->object->readCredentials();
}
@ -626,6 +592,7 @@ class AuthenticationCookieTest extends AbstractTestCase
$config->selectedServer['user'] = 'pmaUser';
$config->settings['Servers'][1] = $arr;
$config->settings['AllowArbitraryServer'] = true;
$config->settings['PmaAbsoluteUri'] = 'http://localhost/phpmyadmin';
$GLOBALS['pma_auth_server'] = 'b 2';
$this->object->password = 'testPW';
$config->settings['LoginCookieStore'] = 100;
@ -635,8 +602,13 @@ class AuthenticationCookieTest extends AbstractTestCase
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
$this->object->storeCredentials();
$this->expectException(ExitException::class);
$this->object->rememberCredentials();
$response = $this->object->rememberCredentials();
self::assertNotNull($response);
self::assertSame(StatusCodeInterface::STATUS_FOUND, $response->getStatusCode());
self::assertStringEndsWith(
'/phpmyadmin/index.php?route=/&db=db&table=table&lang=en',
$response->getHeaderLine('Location'),
);
}
public function testAuthFailsNoPass(): void
@ -656,11 +628,11 @@ class AuthenticationCookieTest extends AbstractTestCase
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
try {
$this->object->showFailure('empty-denied');
$this->object->showFailure(AuthenticationFailure::emptyPasswordDeniedByConfiguration());
} catch (Throwable $throwable) {
}
self::assertInstanceOf(ExitException::class, $throwable);
self::assertInstanceOf(ExitException::class, $throwable ?? null);
$response = $responseStub->getResponse();
self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
self::assertSame(['no-cache'], $response->getHeader('Pragma'));
@ -668,7 +640,7 @@ class AuthenticationCookieTest extends AbstractTestCase
self::assertSame(
$GLOBALS['conn_error'],
'Login without a password is forbidden by configuration (see AllowNoPassword)',
'Login without a password is forbidden by configuration (see AllowNoPassword).',
);
}
@ -724,11 +696,11 @@ class AuthenticationCookieTest extends AbstractTestCase
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
try {
$this->object->showFailure('allow-denied');
$this->object->showFailure(AuthenticationFailure::deniedByAllowDenyRules());
} catch (Throwable $throwable) {
}
self::assertInstanceOf(ExitException::class, $throwable);
self::assertInstanceOf(ExitException::class, $throwable ?? null);
$response = $responseStub->getResponse();
self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
self::assertSame(['no-cache'], $response->getHeader('Pragma'));
@ -756,11 +728,11 @@ class AuthenticationCookieTest extends AbstractTestCase
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
try {
$this->object->showFailure('no-activity');
$this->object->showFailure(AuthenticationFailure::loggedOutDueToInactivity());
} catch (Throwable $throwable) {
}
self::assertInstanceOf(ExitException::class, $throwable);
self::assertInstanceOf(ExitException::class, $throwable ?? null);
$response = $responseStub->getResponse();
self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
self::assertSame(['no-cache'], $response->getHeader('Pragma'));
@ -801,17 +773,17 @@ class AuthenticationCookieTest extends AbstractTestCase
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
try {
$this->object->showFailure('');
$this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
} catch (Throwable $throwable) {
}
self::assertInstanceOf(ExitException::class, $throwable);
self::assertInstanceOf(ExitException::class, $throwable ?? null);
$response = $responseStub->getResponse();
self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
self::assertSame(['no-cache'], $response->getHeader('Pragma'));
self::assertSame(200, $response->getStatusCode());
self::assertSame($GLOBALS['conn_error'], '#42 Cannot log in to the MySQL server');
self::assertSame($GLOBALS['conn_error'], '#42 Cannot log in to the database server.');
}
public function testAuthFailsErrno(): void
@ -842,17 +814,17 @@ class AuthenticationCookieTest extends AbstractTestCase
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
try {
$this->object->showFailure('');
$this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
} catch (Throwable $throwable) {
}
self::assertInstanceOf(ExitException::class, $throwable);
self::assertInstanceOf(ExitException::class, $throwable ?? null);
$response = $responseStub->getResponse();
self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
self::assertSame(['no-cache'], $response->getHeader('Pragma'));
self::assertSame(200, $response->getStatusCode());
self::assertSame($GLOBALS['conn_error'], 'Cannot log in to the MySQL server');
self::assertSame($GLOBALS['conn_error'], 'Cannot log in to the database server.');
}
public function testGetEncryptionSecretEmpty(): void
@ -954,9 +926,10 @@ class AuthenticationCookieTest extends AbstractTestCase
$_POST['pma_password'] = 'testPassword';
ob_start();
$this->object->authenticate();
$response = $this->object->authenticate();
$result = ob_get_clean();
self::assertNull($response);
/* Nothing should be printed */
self::assertSame('', $result);
@ -999,31 +972,20 @@ class AuthenticationCookieTest extends AbstractTestCase
$config->selectedServer['AllowNoPassword'] = $nopass;
$config->selectedServer['AllowDeny'] = $rules;
if ($expected !== '') {
$this->getAuthErrorMockResponse();
}
$responseStub = new ResponseRendererStub();
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
$exception = null;
try {
$this->object->checkRules();
} catch (Throwable $throwable) {
}
$result = $responseStub->getHTMLResult();
if ($expected !== '') {
self::assertInstanceOf(ExitException::class, $throwable ?? null);
} catch (AuthenticationFailure $exception) {
}
if ($expected === '') {
self::assertSame($expected, $result);
} else {
self::assertStringContainsString($expected, $result);
self::assertNull($exception, 'checkRules() should not throw an exception.');
return;
}
ErrorHandler::$instance = null;
self::assertInstanceOf(AuthenticationFailure::class, $exception);
self::assertSame($expected, $exception->failureType);
}
/** @return mixed[] */
@ -1031,9 +993,9 @@ class AuthenticationCookieTest extends AbstractTestCase
{
return [
'nopass-ok' => ['testUser', '', '1.2.3.4', true, true, [], ''],
'nopass' => ['testUser', '', '1.2.3.4', true, false, [], 'Login without a password is forbidden'],
'nopass' => ['testUser', '', '1.2.3.4', true, false, [], AuthenticationFailure::EMPTY_DENIED],
'root-ok' => ['root', 'root', '1.2.3.4', true, true, [], ''],
'root' => ['root', 'root', '1.2.3.4', false, true, [], 'Access denied!'],
'root' => ['root', 'root', '1.2.3.4', false, true, [], AuthenticationFailure::ROOT_DENIED],
'rules-deny-allow-ok' => [
'root',
'root',
@ -1050,7 +1012,7 @@ class AuthenticationCookieTest extends AbstractTestCase
true,
true,
['order' => 'deny,allow', 'rules' => ['allow root 1.2.3.4', 'deny % from all']],
'Access denied!',
AuthenticationFailure::ALLOW_DENIED,
],
'rules-allow-deny-ok' => [
'root',
@ -1068,7 +1030,7 @@ class AuthenticationCookieTest extends AbstractTestCase
true,
true,
['order' => 'allow,deny', 'rules' => ['deny user from all', 'allow root 1.2.3.4']],
'Access denied!',
AuthenticationFailure::ALLOW_DENIED,
],
'rules-explicit-ok' => [
'root',
@ -1086,7 +1048,7 @@ class AuthenticationCookieTest extends AbstractTestCase
true,
true,
['order' => 'explicit', 'rules' => ['deny user from all', 'allow root 1.2.3.4']],
'Access denied!',
AuthenticationFailure::ALLOW_DENIED,
],
];
}

View File

@ -7,7 +7,7 @@ namespace PhpMyAdmin\Tests\Plugins\Auth;
use PhpMyAdmin\Config;
use PhpMyAdmin\Current;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Exceptions\ExitException;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PhpMyAdmin\Plugins\Auth\AuthenticationHttp;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Tests\AbstractTestCase;
@ -16,11 +16,9 @@ use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Medium;
use ReflectionProperty;
use Throwable;
use function base64_encode;
use function ob_get_clean;
use function ob_start;
use function json_decode;
#[CoversClass(AuthenticationHttp::class)]
#[Medium]
@ -82,13 +80,8 @@ class AuthenticationHttpTest extends AbstractTestCase
$responseStub = new ResponseRendererStub();
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
try {
$this->object->showLoginForm();
} catch (Throwable $throwable) {
}
$response = $this->object->showLoginForm();
self::assertInstanceOf(ExitException::class, $throwable);
$response = $responseStub->getResponse();
self::assertSame(['Basic realm="phpMyAdmin verboseMessag"'], $response->getHeader('WWW-Authenticate'));
self::assertSame(401, $response->getStatusCode());
}
@ -103,13 +96,8 @@ class AuthenticationHttpTest extends AbstractTestCase
$responseStub = new ResponseRendererStub();
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
try {
$this->object->showLoginForm();
} catch (Throwable $throwable) {
}
$response = $this->object->showLoginForm();
self::assertInstanceOf(ExitException::class, $throwable);
$response = $responseStub->getResponse();
self::assertSame(['Basic realm="phpMyAdmin hst"'], $response->getHeader('WWW-Authenticate'));
self::assertSame(401, $response->getStatusCode());
}
@ -124,13 +112,8 @@ class AuthenticationHttpTest extends AbstractTestCase
$responseStub = new ResponseRendererStub();
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
try {
$this->object->showLoginForm();
} catch (Throwable $throwable) {
}
$response = $this->object->showLoginForm();
self::assertInstanceOf(ExitException::class, $throwable);
$response = $responseStub->getResponse();
self::assertSame(['Basic realm="realmmessage"'], $response->getHeader('WWW-Authenticate'));
self::assertSame(401, $response->getStatusCode());
}
@ -271,6 +254,8 @@ class AuthenticationHttpTest extends AbstractTestCase
$config = Config::getInstance();
$config->selectedServer['host'] = '';
$_REQUEST = [];
Current::$server = 0;
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
ResponseRenderer::getInstance()->setAjax(false);
$dbi = $this->getMockBuilder(DatabaseInterface::class)
@ -284,40 +269,48 @@ class AuthenticationHttpTest extends AbstractTestCase
DatabaseInterface::$instance = $dbi;
$GLOBALS['errno'] = 31;
ob_start();
try {
$this->object->showFailure('');
} catch (Throwable $throwable) {
}
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
ResponseRenderer::getInstance()->setAjax(false);
$result = ob_get_clean();
self::assertInstanceOf(ExitException::class, $throwable);
self::assertIsString($result);
$response = $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
$result = (string) $response->getBody();
self::assertStringContainsString('<p>error 123</p>', $result);
$this->object = $this->getMockBuilder(AuthenticationHttp::class)
->disableOriginalConstructor()
->onlyMethods(['authForm'])
->getMock();
$this->object->expects(self::exactly(2))
->method('authForm')
->willThrowException(new ExitException());
// case 2
$config->selectedServer['host'] = 'host';
$GLOBALS['errno'] = 1045;
try {
$this->object->showFailure('');
} catch (ExitException) {
}
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
ResponseRenderer::getInstance()->setAjax(false);
$response = $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
$result = (string) $response->getBody();
self::assertStringContainsString('Wrong username/password. Access denied.', $result);
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
ResponseRenderer::getInstance()->setAjax(false);
// case 3
$GLOBALS['errno'] = 1043;
$this->expectException(ExitException::class);
$this->object->showFailure('');
$response = $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
$result = (string) $response->getBody();
self::assertStringContainsString('Wrong username/password. Access denied.', $result);
}
public function testShowLoginFormWithAjax(): void
{
Current::$database = '';
Current::$table = '';
(new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
ResponseRenderer::getInstance()->setAjax(true);
$response = (new AuthenticationHttp())->showLoginForm();
$body = (string) $response->getBody();
self::assertJson($body);
$json = json_decode($body, true);
self::assertIsArray($json);
self::assertArrayHasKey('reload_flag', $json);
self::assertSame('1', $json['reload_flag']);
}
}

View File

@ -8,6 +8,7 @@ use PhpMyAdmin\Config;
use PhpMyAdmin\Config\Settings\Server;
use PhpMyAdmin\Current;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PhpMyAdmin\Exceptions\ExitException;
use PhpMyAdmin\Plugins\Auth\AuthenticationSignon;
use PhpMyAdmin\ResponseRenderer;
@ -15,10 +16,7 @@ use PhpMyAdmin\Tests\AbstractTestCase;
use PhpMyAdmin\Tests\Stubs\ResponseRenderer as ResponseRendererStub;
use PHPUnit\Framework\Attributes\CoversClass;
use ReflectionProperty;
use Throwable;
use function ob_get_clean;
use function ob_start;
use function session_get_cookie_params;
use function session_id;
use function session_name;
@ -61,20 +59,8 @@ class AuthenticationSignonTest extends AbstractTestCase
Config::getInstance()->selectedServer['SignonURL'] = '';
$_REQUEST = [];
ResponseRenderer::getInstance()->setAjax(false);
ob_start();
try {
$this->object->showLoginForm();
} catch (Throwable $throwable) {
}
$result = ob_get_clean();
self::assertInstanceOf(ExitException::class, $throwable);
self::assertIsString($result);
self::assertStringContainsString('You must set SignonURL!', $result);
$response = $this->object->showLoginForm();
self::assertStringContainsString('You must set SignonURL!', (string) $response->getBody());
}
public function testAuthLogoutURL(): void
@ -260,12 +246,12 @@ class AuthenticationSignonTest extends AbstractTestCase
->willThrowException(new ExitException());
try {
$this->object->showFailure('empty-denied');
$this->object->showFailure(AuthenticationFailure::emptyPasswordDeniedByConfiguration());
} catch (ExitException) {
}
self::assertSame(
'Login without a password is forbidden by configuration (see AllowNoPassword)',
'Login without a password is forbidden by configuration (see AllowNoPassword).',
$_SESSION['PMA_single_signon_error_message'],
);
}
@ -285,7 +271,7 @@ class AuthenticationSignonTest extends AbstractTestCase
->willThrowException(new ExitException());
try {
$this->object->showFailure('allow-denied');
$this->object->showFailure(AuthenticationFailure::deniedByAllowDenyRules());
} catch (ExitException) {
}
@ -310,7 +296,7 @@ class AuthenticationSignonTest extends AbstractTestCase
$config->settings['LoginCookieValidity'] = '1440';
try {
$this->object->showFailure('no-activity');
$this->object->showFailure(AuthenticationFailure::loggedOutDueToInactivity());
} catch (ExitException) {
}
@ -347,7 +333,7 @@ class AuthenticationSignonTest extends AbstractTestCase
DatabaseInterface::$instance = $dbi;
try {
$this->object->showFailure('');
$this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
} catch (ExitException) {
}
@ -380,11 +366,11 @@ class AuthenticationSignonTest extends AbstractTestCase
DatabaseInterface::$instance = $dbi;
try {
$this->object->showFailure('');
$this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
} catch (ExitException) {
}
self::assertSame('Cannot log in to the MySQL server', $_SESSION['PMA_single_signon_error_message']);
self::assertSame('Cannot log in to the database server.', $_SESSION['PMA_single_signon_error_message']);
}
public function testSetCookieParamsDefaults(): void

View File

@ -5,8 +5,10 @@ declare(strict_types=1);
namespace PhpMyAdmin\Tests\Plugins;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Exceptions\AuthenticationFailure;
use PhpMyAdmin\Exceptions\ExitException;
use PhpMyAdmin\Http\Factory\ServerRequestFactory;
use PhpMyAdmin\Http\Response;
use PhpMyAdmin\Plugins\AuthenticationPlugin;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Tests\AbstractTestCase;
@ -26,14 +28,20 @@ final class AuthenticationPluginTest extends AbstractTestCase
DatabaseInterface::$instance = $dbi;
$object = new class extends AuthenticationPlugin {
public function showLoginForm(): void
public function showLoginForm(): Response|null
{
return null;
}
public function readCredentials(): bool
{
return false;
}
public function showFailure(AuthenticationFailure $failure): Response
{
throw new ExitException();
}
};
$_SESSION['two_factor_check'] = false;
@ -45,12 +53,9 @@ final class AuthenticationPluginTest extends AbstractTestCase
$request = ServerRequestFactory::create()->createServerRequest('GET', 'http://example.com/');
$object->user = 'test_user';
try {
$object->checkTwoFactor($request);
} catch (ExitException) {
}
$response = $object->checkTwoFactor($request);
$response = $responseRenderer->response();
self::assertNotNull($response);
self::assertStringContainsString(
'You have enabled two factor authentication, please confirm your login.',
(string) $response->getBody(),