Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/Exception/TokenRefreshLockedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\UserOIDC\Exception;

use Exception;

class TokenRefreshLockedException extends Exception {
}
32 changes: 28 additions & 4 deletions lib/Service/TokenService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use OCA\UserOIDC\AppInfo\Application;
use OCA\UserOIDC\Db\ProviderMapper;
use OCA\UserOIDC\Exception\TokenExchangeFailedException;
use OCA\UserOIDC\Exception\TokenRefreshLockedException;
use OCA\UserOIDC\Helper\HttpClientHelper;
use OCA\UserOIDC\Model\Token;
use OCA\UserOIDC\Vendor\Firebase\JWT\JWT;
Expand All @@ -38,6 +39,7 @@
class TokenService {

private const SESSION_TOKEN_KEY = Application::APP_ID . '-user-token';
private const REFRESH_LOCK_KEY = Application::APP_ID . '-refresh-lock';

private IClient $client;

Expand Down Expand Up @@ -122,7 +124,12 @@ public function checkLoginToken(): void {
return;
}

$token = $this->getToken();
try {
$token = $this->getToken();
} catch (TokenRefreshLockedException) {
$this->logger->debug('[TokenService] checkLoginToken: the token refresh is locked by another process');
return;
}
if ($token === null) {
$this->logger->debug('[TokenService] checkLoginToken: token is null');
// if we don't have a token but we had one once,
Expand Down Expand Up @@ -152,11 +159,20 @@ public function reauthenticate(int $providerId) {
/**
* @param Token $token
* @return Token
* @throws \JsonException
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws TokenRefreshLockedException
* @throws \JsonException
*/
public function refresh(Token $token): Token {
// check lock
$sessionLocked = $this->session->get(self::REFRESH_LOCK_KEY);
if ($sessionLocked !== null) {
throw new TokenRefreshLockedException();
}
// acquire lock
$this->session->set(self::REFRESH_LOCK_KEY, 1);
Comment on lines +169 to +174

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this also racy? The set operation would better be some kind of atomic check-and-set


$oidcProvider = $this->providerMapper->getProvider($token->getProviderId());
$discovery = $this->discoveryService->obtainDiscovery($oidcProvider);

Expand Down Expand Up @@ -188,13 +204,16 @@ public function refresh(Token $token): Token {

$bodyArray = json_decode(trim($body), true, 512, JSON_THROW_ON_ERROR);
$this->logger->debug('[TokenService] ---- Refresh token success');
return $this->storeToken(
$refreshedToken = $this->storeToken(
array_merge(
$bodyArray,
['provider_id' => $token->getProviderId()],
)
);
$this->session->remove(self::REFRESH_LOCK_KEY);
return $refreshedToken;
} catch (\Exception $e) {
$this->session->remove(self::REFRESH_LOCK_KEY);
$this->logger->error('[TokenService] Failed to refresh token ', ['exception' => $e]);
// Failed to refresh, return old token which will be retried or otherwise timeout if expired
return $token;
Expand Down Expand Up @@ -229,7 +248,12 @@ public function getExchangedToken(string $targetAudience, array $extraScopes = [
}
$this->logger->debug('[TokenService] Starting token exchange');

$loginToken = $this->getToken();
try {
$loginToken = $this->getToken();
} catch (TokenRefreshLockedException $e) {
$this->logger->error('[TokenService] Failed to exchange token. The login token refresh failed because it was locked');
throw new TokenExchangeFailedException('Failed to exchange token. The login token refresh failed because it was locked', 0, $e);
}
if ($loginToken === null) {
$this->logger->debug('[TokenService] Failed to exchange token, no login token found in the session');
throw new TokenExchangeFailedException('Failed to exchange token, no login token found in the session');
Expand Down
Loading