Skip to content

Commit 75e9dc2

Browse files
committed
Handle exceptions when tokens are revoked
1 parent dd5fd41 commit 75e9dc2

File tree

2 files changed

+90
-4
lines changed

2 files changed

+90
-4
lines changed

src/Connection.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Sendy\Api;
44

55
use Psr\Http\Message\UriInterface;
6+
use Sendy\Api\Exceptions\ClientException;
67
use Sendy\Api\Exceptions\SendyException;
78
use Sendy\Api\Http\Request;
89
use Sendy\Api\Http\Response;
@@ -244,10 +245,21 @@ private function acquireAccessToken(): void
244245
];
245246
}
246247

247-
$body = $this->performRequest(
248-
$this->createRequest('POST', self::BASE_URL . self::TOKEN_URL, json_encode($parameters)),
249-
false,
250-
);
248+
try {
249+
$body = $this->performRequest(
250+
$this->createRequest('POST', self::BASE_URL . self::TOKEN_URL, json_encode($parameters)),
251+
false,
252+
);
253+
} catch (ClientException $exception) {
254+
// When the refresh token was refreshed in another process, it could occur that the connection still holds
255+
// a valid refresh token with a revoked refresh token. When this occurs it is safe to use the valid access
256+
// token.
257+
if ($this->refreshTokenIsRevoked($exception->getResponse()) && $this->tokenExpires > time()) {
258+
return;
259+
}
260+
261+
throw $exception;
262+
}
251263

252264
$this->accessToken = $body['access_token'];
253265
$this->refreshToken = $body['refresh_token'];
@@ -439,4 +451,22 @@ public function __get(string $resource): Resource
439451

440452
return new $className($this);
441453
}
454+
455+
/**
456+
* @throws Exceptions\JsonException
457+
*/
458+
private function refreshTokenIsRevoked(Response $response): bool
459+
{
460+
if ($response->getStatusCode() !== 400) {
461+
return false;
462+
}
463+
464+
$body = $response->getDecodedBody();
465+
466+
if (! isset($body['error'], $body['hint'])) {
467+
return false;
468+
}
469+
470+
return $body['error'] === 'invalid_grant' && $body['hint'] === 'Token has been revoked';
471+
}
442472
}

tests/ConnectionTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use PHPUnit\Framework\TestCase;
66
use Sendy\Api\ApiException;
77
use Sendy\Api\Connection;
8+
use Sendy\Api\Exceptions\ClientException;
9+
use Sendy\Api\Exceptions\SendyException;
810
use Sendy\Api\Http\Request;
911
use Sendy\Api\Http\Response;
1012
use Sendy\Api\Http\Transport\MockTransport;
@@ -212,6 +214,60 @@ public function testTokensAreAcquiredWithRefreshToken(): void
212214
);
213215
}
214216

217+
public function testRevokedRefreshTokenIsHandled(): void
218+
{
219+
$connection = new Connection();
220+
221+
$transport = new MockTransport(
222+
new Response(400, [], json_encode([
223+
'error' => 'invalid_grant',
224+
'hint' => 'Token has been revoked',
225+
])),
226+
);
227+
228+
$connection->setTransport($transport);
229+
230+
$connection->setOauthClient(true);
231+
$connection->setClientId('clientId');
232+
$connection->setAccessToken('accessToken');
233+
$connection->setClientSecret('clientSecret');
234+
$connection->setRefreshToken('RefreshToken');
235+
$connection->setTokenExpires(time() + 5);
236+
237+
try {
238+
$connection->checkOrAcquireAccessToken();
239+
} catch (SendyException $exception) {
240+
$this->fail($exception->getMessage());
241+
}
242+
243+
$this->assertTrue(true);
244+
}
245+
246+
public function testUnexpectedExceptionWhenRefreshingTokensAreHandled(): void
247+
{
248+
$connection = new Connection();
249+
250+
$transport = new MockTransport(
251+
new Response(400, [], json_encode([
252+
'error' => 'unknown_error',
253+
'hint' => 'Unknown error',
254+
])),
255+
);
256+
257+
$connection->setTransport($transport);
258+
259+
$connection->setOauthClient(true);
260+
$connection->setClientId('clientId');
261+
$connection->setAccessToken('accessToken');
262+
$connection->setClientSecret('clientSecret');
263+
$connection->setRefreshToken('RefreshToken');
264+
$connection->setTokenExpires(time() + 5);
265+
266+
$this->expectException(ClientException::class);
267+
268+
$connection->checkOrAcquireAccessToken();
269+
}
270+
215271
public function testTokenUpdateCallbackIsCalled(): void
216272
{
217273
$connection = new Connection();

0 commit comments

Comments
 (0)