From c1c92dc296f561d631f16acb70dbee307ed31bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 9 Jan 2023 20:55:25 +0100 Subject: [PATCH] Feature: use JWT access token for securing wopi endpoints --- .../ChillWopiBundle/src/Controller/Editor.php | 30 ++++++++++++++++--- .../src/Service/Wopi/ChillWopi.php | 30 +++++++++---------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/Bundle/ChillWopiBundle/src/Controller/Editor.php b/src/Bundle/ChillWopiBundle/src/Controller/Editor.php index 486a63eb2..4be848489 100644 --- a/src/Bundle/ChillWopiBundle/src/Controller/Editor.php +++ b/src/Bundle/ChillWopiBundle/src/Controller/Editor.php @@ -15,11 +15,13 @@ use ChampsLibres\WopiLib\Contract\Service\Configuration\ConfigurationInterface; use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface; use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\MainBundle\Entity\User; use Chill\WopiBundle\Service\Controller\ResponderInterface; use Exception; +use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use loophp\psr17\Psr17Interface; -use Symfony\Component\Finder\Exception\AccessDeniedException; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RouterInterface; @@ -33,6 +35,8 @@ final class Editor { private DocumentManagerInterface $documentManager; + private JWTTokenManagerInterface $JWTTokenManager; + private Psr17Interface $psr17; private ResponderInterface $responder; @@ -49,12 +53,14 @@ final class Editor ConfigurationInterface $wopiConfiguration, DiscoveryInterface $wopiDiscovery, DocumentManagerInterface $documentManager, + JWTTokenManagerInterface $JWTTokenManager, ResponderInterface $responder, Security $security, Psr17Interface $psr17, RouterInterface $router ) { $this->documentManager = $documentManager; + $this->JWTTokenManager = $JWTTokenManager; $this->wopiConfiguration = $wopiConfiguration; $this->wopiDiscovery = $wopiDiscovery; $this->responder = $responder; @@ -65,8 +71,12 @@ final class Editor public function __invoke(string $fileId): Response { - if (null === $user = $this->security->getUser()->getUsername()) { - throw new AccessDeniedException('You must be logged in to access to this resource.'); + if (null === $user = $this->security->getUser()) { + throw new AccessDeniedHttpException('Please authenticate to access this feature'); + } + + if (!$user instanceof User) { + throw new AccessDeniedHttpException('Please authenticate as a user to access this feature'); } $configuration = $this->wopiConfiguration->jsonSerialize(); @@ -82,7 +92,19 @@ final class Editor } $configuration['favIconUrl'] = ''; - $configuration['access_token'] = $user; + $configuration['access_token'] = $this->JWTTokenManager->createFromPayload($user, [ + 'UserCanWrite' => true, + 'UserCanAttend' => true, + 'UserCanPresent' => true, + 'fileId' => $fileId, + ]); + + // we parse the token back to get the access_token_ttl + // reminder: access_token_ttl is a javascript epoch, not a number of seconds; it is the + // time when the token will expire, not the time to live: + // https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/concepts#the-access_token_ttl-property + $jwt = $this->JWTTokenManager->parse($configuration['access_token']); + $configuration['access_token_ttl'] = $jwt['exp'] * 1000; $configuration['server'] = $this ->psr17 diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php index 2062080a7..3629c6126 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/ChillWopi.php @@ -13,16 +13,15 @@ namespace Chill\WopiBundle\Service\Wopi; use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface; use ChampsLibres\WopiLib\Contract\Service\WopiInterface; -use DateTimeImmutable; +use Chill\MainBundle\Entity\User; use DateTimeInterface; use loophp\psr17\Psr17Interface; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserProviderInterface; -use function strlen; final class ChillWopi implements WopiInterface { @@ -32,6 +31,8 @@ final class ChillWopi implements WopiInterface private Psr17Interface $psr17; + private Security $security; + private UserProviderInterface $userProvider; private WopiInterface $wopi; @@ -40,12 +41,14 @@ final class ChillWopi implements WopiInterface CacheItemPoolInterface $cache, DocumentManagerInterface $documentManager, Psr17Interface $psr17, + Security $security, UserProviderInterface $userProvider, WopiInterface $wopi ) { $this->cache = $cache; $this->documentManager = $documentManager; $this->psr17 = $psr17; + $this->security = $security; $this->userProvider = $userProvider; $this->wopi = $wopi; } @@ -55,18 +58,13 @@ final class ChillWopi implements WopiInterface ?string $accessToken, RequestInterface $request ): ResponseInterface { - try { - $user = $this->userProvider->loadUserByUsername($accessToken); - } catch (UsernameNotFoundException $e) { - return $this - ->psr17 - ->createResponse(401); - } + $user = $this->security->getUser(); - // @ TODO : Replace this with a call to decorated object once authentication is done. + if (!$user instanceof User) { + throw new AccessDeniedHttpException('User must be authenticated'); + } $document = $this->documentManager->findByDocumentId($fileId); - $userIdentifier = $user->getUsername(); - $userCacheKey = sprintf('wopi_putUserInfo_%s', $userIdentifier); + $userCacheKey = sprintf('wopi_putUserInfo_%s', $user->getId()); return $this ->psr17 @@ -77,7 +75,7 @@ final class ChillWopi implements WopiInterface 'BaseFileName' => $this->documentManager->getBasename($document), 'OwnerId' => 'Symfony', 'Size' => $this->documentManager->getSize($document), - 'UserId' => $userIdentifier, + 'UserId' => $user->getId(), 'ReadOnly' => false, 'UserCanAttend' => true, 'UserCanPresent' => true, @@ -89,7 +87,7 @@ final class ChillWopi implements WopiInterface 'SupportsLocks' => true, 'SupportsGetLock' => true, 'SupportsExtendedLockLength' => true, - 'UserFriendlyName' => $userIdentifier, + 'UserFriendlyName' => $user->getLabel(), 'SupportsUpdate' => true, 'SupportsRename' => true, 'SupportsFolder' => false,