Files
Cloud-CMS/lib/Middleware/ApiAuthorization.php

216 lines
7.7 KiB
PHP
Raw Normal View History

2025-12-02 10:32:59 -05:00
<?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'));
}
}