216 lines
7.7 KiB
PHP
216 lines
7.7 KiB
PHP
<?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'));
|
|
}
|
|
}
|