Skip to content

Commit

Permalink
Refactor token validation into service class
Browse files Browse the repository at this point in the history
  • Loading branch information
maccabeelevine committed Nov 27, 2024
1 parent 4b0a894 commit 1b73f94
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 44 deletions.
3 changes: 2 additions & 1 deletion module/VuFind/config/module.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@
'VuFind\Controller\SummonController' => 'VuFind\Controller\AbstractBaseFactory',
'VuFind\Controller\SummonrecordController' => 'VuFind\Controller\AbstractBaseFactory',
'VuFind\Controller\TagController' => 'VuFind\Controller\AbstractBaseFactory',
'VuFind\Controller\TurnstileController' => 'VuFind\Controller\AbstractBaseFactory',
'VuFind\Controller\TurnstileController' => 'VuFind\Controller\TurnstileControllerFactory',
'VuFind\Controller\UpgradeController' => 'VuFind\Controller\UpgradeControllerFactory',
'VuFind\Controller\WebController' => 'VuFind\Controller\AbstractBaseFactory',
'VuFind\Controller\WorldcatController' => 'VuFind\Controller\AbstractBaseFactory',
Expand Down Expand Up @@ -492,6 +492,7 @@
'VuFind\OAuth2\Repository\ScopeRepository' => 'VuFind\OAuth2\Repository\RepositoryWithOAuth2ConfigFactory',
'VuFind\QRCode\Loader' => 'VuFind\QRCode\LoaderFactory',
'VuFind\RateLimiter\RateLimiterManager' => 'VuFind\RateLimiter\RateLimiterManagerFactory',
'VuFind\RateLimiter\Turnstile\Turnstile' => 'VuFind\RateLimiter\Turnstile\TurnstileFactory',
'VuFind\Ratings\RatingsService' => 'VuFind\Ratings\RatingsServiceFactory',
'VuFind\Recommend\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory',
'VuFind\Record\Cache' => 'VuFind\Record\CacheFactory',
Expand Down
73 changes: 30 additions & 43 deletions module/VuFind/src/VuFind/Controller/TurnstileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@
namespace VuFind\Controller;

use Laminas\Log\LoggerAwareInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
use VuFind\Crypt\HMAC;
use VuFind\Log\LoggerAwareTrait;
use VuFindHttp\HttpServiceAwareInterface;
use VuFindHttp\HttpServiceAwareTrait;
use VuFind\RateLimiter\RateLimiterManager;
use VuFind\RateLimiter\Turnstile\Turnstile;

/**
* Controller Cloudflare Turnstile access checks.
Expand All @@ -44,14 +46,31 @@
* @link https://vufind.org Main Page
*/
class TurnstileController extends AbstractBase implements
HttpServiceAwareInterface,
LoggerAwareInterface
{
use HttpServiceAwareTrait;
use LoggerAwareTrait;

protected $hashKeys = ['siteKey', 'policyId', 'destination'];

/**
* Constructor
*
* @param ServiceLocatorInterface $sm Service locator
* @param Turnstile $turnstile Turnstile service
* @param RateLimiterManager $rateLimiterManager Rate Limiter Manager instance
* @param array $config Rate Limiter configuration
* @param HMAC $hmac HMAC service
*/
public function __construct(
ServiceLocatorInterface $sm,
protected Turnstile $turnstile,
protected RateLimiterManager $rateLimiterManager,
protected array $config,
protected HMAC $hmac
) {
parent::__construct($sm);
}

/**
* Present the Turnstile challenge to the user
*
Expand All @@ -60,14 +79,10 @@ class TurnstileController extends AbstractBase implements
public function challengeAction()
{
$context = json_decode(base64_decode($this->params()->fromQuery('context')), true);

$yamlReader = $this->getService(\VuFind\Config\YamlReader::class);
$config = $yamlReader->get('RateLimiter.yaml');
$context['siteKey'] = $config['Turnstile']['siteKey'];
$context['jsLibraryUrl'] = $config['Turnstile']['jsLibraryUrl'] ??
$context['siteKey'] = $this->config['Turnstile']['siteKey'];
$context['jsLibraryUrl'] = $this->config['Turnstile']['jsLibraryUrl'] ??
'https://challenges.cloudflare.com/turnstile/v0/api.js';
$hmac = $this->getService(\VuFind\Crypt\HMAC::class);
$context['hash'] = $hmac->generate($this->hashKeys, $context);
$context['hash'] = $this->hmac->generate($this->hashKeys, $context);

$this->layout()->searchbox = false;
return $this->createViewModel($context);
Expand All @@ -85,44 +100,16 @@ public function verifyAction()
$destination = $this->params()->fromPost('destination');
$priorHash = $this->params()->fromPost('hash');

$hmac = $this->getService(\VuFind\Crypt\HMAC::class);
$yamlReader = $this->getService(\VuFind\Config\YamlReader::class);
$config = $yamlReader->get('RateLimiter.yaml');
$siteKey = $config['Turnstile']['siteKey'];
$newHash = $hmac->generate($this->hashKeys, compact($this->hashKeys));
$siteKey = $this->config['Turnstile']['siteKey'];
$newHash = $this->hmac->generate($this->hashKeys, compact($this->hashKeys));
if ($newHash != $priorHash) {
throw new \Exception('Wrong hash value used in Turnstile verification.');
}

// Call the Turnstile verify API to validate the token
$yamlReader = $this->getService(\VuFind\Config\YamlReader::class);
$config = $yamlReader->get('RateLimiter.yaml');
$secretKey = $config['Turnstile']['secretKey'];
$url = $config['Turnstile']['verifyUrl'] ??
'https://challenges.cloudflare.com/turnstile/v0/siteverify';
$body = [
'secret' => $secretKey,
'response' => $token,
];
$response = $this->httpService->post(
$url,
json_encode($body),
'application/json'
);

if ($response->isOk()) {
$responseData = json_decode($response->getBody(), true);
$success = $responseData['success'];
} else {
// Unexpected error. Treat as a positive result, since it's not the user's fault.
$this->logWarning('Verification process failed, allowing traffic: '
. $response->getStatusCode() . $response->getBody());
$success = true;
}
$success = $this->turnstile->validateToken($token, $policyId);

// Save the Turnstile result for future requests
$rateLimiterManager = $this->getService(\VuFind\RateLimiter\RateLimiterManager::class);
$rateLimiterManager->setTurnstileResult(
$this->rateLimiterManager->setTurnstileResult(
$policyId,
$this->event->getRequest()->getServer('REMOTE_ADDR'),
$success
Expand Down
86 changes: 86 additions & 0 deletions module/VuFind/src/VuFind/Controller/TurnstileControllerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/**
* Turnstile controller factory.
*
* PHP version 8
*
* Copyright (C) Villanova University 2024.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* @category VuFind
* @package Controller
* @author Maccabee Levine <[email protected]>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development Wiki
*/

namespace VuFind\Controller;

use Laminas\ServiceManager\Exception\ServiceNotCreatedException;
use Laminas\ServiceManager\Exception\ServiceNotFoundException;
use Psr\Container\ContainerExceptionInterface as ContainerException;
use Psr\Container\ContainerInterface;
use VuFind\Service\GetServiceTrait;

/**
* Turnstile controller factory.
*
* @category VuFind
* @package Controller
* @author Maccabee Levine <[email protected]>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development Wiki
*/
class TurnstileControllerFactory extends AbstractBaseFactory
{
use GetServiceTrait;

/**
* Create an object
*
* @param ContainerInterface $container Service manager
* @param string $requestedName Service being created
* @param null|array $options Extra options (optional)
*
* @return object
*
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
* @throws ContainerException&\Throwable if any other error occurs
*/
public function __invoke(
ContainerInterface $container,
$requestedName,
array $options = null
) {
if (!empty($options)) {
throw new \Exception('Unexpected options sent to factory.');
}
$this->serviceLocator = $container;

$yamlReader = $container->get(\VuFind\Config\YamlReader::class);
$config = $yamlReader->get('RateLimiter.yaml');

return new $requestedName(
$container,
$container->get(\VuFind\RateLimiter\Turnstile\Turnstile::class),
$container->get(\VuFind\RateLimiter\RateLimiterManager::class),
$config,
$container->get(\VuFind\Crypt\HMAC::class),
);
}
}
99 changes: 99 additions & 0 deletions module/VuFind/src/VuFind/RateLimiter/Turnstile/Turnstile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

/**
* Cloudflare Turnstile service.
*
* PHP version 8
*
* Copyright (C) Villanova University 2024.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* @category VuFind
* @package RateLimiter
* @author Maccabee Levine <[email protected]>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org Main Page
*/

namespace VuFind\RateLimiter\Turnstile;

use Laminas\Log\LoggerAwareInterface;
use VuFind\Log\LoggerAwareTrait;
use VuFind\RateLimiter\RateLimiterManager;
use VuFindHttp\HttpServiceAwareInterface;
use VuFindHttp\HttpServiceAwareTrait;

/**
* Rate limiter manager.
*
* @category VuFind
* @package Cache
* @author Maccabee Levine <[email protected]>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org Main Page
*/
class Turnstile implements HttpServiceAwareInterface, LoggerAwareInterface
{
use HttpServiceAwareTrait;
use LoggerAwareTrait;

/**
* Constructor
*
* @param array $config Rate Limiter config
* @param RateLimiterManager $rateLimiterManager Rate limiter manager
*/
public function __construct(
protected array $config,
protected RateLimiterManager $rateLimiterManager
) {
}

/**
* Validate a token against the Turnstile API
*
* @param string $token The token generated by the Turnstile widget
*
* @return bool Whether validation was successful
*/
public function validateToken($token)
{
// Call the Turnstile verify API to validate the token
$secretKey = $this->config['Turnstile']['secretKey'];
$url = $this->config['Turnstile']['verifyUrl'] ??
'https://challenges.cloudflare.com/turnstile/v0/siteverify';
$body = [
'secret' => $secretKey,
'response' => $token,
];
$response = $this->httpService->post(
$url,
json_encode($body),
'application/json'
);

if ($response->isOk()) {
$responseData = json_decode($response->getBody(), true);
$success = $responseData['success'];
} else {
// Unexpected error. Treat as a positive result, since it's not the user's fault.
$this->logWarning('Verification process failed, allowing traffic: '
. $response->getStatusCode() . $response->getBody());
$success = true;
}

return $success;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

/**
* Turnstile service factory.
*
* PHP version 8
*
* Copyright (C) Villanova University 2024.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* @category VuFind
* @package Service
* @author Maccabee Levine <[email protected]>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development Wiki
*/

namespace VuFind\RateLimiter\Turnstile;

use Laminas\ServiceManager\Exception\ServiceNotCreatedException;
use Laminas\ServiceManager\Exception\ServiceNotFoundException;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Psr\Container\ContainerExceptionInterface as ContainerException;
use Psr\Container\ContainerInterface;

/**
* Turnstile service factory.
*
* @category VuFind
* @package RateLimiter
* @author Maccabee Levine <[email protected]>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development Wiki
*/
class TurnstileFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container Service manager
* @param string $requestedName Service being created
* @param null|array $options Extra options (optional)
*
* @return object
*
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
* @throws ContainerException&\Throwable if any other error occurs
*/
public function __invoke(
ContainerInterface $container,
$requestedName,
array $options = null
) {
if (!empty($options)) {
throw new \Exception('Unexpected options sent to factory.');
}

$yamlReader = $container->get(\VuFind\Config\YamlReader::class);
$config = $yamlReader->get('RateLimiter.yaml');

return new $requestedName(
$config,
$container->get(\VuFind\RateLimiter\RateLimiterManager::class),
);
}
}

0 comments on commit 1b73f94

Please sign in to comment.