Skip to content

Commit

Permalink
add token exchange event-based mechanism, store the login token and k…
Browse files Browse the repository at this point in the history
…eep it fresh

Signed-off-by: Julien Veyssier <[email protected]>
  • Loading branch information
julien-nc committed Nov 4, 2024
1 parent 42fde60 commit aa6b287
Show file tree
Hide file tree
Showing 7 changed files with 559 additions and 0 deletions.
9 changes: 9 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
use OC_User;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\UserOIDC\Db\ProviderMapper;
use OCA\UserOIDC\Event\ExchangedTokenRequestedEvent;
use OCA\UserOIDC\Listener\ExchangedTokenRequestedListener;
use OCA\UserOIDC\Listener\TimezoneHandlingListener;
use OCA\UserOIDC\Service\ID4MeService;
use OCA\UserOIDC\Service\SettingsService;
use OCA\UserOIDC\Service\TokenService;
use OCA\UserOIDC\User\Backend;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
Expand Down Expand Up @@ -52,10 +55,12 @@ public function register(IRegistrationContext $context): void {
OC_User::useBackend($this->backend);

$context->registerEventListener(LoadAdditionalScriptsEvent::class, TimezoneHandlingListener::class);
$context->registerEventListener(ExchangedTokenRequestedEvent::class, ExchangedTokenRequestedListener::class);
}

public function boot(IBootContext $context): void {
$context->injectFn(\Closure::fromCallable([$this->backend, 'injectSession']));
$context->injectFn(\Closure::fromCallable([$this, 'checkLoginToken']));
/** @var IUserSession $userSession */
$userSession = $this->getContainer()->get(IUserSession::class);
if ($userSession->isLoggedIn()) {
Expand All @@ -69,6 +74,10 @@ public function boot(IBootContext $context): void {
}
}

private function checkLoginToken(TokenService $tokenService): void {
$tokenService->checkLoginToken();
}

private function registerRedirect(IRequest $request, IURLGenerator $urlGenerator, SettingsService $settings, ProviderMapper $providerMapper): void {
$providers = $this->getCachedProviders($providerMapper);
$redirectUrl = $request->getParam('redirect_url');
Expand Down
10 changes: 10 additions & 0 deletions lib/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use OCA\UserOIDC\Service\LdapService;
use OCA\UserOIDC\Service\ProviderService;
use OCA\UserOIDC\Service\ProvisioningService;
use OCA\UserOIDC\Service\TokenService;
use OCA\UserOIDC\Vendor\Firebase\JWT\JWT;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
Expand Down Expand Up @@ -77,6 +78,7 @@ public function __construct(
private IL10N $l10n,
private LoggerInterface $logger,
private ICrypto $crypto,
private TokenService $tokenService,
) {
parent::__construct($request, $config);
}
Expand Down Expand Up @@ -496,6 +498,14 @@ public function code(string $state = '', string $code = '', string $scope = '',
$this->userSession->createRememberMeToken($user);
}

// store all token information for potential token exchange requests
$tokenData = array_merge(
$data,
['provider_id' => $providerId],
);
$this->tokenService->storeToken($tokenData);
$this->config->setUserValue($user->getUID(), Application::APP_ID, 'had_token_once', '1');

// Set last password confirm to the future as we don't have passwords to confirm against with SSO
$this->session->set('last-password-confirm', strtotime('+4 year', time()));

Expand Down
59 changes: 59 additions & 0 deletions lib/Event/ExchangedTokenRequestedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/*
* @copyright Copyright (c) 2022 Julien Veyssier <[email protected]>
*
* @author Julien Veyssier <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

declare(strict_types=1);

namespace OCA\UserOIDC\Event;

use OCA\UserOIDC\Model\Token;
use OCP\EventDispatcher\Event;

/**
* This event is emitted with by other apps which need an exchanged token for another audience (another client ID)
*/
class ExchangedTokenRequestedEvent extends Event {

private ?Token $token = null;

public function __construct(
private string $targetAudience,
) {
parent::__construct();
}

public function getTargetAudience(): string {
return $this->targetAudience;
}

public function setTargetAudience(string $targetAudience): void {
$this->targetAudience = $targetAudience;
}

public function getToken(): ?Token {
return $this->token;
}

public function setToken(?Token $token): void {
$this->token = $token;
}
}
15 changes: 15 additions & 0 deletions lib/Exception/TokenExchangeFailedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

namespace OCA\UserOIDC\Exception;

use Exception;

class TokenExchangeFailedException extends Exception {
}
58 changes: 58 additions & 0 deletions lib/Listener/ExchangedTokenRequestedListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
/**
* @copyright Copyright (c) 2024 Julien Veyssier <[email protected]>
*
* @author Julien Veyssier <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/

namespace OCA\UserOIDC\Listener;

use OCA\UserOIDC\Event\ExchangedTokenRequestedEvent;
use OCA\UserOIDC\Service\TokenService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;

/**
* @implements IEventListener<ExchangedTokenRequestedEvent|Event>
*/
class ExchangedTokenRequestedListener implements IEventListener {

public function __construct(
private IUserSession $userSession,
private TokenService $tokenService,
private LoggerInterface $logger,
) {
}

public function handle(Event $event): void {
if (!$event instanceof ExchangedTokenRequestedEvent) {
return;
}

if (!$this->userSession->isLoggedIn()) {
return;
}

$targetAudience = $event->getTargetAudience();
$this->logger->debug('[TokenExchange Listener] received request for audience: ' . $targetAudience);
$token = $this->tokenService->getExchangedToken($targetAudience);
$event->setToken($token);
}
}
115 changes: 115 additions & 0 deletions lib/Model/Token.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php
/**
* @copyright Copyright (c) 2021 Julien Veyssier <[email protected]>
*
* @author Julien Veyssier <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

declare(strict_types=1);

namespace OCA\UserOIDC\Model;

use JsonSerializable;

class Token implements JsonSerializable {

private string $idToken;
private string $accessToken;
private int $expiresIn;
private int $refreshExpiresIn;
private string $refreshToken;
private int $createdAt;
private ?int $providerId;

public function __construct(array $tokenData) {
$this->idToken = $tokenData['id_token'];
$this->accessToken = $tokenData['access_token'];
$this->expiresIn = $tokenData['expires_in'];
$this->refreshExpiresIn = $tokenData['refresh_expires_in'];
$this->refreshToken = $tokenData['refresh_token'];
$this->createdAt = $tokenData['created_at'] ?? time();
$this->providerId = $tokenData['provider_id'] ?? null;
}

public function getAccessToken(): string {
return $this->accessToken;
}

public function getIdToken(): string {
return $this->idToken;
}

public function getExpiresIn(): int {
return $this->expiresIn;
}

public function getExpiresInFromNow(): int {
$expiresAt = $this->createdAt + $this->expiresIn;
return $expiresAt - time();
}

public function getRefreshExpiresIn(): int {
return $this->refreshExpiresIn;
}

public function getRefreshExpiresInFromNow(): int {
$refreshExpiresAt = $this->createdAt + $this->refreshExpiresIn;
return $refreshExpiresAt - time();
}

public function getRefreshToken(): string {
return $this->refreshToken;
}

public function getProviderId(): ?int {
return $this->providerId;
}

public function isExpired(): bool {
return time() > ($this->createdAt + $this->expiresIn);
}

public function isExpiring(): bool {
return time() > ($this->createdAt + (int)($this->expiresIn / 2));
}

public function refreshIsExpired(): bool {
return time() > ($this->createdAt + $this->refreshExpiresIn);
}

public function refreshIsExpiring(): bool {
return time() > ($this->createdAt + (int)($this->refreshExpiresIn / 2));
}

public function getCreatedAt() {
return $this->createdAt;
}

public function jsonSerialize(): array {
return [
'id_token' => $this->idToken,
'access_token' => $this->accessToken,
'expires_in' => $this->expiresIn,
'refresh_expires_in' => $this->refreshExpiresIn,
'refresh_token' => $this->refreshToken,
'created_at' => $this->createdAt,
'provider_id' => $this->providerId,
];
}
}
Loading

0 comments on commit aa6b287

Please sign in to comment.