3232use OCP \ISession ;
3333use OCP \IURLGenerator ;
3434use OCP \IUserSession ;
35+ use OCP \Lock \ILockingProvider ;
36+ use OCP \Lock \LockedException ;
3537use OCP \PreConditionNotMetException ;
3638use OCP \Security \ICrypto ;
3739use OCP \Session \Exceptions \SessionNotAvailableException ;
4547class TokenService {
4648
4749 private const SESSION_TOKEN_KEY = Application::APP_ID . '-user-token ' ;
50+ private const REFRESH_LOCK_KEY = Application::APP_ID . '-lock-key ' ;
4851
4952 private IClient $ client ;
5053
@@ -63,8 +66,8 @@ public function __construct(
6366 private IAppManager $ appManager ,
6467 private DiscoveryService $ discoveryService ,
6568 private ProviderMapper $ providerMapper ,
69+ private ILockingProvider $ lockingProvider ,
6670 ) {
67-
6871 }
6972
7073 public function storeToken (array $ tokenData ): Token {
@@ -192,18 +195,53 @@ public function reauthenticate(int $providerId) {
192195 * @throws MultipleObjectsReturnedException
193196 */
194197 public function refresh (Token $ token ): Token {
195- $ oidcProvider = $ this ->providerMapper ->getProvider ($ token ->getProviderId ());
196- $ discovery = $ this ->discoveryService ->obtainDiscovery ($ oidcProvider );
198+ $ lockKey = self ::REFRESH_LOCK_KEY . '_ ' . $ this ->session ->getId ();
199+
200+ // Retry loop to acquire lock with timeout
201+ $ maxRetries = 50 ; // 5 seconds total (50 × 100ms)
202+ $ retryCount = 0 ;
203+ $ lockAcquired = false ;
204+
205+ while (!$ lockAcquired && $ retryCount < $ maxRetries ) {
206+ try {
207+ $ this ->lockingProvider ->acquireLock ($ lockKey , ILockingProvider::LOCK_EXCLUSIVE );
208+ $ lockAcquired = true ;
209+ $ this ->logger ->debug ('[TokenService] Acquired lock for token refresh ' );
210+ } catch (LockedException $ e ) {
211+ // Another request is refreshing, wait and retry
212+ $ retryCount ++;
213+ if ($ retryCount >= $ maxRetries ) {
214+ $ this ->logger ->warning ('[TokenService] Failed to acquire lock after retries, returning old token ' );
215+ return $ token ;
216+ }
217+ usleep (100000 ); // 100ms between retries
218+ }
219+ }
197220
198221 try {
222+ // Double-check: token might have been refreshed while we waited for the lock
223+ $ sessionData = $ this ->session ->get (self ::SESSION_TOKEN_KEY );
224+ if ($ sessionData ) {
225+ $ currentToken = new Token (json_decode ($ sessionData , true , 512 , JSON_THROW_ON_ERROR ));
226+ if (!$ currentToken ->isExpired ()) {
227+ $ this ->logger ->debug ('[TokenService] Token already refreshed by another request ' );
228+ return $ currentToken ;
229+ }
230+ }
231+
232+ // Token still expired, proceed with refresh
233+ $ oidcProvider = $ this ->providerMapper ->getProvider ($ token ->getProviderId ());
234+ $ discovery = $ this ->discoveryService ->obtainDiscovery ($ oidcProvider );
235+
199236 $ clientSecret = $ oidcProvider ->getClientSecret ();
200237 if ($ clientSecret !== '' ) {
201238 try {
202239 $ clientSecret = $ this ->crypto ->decrypt ($ clientSecret );
203240 } catch (\Exception $ e ) {
204- $ this ->logger ->error ('[TokenService] Failed to decrypt oidc client secret to refresh the token ' );
241+ $ this ->logger ->error ('[TokenService] Failed to decrypt oidc client secret ' );
205242 }
206243 }
244+
207245 $ this ->logger ->debug ('[TokenService] Refreshing the token: ' . $ discovery ['token_endpoint ' ]);
208246 $ body = $ this ->clientService ->post (
209247 $ discovery ['token_endpoint ' ],
@@ -214,25 +252,24 @@ public function refresh(Token $token): Token {
214252 'refresh_token ' => $ token ->getRefreshToken (),
215253 ]
216254 );
217- $ this ->logger ->debug ('[TokenService] Token refresh request params ' , [
218- 'client_id ' => $ oidcProvider ->getClientId (),
219- // 'client_secret' => $clientSecret,
220- 'grant_type ' => 'refresh_token ' ,
221- // 'refresh_token' => $token->getRefreshToken(),
222- ]);
223255
224256 $ bodyArray = json_decode (trim ($ body ), true , 512 , JSON_THROW_ON_ERROR );
225257 $ this ->logger ->debug ('[TokenService] ---- Refresh token success ' );
226258 return $ this ->storeToken (
227- array_merge (
228- $ bodyArray ,
229- ['provider_id ' => $ token ->getProviderId ()],
230- )
259+ array_merge ($ bodyArray , ['provider_id ' => $ token ->getProviderId ()])
231260 );
232261 } catch (\Exception $ e ) {
233- $ this ->logger ->error ('[TokenService] Failed to refresh token ' , ['exception ' => $ e ]);
234- // Failed to refresh, return old token which will be retried or otherwise timeout if expired
262+ $ this ->logger ->error ('[TokenService] Failed to refresh token ' , ['exception ' => $ e ]);
235263 return $ token ;
264+ } finally {
265+ if ($ lockAcquired ) {
266+ try {
267+ $ this ->lockingProvider ->releaseLock ($ lockKey , ILockingProvider::LOCK_EXCLUSIVE );
268+ $ this ->logger ->debug ('[TokenService] Released lock for token refresh ' );
269+ } catch (\Exception $ e ) {
270+ $ this ->logger ->error ('[TokenService] Failed to release lock ' , ['exception ' => $ e ]);
271+ }
272+ }
236273 }
237274 }
238275
@@ -316,9 +353,7 @@ public function getExchangedToken(string $targetAudience, array $extraScopes = [
316353 );
317354 $ this ->logger ->debug ('[TokenService] Token exchange request params ' , [
318355 'client_id ' => $ oidcProvider ->getClientId (),
319- // 'client_secret' => $clientSecret,
320356 'grant_type ' => 'urn:ietf:params:oauth:grant-type:token-exchange ' ,
321- // 'subject_token' => $loginToken->getAccessToken(),
322357 'subject_token_type ' => 'urn:ietf:params:oauth:token-type:access_token ' ,
323358 'requested_token_type ' => 'urn:ietf:params:oauth:token-type:refresh_token ' ,
324359 'audience ' => $ targetAudience ,
0 commit comments