|
13 | 13 | use OCA\UserOIDC\AppInfo\Application; |
14 | 14 | use OCA\UserOIDC\Db\ProviderMapper; |
15 | 15 | use OCA\UserOIDC\Exception\TokenExchangeFailedException; |
| 16 | +use OCA\UserOIDC\Exception\TokenRefreshLockedException; |
16 | 17 | use OCA\UserOIDC\Helper\HttpClientHelper; |
17 | 18 | use OCA\UserOIDC\Model\Token; |
18 | 19 | use OCA\UserOIDC\Vendor\Firebase\JWT\JWT; |
|
38 | 39 | class TokenService { |
39 | 40 |
|
40 | 41 | private const SESSION_TOKEN_KEY = Application::APP_ID . '-user-token'; |
| 42 | + private const REFRESH_LOCK_KEY = Application::APP_ID . '-refresh-lock'; |
41 | 43 |
|
42 | 44 | private IClient $client; |
43 | 45 |
|
@@ -122,7 +124,12 @@ public function checkLoginToken(): void { |
122 | 124 | return; |
123 | 125 | } |
124 | 126 |
|
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 | + } |
126 | 133 | if ($token === null) { |
127 | 134 | $this->logger->debug('[TokenService] checkLoginToken: token is null'); |
128 | 135 | // if we don't have a token but we had one once, |
@@ -152,11 +159,20 @@ public function reauthenticate(int $providerId) { |
152 | 159 | /** |
153 | 160 | * @param Token $token |
154 | 161 | * @return Token |
155 | | - * @throws \JsonException |
156 | 162 | * @throws DoesNotExistException |
157 | 163 | * @throws MultipleObjectsReturnedException |
| 164 | + * @throws TokenRefreshLockedException |
| 165 | + * @throws \JsonException |
158 | 166 | */ |
159 | 167 | 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 | + |
160 | 176 | $oidcProvider = $this->providerMapper->getProvider($token->getProviderId()); |
161 | 177 | $discovery = $this->discoveryService->obtainDiscovery($oidcProvider); |
162 | 178 |
|
@@ -188,13 +204,16 @@ public function refresh(Token $token): Token { |
188 | 204 |
|
189 | 205 | $bodyArray = json_decode(trim($body), true, 512, JSON_THROW_ON_ERROR); |
190 | 206 | $this->logger->debug('[TokenService] ---- Refresh token success'); |
191 | | - return $this->storeToken( |
| 207 | + $refreshedToken = $this->storeToken( |
192 | 208 | array_merge( |
193 | 209 | $bodyArray, |
194 | 210 | ['provider_id' => $token->getProviderId()], |
195 | 211 | ) |
196 | 212 | ); |
| 213 | + $this->session->remove(self::REFRESH_LOCK_KEY); |
| 214 | + return $refreshedToken; |
197 | 215 | } catch (\Exception $e) { |
| 216 | + $this->session->remove(self::REFRESH_LOCK_KEY); |
198 | 217 | $this->logger->error('[TokenService] Failed to refresh token ', ['exception' => $e]); |
199 | 218 | // Failed to refresh, return old token which will be retried or otherwise timeout if expired |
200 | 219 | return $token; |
@@ -229,7 +248,12 @@ public function getExchangedToken(string $targetAudience, array $extraScopes = [ |
229 | 248 | } |
230 | 249 | $this->logger->debug('[TokenService] Starting token exchange'); |
231 | 250 |
|
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 | + } |
233 | 257 | if ($loginToken === null) { |
234 | 258 | $this->logger->debug('[TokenService] Failed to exchange token, no login token found in the session'); |
235 | 259 | throw new TokenExchangeFailedException('Failed to exchange token, no login token found in the session'); |
|
0 commit comments