Skip to content

Commit 019e2c2

Browse files
committed
fix(login-token-refresh): prevent multiple simultaneous token refresh, store lock in the php session
Signed-off-by: Julien Veyssier <[email protected]>
1 parent 1188397 commit 019e2c2

File tree

2 files changed

+42
-4
lines changed

2 files changed

+42
-4
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\UserOIDC\Exception;
10+
11+
use Exception;
12+
13+
class TokenRefreshLockedException extends Exception {
14+
}

lib/Service/TokenService.php

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use OCA\UserOIDC\AppInfo\Application;
1414
use OCA\UserOIDC\Db\ProviderMapper;
1515
use OCA\UserOIDC\Exception\TokenExchangeFailedException;
16+
use OCA\UserOIDC\Exception\TokenRefreshLockedException;
1617
use OCA\UserOIDC\Helper\HttpClientHelper;
1718
use OCA\UserOIDC\Model\Token;
1819
use OCA\UserOIDC\Vendor\Firebase\JWT\JWT;
@@ -38,6 +39,7 @@
3839
class TokenService {
3940

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

4244
private IClient $client;
4345

@@ -122,7 +124,12 @@ public function checkLoginToken(): void {
122124
return;
123125
}
124126

125-
$token = $this->getToken();
127+
try {
128+
$token = $this->getToken();
129+
} catch (TokenRefreshLockedException) {
130+
$this->logger->debug('[TokenService] checkLoginToken: the token refresh is locked by another process');
131+
return;
132+
}
126133
if ($token === null) {
127134
$this->logger->debug('[TokenService] checkLoginToken: token is null');
128135
// if we don't have a token but we had one once,
@@ -152,11 +159,20 @@ public function reauthenticate(int $providerId) {
152159
/**
153160
* @param Token $token
154161
* @return Token
155-
* @throws \JsonException
156162
* @throws DoesNotExistException
157163
* @throws MultipleObjectsReturnedException
164+
* @throws TokenRefreshLockedException
165+
* @throws \JsonException
158166
*/
159167
public function refresh(Token $token): Token {
168+
// check lock
169+
$sessionLocked = $this->session->get(self::REFRESH_LOCK_KEY);
170+
if ($sessionLocked !== null) {
171+
throw new TokenRefreshLockedException();
172+
}
173+
// acquire lock
174+
$this->session->set(self::REFRESH_LOCK_KEY, 1);
175+
160176
$oidcProvider = $this->providerMapper->getProvider($token->getProviderId());
161177
$discovery = $this->discoveryService->obtainDiscovery($oidcProvider);
162178

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

189205
$bodyArray = json_decode(trim($body), true, 512, JSON_THROW_ON_ERROR);
190206
$this->logger->debug('[TokenService] ---- Refresh token success');
191-
return $this->storeToken(
207+
$refreshedToken = $this->storeToken(
192208
array_merge(
193209
$bodyArray,
194210
['provider_id' => $token->getProviderId()],
195211
)
196212
);
213+
$this->session->remove(self::REFRESH_LOCK_KEY);
214+
return $refreshedToken;
197215
} catch (\Exception $e) {
216+
$this->session->remove(self::REFRESH_LOCK_KEY);
198217
$this->logger->error('[TokenService] Failed to refresh token ', ['exception' => $e]);
199218
// Failed to refresh, return old token which will be retried or otherwise timeout if expired
200219
return $token;
@@ -229,7 +248,12 @@ public function getExchangedToken(string $targetAudience, array $extraScopes = [
229248
}
230249
$this->logger->debug('[TokenService] Starting token exchange');
231250

232-
$loginToken = $this->getToken();
251+
try {
252+
$loginToken = $this->getToken();
253+
} catch (TokenRefreshLockedException $e) {
254+
$this->logger->error('[TokenService] Failed to exchange token. The login token refresh failed because it was locked');
255+
throw new TokenExchangeFailedException('Failed to exchange token. The login token refresh failed because it was locked', 0, $e);
256+
}
233257
if ($loginToken === null) {
234258
$this->logger->debug('[TokenService] Failed to exchange token, no login token found in the session');
235259
throw new TokenExchangeFailedException('Failed to exchange token, no login token found in the session');

0 commit comments

Comments
 (0)