Initial Upload
This commit is contained in:
215
lib/Middleware/ApiAuthorization.php
Normal file
215
lib/Middleware/ApiAuthorization.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Xibo\Middleware;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\ResourceServer;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface as Middleware;
|
||||
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||
use Slim\App as App;
|
||||
use Slim\Routing\RouteContext;
|
||||
use Xibo\Factory\ApplicationScopeFactory;
|
||||
use Xibo\Helper\UserLogProcessor;
|
||||
use Xibo\OAuth\AccessTokenRepository;
|
||||
use Xibo\Support\Exception\AccessDeniedException;
|
||||
|
||||
/**
|
||||
* Class ApiAuthenticationOAuth
|
||||
* This middleware protects the API entry point
|
||||
* @package Xibo\Middleware
|
||||
*/
|
||||
class ApiAuthorization implements Middleware
|
||||
{
|
||||
/* @var App $app */
|
||||
private $app;
|
||||
|
||||
/**
|
||||
* ApiAuthenticationOAuth constructor.
|
||||
* @param $app
|
||||
*/
|
||||
public function __construct($app)
|
||||
{
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param RequestHandler $handler
|
||||
* @return Response
|
||||
* @throws OAuthServerException
|
||||
* @throws \Xibo\Support\Exception\AccessDeniedException
|
||||
* @throws \Xibo\Support\Exception\ConfigurationException
|
||||
* @throws \Xibo\Support\Exception\NotFoundException
|
||||
*/
|
||||
public function process(Request $request, RequestHandler $handler): Response
|
||||
{
|
||||
/* @var \Xibo\Entity\User $user */
|
||||
$user = null;
|
||||
|
||||
/** @var \Xibo\Service\LogServiceInterface $logger */
|
||||
$logger = $this->app->getContainer()->get('logService');
|
||||
|
||||
// Setup the authorization server
|
||||
$this->app->getContainer()->set('server', function (ContainerInterface $container) use ($logger) {
|
||||
// oAuth Resource
|
||||
$apiKeyPaths = $container->get('configService')->getApiKeyDetails();
|
||||
|
||||
$accessTokenRepository = new AccessTokenRepository(
|
||||
$logger,
|
||||
$container->get('pool'),
|
||||
$container->get('applicationFactory')
|
||||
);
|
||||
return new ResourceServer(
|
||||
$accessTokenRepository,
|
||||
$apiKeyPaths['publicKeyPath']
|
||||
);
|
||||
});
|
||||
|
||||
/** @var ResourceServer $server */
|
||||
$server = $this->app->getContainer()->get('server');
|
||||
$validatedRequest = $server->validateAuthenticatedRequest($request);
|
||||
|
||||
// We have a valid JWT/token
|
||||
// get our user from it.
|
||||
$userFactory = $this->app->getContainer()->get('userFactory');
|
||||
|
||||
// What type of Access Token to we have? Client Credentials or AuthCode
|
||||
// client_credentials grants are issued with the correct oauth_user_id in the token, so we don't need to
|
||||
// distinguish between them here! nice!
|
||||
$userId = $validatedRequest->getAttribute('oauth_user_id');
|
||||
|
||||
$user = $userFactory->getById($userId);
|
||||
$user->setChildAclDependencies($this->app->getContainer()->get('userGroupFactory'));
|
||||
$user->load();
|
||||
|
||||
// Block access by retired users.
|
||||
if ($user->retired === 1) {
|
||||
throw new AccessDeniedException(__('Sorry this account does not exist or cannot be authenticated.'));
|
||||
}
|
||||
|
||||
// We must check whether this user has access to the route they have requested.
|
||||
// Get the current route pattern
|
||||
$routeContext = RouteContext::fromRequest($request);
|
||||
$route = $routeContext->getRoute();
|
||||
$resource = $route->getPattern();
|
||||
|
||||
// Allow public routes
|
||||
if (!in_array($resource, $validatedRequest->getAttribute('publicRoutes', []))) {
|
||||
$request = $request->withAttribute('public', false);
|
||||
|
||||
// Check that the Scopes granted to this token are allowed access to the route/method of this request
|
||||
/** @var ApplicationScopeFactory $applicationScopeFactory */
|
||||
$applicationScopeFactory = $this->app->getContainer()->get('applicationScopeFactory');
|
||||
$scopes = $validatedRequest->getAttribute('oauth_scopes');
|
||||
|
||||
// If there are no scopes in the JWT, we should not authorise
|
||||
// An older client which makes a request with no scopes should get an access token with
|
||||
// all scopes configured for the application
|
||||
if (!is_array($scopes) || count($scopes) <= 0) {
|
||||
throw new AccessDeniedException();
|
||||
}
|
||||
|
||||
$logger->debug('Scopes provided with request: ' . count($scopes));
|
||||
|
||||
// Check all scopes in the request before we deny access.
|
||||
$grantAccess = false;
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
// If I have the "all" scope granted then there isn't any need to test further
|
||||
if ($scope === 'all') {
|
||||
$grantAccess = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$logger->debug(
|
||||
sprintf(
|
||||
'Test authentication for %s %s against scope %s',
|
||||
$resource,
|
||||
$request->getMethod(),
|
||||
$scope
|
||||
)
|
||||
);
|
||||
|
||||
// Check the route and request method
|
||||
if ($applicationScopeFactory->getById($scope)->checkRoute($request->getMethod(), $resource)) {
|
||||
$grantAccess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$grantAccess) {
|
||||
throw new AccessDeniedException(__('Access to this route is denied for this scope'));
|
||||
}
|
||||
} else {
|
||||
// Public request
|
||||
$validatedRequest = $validatedRequest->withAttribute('public', true);
|
||||
}
|
||||
|
||||
$requestId = $this->app->getContainer()->get('store')->insert('
|
||||
INSERT INTO `application_requests_history` (
|
||||
userId,
|
||||
applicationId,
|
||||
url,
|
||||
method,
|
||||
startTime,
|
||||
endTime,
|
||||
duration
|
||||
)
|
||||
VALUES (
|
||||
:userId,
|
||||
:applicationId,
|
||||
:url,
|
||||
:method,
|
||||
:startTime,
|
||||
:endTime,
|
||||
:duration
|
||||
)
|
||||
', [
|
||||
'userId' => $user->userId,
|
||||
'applicationId' => $validatedRequest->getAttribute('oauth_client_id'),
|
||||
'url' => htmlspecialchars($request->getUri()->getPath()),
|
||||
'method' => $request->getMethod(),
|
||||
'startTime' => Carbon::now(),
|
||||
'endTime' => Carbon::now(),
|
||||
'duration' => 0
|
||||
], 'api_requests_history');
|
||||
|
||||
$this->app->getContainer()->get('store')->commitIfNecessary('api_requests_history');
|
||||
|
||||
$logger->setUserId($user->userId);
|
||||
$this->app->getContainer()->set('user', $user);
|
||||
$logger->setRequestId($requestId);
|
||||
|
||||
// Add this request information to the logger.
|
||||
$logger->getLoggerInterface()->pushProcessor(new UserLogProcessor(
|
||||
$user->userId,
|
||||
null,
|
||||
$requestId,
|
||||
));
|
||||
|
||||
return $handler->handle($validatedRequest->withAttribute('name', 'API'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user