diff --git a/README.md b/README.md index c1b6683..4cb8557 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ ## OIDC token handling +This app needs a valid OIDC token to get the central menu content and contact OpenXChange. We get this token from the user_oidc app. +Depending on how user_oidc is configured, we either store and refresh the login token ourselves or rely on user_oidc to do so. + +If "Store login tokens" is enabled in user_oidc's admin settings, we know we can use the `OCA\UserOIDC\Event\ExternalTokenRequestedEvent` +to ask user_oidc to provide the login token (or a refreshed one) instead of storing this token ourselves (and refreshing it). + During login the access_token and refresh token are passed by the user_oidc app to the integration_swp app through a dispatched event. integration_swp will request a fresh token and regularly refresh it with the refresh token that was initially provided by the OpenID Connect login. diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 2379e6c..8240b84 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -56,7 +56,7 @@ public function __construct( #[PublicPage] #[NoCSRFRequired] #[UseSession] - public function index() { + public function index(): JSONResponse { /** @var Token $token */ $token = \OC::$server->get(TokenService::class)->getToken(true); if ($token === null) { diff --git a/lib/Listener/PublicShareTemplateLoader.php b/lib/Listener/PublicShareTemplateLoader.php index cbff354..ce5353d 100644 --- a/lib/Listener/PublicShareTemplateLoader.php +++ b/lib/Listener/PublicShareTemplateLoader.php @@ -18,7 +18,7 @@ use OCP\Util; /** - * @implements IEventListener + * @implements IEventListener * Helper class to extend the "publicshare" template from the server. */ class PublicShareTemplateLoader implements IEventListener { diff --git a/lib/Listener/TokenObtainedEventListener.php b/lib/Listener/TokenObtainedEventListener.php index e76aa0c..e2c116d 100644 --- a/lib/Listener/TokenObtainedEventListener.php +++ b/lib/Listener/TokenObtainedEventListener.php @@ -10,22 +10,18 @@ namespace OCA\Swp\Listener; use OCA\Swp\AppInfo\Application; -//use OCA\Swp\Service\OxMailService; use OCA\Swp\Service\TokenService; use OCA\UserOIDC\Event\TokenObtainedEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; -//use OCP\Http\Client\IClientService; use Psr\Log\LoggerInterface; /** - * @implements IEventListener + * @implements IEventListener */ class TokenObtainedEventListener implements IEventListener { public function __construct( - // private IClientService $clientService, - // private OxMailService $mailService, private LoggerInterface $logger, private TokenService $tokenService, ) { @@ -39,39 +35,9 @@ public function handle(Event $event): void { $token = $event->getToken(); $provider = $event->getProvider(); - $discovery = $event->getDiscovery(); - - //$refreshToken = $token['refresh_token'] ?? null; - - //if (!$refreshToken) { - // $this->logger->debug('handle TokenObtainedEvent NO REFRESH TOKEN', ['app' => Application::APP_ID]); - // return; - //} - - //$client = $this->clientService->newClient(); - //$this->logger->debug('TokenObtainedEventListener TOKEN REQUEST to ' . $discovery['token_endpoint'] . ' with refresh token=' . $refreshToken . ' and client id=' . $provider->getClientId(), ['app' => Application::APP_ID]); - //$result = $client->post( - // $discovery['token_endpoint'], - // [ - // 'body' => [ - // 'client_id' => $provider->getClientId(), - // 'client_secret' => $provider->getClientSecret(), - // 'grant_type' => 'refresh_token', - // 'refresh_token' => $refreshToken, - // // TODO check if we need a different scope for this - // 'scope' => $provider->getScope(), - // ], - // ] - //); - //$this->logger->debug('refresh request STATUS CODE:' . $result->getStatusCode(), ['app' => Application::APP_ID]); - - //$tokenData = json_decode($result->getBody(), true); $tokenData = $token; $this->logger->debug('Storing the token: ' . json_encode($tokenData), ['app' => Application::APP_ID]); $this->tokenService->storeToken(array_merge($tokenData, ['provider_id' => $provider->getId()])); - - // $this->mailService->resetCache(); - // $this->mailService->fetchUnreadCounter(); } } diff --git a/lib/Model/Token.php b/lib/Model/Token.php index 341a9ab..d923a9b 100644 --- a/lib/Model/Token.php +++ b/lib/Model/Token.php @@ -41,6 +41,11 @@ public function getExpiresIn(): int { return $this->expiresIn; } + public function getExpiresInFromNow(): int { + $expiresAt = $this->createdAt + $this->expiresIn; + return $expiresAt - time(); + } + public function getRefreshToken(): string { return $this->refreshToken; } diff --git a/lib/Service/TokenService.php b/lib/Service/TokenService.php index 11eaf46..c650b38 100644 --- a/lib/Service/TokenService.php +++ b/lib/Service/TokenService.php @@ -22,8 +22,10 @@ use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\Authentication\Exceptions\WipeTokenException; use OCP\Authentication\Token\IToken; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; +use OCP\IAppConfig; use OCP\ICache; use OCP\ICacheFactory; use OCP\IConfig; @@ -53,8 +55,10 @@ public function __construct( private LoggerInterface $logger, private IRequest $request, private IConfig $config, + private IAppConfig $appConfig, private ICrypto $crypto, private IAppManager $appManager, + private IEventDispatcher $eventDispatcher, ) { $this->client = $clientService->newClient(); $this->cache = $cacheFactory->createDistributed(Application::APP_ID); @@ -88,6 +92,10 @@ public function storeToken(array $tokenData): Token { } public function getToken(bool $refresh = true): ?Token { + $isUserOidcStoringTokens = $this->appConfig->getValueString(\OCA\UserOIDC\AppInfo\Application::APP_ID, 'store_login_token', '0') === '1'; + if ($isUserOidcStoringTokens) { + return $this->getTokenFromUserOidc(); + } $sessionData = $this->session->get(self::SESSION_TOKEN_KEY); if (!$sessionData) { return null; @@ -101,10 +109,51 @@ public function getToken(bool $refresh = true): ?Token { if ($refresh && $token->isExpiring()) { $token = $this->refresh($token); } + $this->logger->debug('[SWPTokenService] Obtained a token from our own session storage that expires in ' . $token->getExpiresInFromNow()); return $token; } - public function refresh(Token $token) { + private function getTokenFromUserOidc(): ?Token { + $event = new \OCA\UserOIDC\Event\ExternalTokenRequestedEvent(); + try { + $this->eventDispatcher->dispatchTyped($event); + } catch (\OCA\UserOIDC\Exception\GetExternalTokenFailedException $e) { + $this->logger->debug('Failed to get external token: ' . $e->getMessage()); + $error = $e->getError(); + $errorDescription = $e->getErrorDescription(); + if ($error && $errorDescription) { + $this->logger->debug('Token obtention error: ' . $error . ' (' . $errorDescription . ')'); + } + } + $userOidcToken = $event->getToken(); + if ($userOidcToken === null) { + $this->logger->debug('There was no token found in the session'); + return null; + } else { + $this->logger->debug('[SWPTokenService] Obtained a token from user_oidc that expires in ' . $userOidcToken->getExpiresInFromNow()); + return new Token([ + 'id_token' => $userOidcToken->getIdToken(), + 'access_token' => $userOidcToken->getAccessToken(), + 'expires_in' => $userOidcToken->getExpiresIn(), + 'refresh_token' => $userOidcToken->getRefreshToken(), + 'created_at' => $userOidcToken->getCreatedAt(), + 'provider_id' => $userOidcToken->getProviderId(), + ]); + } + } + + /** + * Refresh the token we stored ourselves if user_oidc does not store its tokens and we didn't use the events + * + * @param Token $token + * @return Token + * @throws \JsonException + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface + */ + public function refresh(Token $token): Token { /** @var ProviderMapper $providerMapper */ $providerMapper = \OC::$server->get(ProviderMapper::class); $oidcProvider = $providerMapper->getProvider($token->getProviderId()); diff --git a/psalm.xml b/psalm.xml index 0873760..1e8738f 100644 --- a/psalm.xml +++ b/psalm.xml @@ -39,6 +39,8 @@ + + @@ -52,5 +54,6 @@ + diff --git a/tests/stubs/oca_events.php b/tests/stubs/oca_events.php index 422a881..a32f3fc 100644 --- a/tests/stubs/oca_events.php +++ b/tests/stubs/oca_events.php @@ -16,19 +16,3 @@ public function getScope(): ?string { } } } - -namespace OCA\UserOIDC\Event { - - use OCP\EventDispatcher\Event; - - class TokenObtainedEvent extends Event { - public function getToken(): array { - } - - public function getProvider(): \OCA\UserOIDC\Db\Provider { - } - - public function getDiscovery(): array { - } - } -} diff --git a/tests/stubs/oca_user_oidc.php b/tests/stubs/oca_user_oidc.php new file mode 100644 index 0000000..42a4475 --- /dev/null +++ b/tests/stubs/oca_user_oidc.php @@ -0,0 +1,72 @@ +