Skip to content

Commit ceb14dc

Browse files
committed
Fix for a possible race condition. thephpleague#1306
1 parent 2ed9e5f commit ceb14dc

File tree

3 files changed

+118
-15
lines changed

3 files changed

+118
-15
lines changed

src/Grant/AuthCodeGrant.php

+29-15
Original file line numberDiff line numberDiff line change
@@ -141,23 +141,33 @@ public function respondToAccessTokenRequest(
141141
$this->validateCodeChallenge($authCodePayload, $codeVerifier);
142142
}
143143

144-
// Issue and persist new access token
145-
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
146-
$this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
147-
$responseType->setAccessToken($accessToken);
148-
149-
// Issue and persist new refresh token if given
150-
$refreshToken = $this->issueRefreshToken($accessToken);
144+
if ($this->authCodeRepository->lockAuthCode($authCodePayload->auth_code_id)) {
145+
try {
146+
// Issue and persist new access token
147+
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
148+
$this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
149+
$responseType->setAccessToken($accessToken);
150+
151+
// Issue and persist new refresh token if given
152+
$refreshToken = $this->issueRefreshToken($accessToken);
153+
154+
if ($refreshToken !== null) {
155+
$this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
156+
$responseType->setRefreshToken($refreshToken);
157+
}
151158

152-
if ($refreshToken !== null) {
153-
$this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
154-
$responseType->setRefreshToken($refreshToken);
159+
return $responseType;
160+
} catch (Exception $e) {
161+
$this->authCodeRepository->unlockAuthCode($authCodePayload->auth_code_id);
162+
throw OAuthServerException::serverError(
163+
'access_token',
164+
$e
165+
);
166+
} finally {
167+
// Revoke used auth code
168+
$this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
169+
}
155170
}
156-
157-
// Revoke used auth code
158-
$this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
159-
160-
return $responseType;
161171
}
162172

163173
private function validateCodeChallenge(object $authCodePayload, ?string $codeVerifier): void
@@ -213,6 +223,10 @@ private function validateAuthorizationCode(
213223
throw OAuthServerException::invalidGrant('Authorization code has been revoked');
214224
}
215225

226+
if ($this->authCodeRepository->isAuthCodeLocked($authCodePayload->auth_code_id) === true) {
227+
throw OAuthServerException::invalidGrant('Authorization code has been locked while an access code beeing issued');
228+
}
229+
216230
if ($authCodePayload->client_id !== $client->getIdentifier()) {
217231
throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
218232
}

src/Repositories/AuthCodeRepositoryInterface.php

+22
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,26 @@ public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): voi
3030
public function revokeAuthCode(string $codeId): void;
3131

3232
public function isAuthCodeRevoked(string $codeId): bool;
33+
34+
/**
35+
* Method locks an auth code in case an error occurs during the issuance of an access code.
36+
*
37+
* The storage engine should make this persistent immediately to prevent possible race conditions while issuing an access token.
38+
*
39+
* @param string $codeId
40+
*
41+
* @return void
42+
*/
43+
public function lockAuthCode(string $codeId): bool;
44+
45+
/**
46+
* This method is used to make the auth code available again after an error occurred in the access code issuance process.
47+
*
48+
* @param string $codeId
49+
*
50+
* @return void
51+
*/
52+
public function unlockAuthCode(string $codeId): void;
53+
54+
public function isAuthCodeLocked(string $codeId): bool;
3355
}

tests/Grant/AuthCodeGrantTest.php

+67
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,73 @@ public function testRespondToAccessTokenRequestRevokedCode(): void
13871387
}
13881388
}
13891389

1390+
public function testRespondToAccessTokenRequestLockedCode(): void
1391+
{
1392+
$client = new ClientEntity();
1393+
1394+
$client->setIdentifier('foo');
1395+
$client->setRedirectUri(self::REDIRECT_URI);
1396+
$client->setConfidential();
1397+
1398+
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
1399+
1400+
$clientRepositoryMock->method('getClientEntity')->willReturn($client);
1401+
$clientRepositoryMock->method('validateClient')->willReturn(true);
1402+
1403+
$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
1404+
$accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
1405+
1406+
$refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
1407+
$refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf();
1408+
1409+
$authCodeRepositoryMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock();
1410+
$authCodeRepositoryMock->method('isAuthCodeLocked')->willReturn(true);
1411+
1412+
$grant = new AuthCodeGrant(
1413+
$authCodeRepositoryMock,
1414+
$this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(),
1415+
new DateInterval('PT10M')
1416+
);
1417+
$grant->setClientRepository($clientRepositoryMock);
1418+
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
1419+
$grant->setRefreshTokenRepository($refreshTokenRepositoryMock);
1420+
$grant->setEncryptionKey($this->cryptStub->getKey());
1421+
1422+
$request = new ServerRequest(
1423+
[],
1424+
[],
1425+
null,
1426+
'POST',
1427+
'php://input',
1428+
[],
1429+
[],
1430+
[],
1431+
[
1432+
'grant_type' => 'authorization_code',
1433+
'client_id' => 'foo',
1434+
'redirect_uri' => self::REDIRECT_URI,
1435+
'code' => $this->cryptStub->doEncrypt(
1436+
json_encode([
1437+
'auth_code_id' => uniqid(),
1438+
'expire_time' => time() + 3600,
1439+
'client_id' => 'foo',
1440+
'user_id' => 123,
1441+
'scopes' => ['foo'],
1442+
'redirect_uri' => 'http://foo/bar',
1443+
], JSON_THROW_ON_ERROR)
1444+
),
1445+
]
1446+
);
1447+
1448+
try {
1449+
/* @var StubResponseType $response */
1450+
$grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M'));
1451+
} catch (OAuthServerException $e) {
1452+
self::assertEquals($e->getHint(), 'Authorization code has been revoked');
1453+
self::assertEquals($e->getErrorType(), 'invalid_grant');
1454+
}
1455+
}
1456+
13901457
public function testRespondToAccessTokenRequestClientMismatch(): void
13911458
{
13921459
$client = new ClientEntity();

0 commit comments

Comments
 (0)