Skip to content

Commit 12b02ed

Browse files
committed
AC-1619: Integration access tokens do not work as Bearer tokens
1 parent f95efe4 commit 12b02ed

File tree

11 files changed

+388
-18
lines changed

11 files changed

+388
-18
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Integration\Model;
10+
11+
use Magento\Integration\Api\Data\UserToken;
12+
use Magento\Integration\Api\Exception\UserTokenException;
13+
use Magento\Integration\Api\UserTokenReaderInterface;
14+
15+
/**
16+
* Checks multiple sources for reading a token
17+
*/
18+
class CompositeTokenReader implements UserTokenReaderInterface
19+
{
20+
/**
21+
* @var UserTokenReaderInterface[]
22+
*/
23+
private $readers;
24+
25+
/**
26+
* @param UserTokenReaderInterface[] $readers
27+
*/
28+
public function __construct(array $readers)
29+
{
30+
$this->readers = $readers;
31+
}
32+
33+
/**
34+
* @inheritDoc
35+
*/
36+
public function read(string $token): UserToken
37+
{
38+
foreach ($this->readers as $reader) {
39+
try {
40+
return $reader->read($token);
41+
} catch (UserTokenException $exception) {
42+
continue;
43+
}
44+
}
45+
46+
throw new UserTokenException('Composite reader could not read a token');
47+
}
48+
}

app/code/Magento/Integration/Model/OpaqueToken/Reader.php

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,20 @@
88

99
namespace Magento\Integration\Model\OpaqueToken;
1010

11+
use Magento\Framework\App\ObjectManager;
1112
use Magento\Integration\Api\Data\UserToken;
1213
use Magento\Integration\Api\Exception\UserTokenException;
14+
use Magento\Integration\Api\IntegrationServiceInterface;
1315
use Magento\Integration\Api\UserTokenReaderInterface;
1416
use Magento\Integration\Model\CustomUserContext;
1517
use Magento\Integration\Model\Oauth\Token;
1618
use Magento\Integration\Model\Oauth\TokenFactory;
1719
use Magento\Integration\Helper\Oauth\Data as OauthHelper;
20+
use Magento\Webapi\Model\Authorization\AuthorizationConfig;
1821

22+
/**
23+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
24+
*/
1925
class Reader implements UserTokenReaderInterface
2026
{
2127
/**
@@ -28,22 +34,67 @@ class Reader implements UserTokenReaderInterface
2834
*/
2935
private $helper;
3036

37+
/**
38+
* @var AuthorizationConfig
39+
*/
40+
private $authorizationConfig;
41+
42+
/**
43+
* @var IntegrationServiceInterface
44+
*/
45+
private IntegrationServiceInterface $integrationService;
46+
3147
/**
3248
* @param TokenFactory $tokenFactory
3349
* @param OauthHelper $helper
50+
* @param AuthorizationConfig|null $authorizationConfig
51+
* @param IntegrationServiceInterface|null $integrationService
3452
*/
35-
public function __construct(TokenFactory $tokenFactory, OauthHelper $helper)
36-
{
53+
public function __construct(
54+
TokenFactory $tokenFactory,
55+
OauthHelper $helper,
56+
?AuthorizationConfig $authorizationConfig = null,
57+
?IntegrationServiceInterface $integrationService = null
58+
) {
3759
$this->tokenFactory = $tokenFactory;
3860
$this->helper = $helper;
61+
$this->authorizationConfig = $authorizationConfig ?? ObjectManager::getInstance()
62+
->get(AuthorizationConfig::class);
63+
$this->integrationService = $integrationService ?? ObjectManager::getInstance()
64+
->get(IntegrationServiceInterface::class);
3965
}
4066

4167
/**
4268
* @inheritDoc
4369
*/
4470
public function read(string $token): UserToken
4571
{
46-
/** @var Token $tokenModel */
72+
73+
$tokenModel = $this->getTokenModel($token);
74+
$userType = (int) $tokenModel->getUserType();
75+
$this->validateUserType($userType);
76+
$userId = $this->getUserId($tokenModel);
77+
78+
$issued = \DateTimeImmutable::createFromFormat(
79+
'Y-m-d H:i:s',
80+
$tokenModel->getCreatedAt(),
81+
new \DateTimeZone('UTC')
82+
);
83+
$lifetimeHours = $userType === CustomUserContext::USER_TYPE_ADMIN
84+
? $this->helper->getAdminTokenLifetime() : $this->helper->getCustomerTokenLifetime();
85+
$expires = $issued->add(new \DateInterval("PT{$lifetimeHours}H"));
86+
87+
return new UserToken(new CustomUserContext((int) $userId, (int) $userType), new Data($issued, $expires));
88+
}
89+
90+
/**
91+
* Create the token model from the input
92+
*
93+
* @param string $token
94+
* @return Token
95+
*/
96+
private function getTokenModel(string $token): Token
97+
{
4798
$tokenModel = $this->tokenFactory->create();
4899
$tokenModel = $tokenModel->load($token, 'token');
49100

@@ -53,27 +104,49 @@ public function read(string $token): UserToken
53104
if ($tokenModel->getRevoked()) {
54105
throw new UserTokenException('Token was revoked');
55106
}
56-
$userType = (int) $tokenModel->getUserType();
57-
if ($userType !== CustomUserContext::USER_TYPE_ADMIN && $userType !== CustomUserContext::USER_TYPE_CUSTOMER) {
107+
108+
return $tokenModel;
109+
}
110+
111+
/**
112+
* Validate the given user type
113+
*
114+
* @param int $userType
115+
*/
116+
private function validateUserType(int $userType): void
117+
{
118+
if ($userType === CustomUserContext::USER_TYPE_INTEGRATION) {
119+
if (!$this->authorizationConfig->isIntegrationAsBearerEnabled()) {
120+
throw new UserTokenException('Invalid token found');
121+
}
122+
} elseif ($userType !== CustomUserContext::USER_TYPE_ADMIN
123+
&& $userType !== CustomUserContext::USER_TYPE_CUSTOMER
124+
) {
58125
throw new UserTokenException('Invalid token found');
59126
}
127+
}
128+
129+
/**
130+
* Determine the user id for a given token
131+
*
132+
* @param Token $tokenModel
133+
* @return int
134+
*/
135+
private function getUserId(Token $tokenModel): int
136+
{
137+
$userType = (int)$tokenModel->getUserType();
138+
60139
if ($userType === CustomUserContext::USER_TYPE_ADMIN) {
61140
$userId = $tokenModel->getAdminId();
141+
} elseif ($userType === CustomUserContext::USER_TYPE_INTEGRATION) {
142+
$userId = $this->integrationService->findByConsumerId($tokenModel->getConsumerId())->getId();
62143
} else {
63144
$userId = $tokenModel->getCustomerId();
64145
}
65146
if (!$userId) {
66147
throw new UserTokenException('Invalid token found');
67148
}
68-
$issued = \DateTimeImmutable::createFromFormat(
69-
'Y-m-d H:i:s',
70-
$tokenModel->getCreatedAt(),
71-
new \DateTimeZone('UTC')
72-
);
73-
$lifetimeHours = $userType === CustomUserContext::USER_TYPE_ADMIN
74-
? $this->helper->getAdminTokenLifetime() : $this->helper->getCustomerTokenLifetime();
75-
$expires = $issued->add(new \DateInterval("PT{$lifetimeHours}H"));
76149

77-
return new UserToken(new CustomUserContext((int) $userId, (int) $userType), new Data($issued, $expires));
150+
return $userId;
78151
}
79152
}

app/code/Magento/Integration/Model/UserToken/ExpirationValidator.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace Magento\Integration\Model\UserToken;
1010

11+
use Magento\Authorization\Model\UserContextInterface;
1112
use Magento\Framework\Exception\AuthorizationException;
1213
use Magento\Integration\Api\Data\UserToken;
1314
use Magento\Integration\Api\UserTokenValidatorInterface;
@@ -33,7 +34,9 @@ public function __construct(DtUtil $datetimeUtil)
3334
*/
3435
public function validate(UserToken $token): void
3536
{
36-
if ($token->getData()->getExpires()->getTimestamp() <= $this->datetimeUtil->gmtTimestamp()) {
37+
if ($token->getUserContext()->getUserType() !== UserContextInterface::USER_TYPE_INTEGRATION
38+
&& $token->getData()->getExpires()->getTimestamp() <= $this->datetimeUtil->gmtTimestamp()
39+
) {
3740
throw new AuthorizationException(__('Consumer key has expired'));
3841
}
3942
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Integration\Test\Unit\Model;
10+
11+
use Magento\Integration\Api\Data\UserToken;
12+
use Magento\Integration\Api\Exception\UserTokenException;
13+
use Magento\Integration\Api\UserTokenReaderInterface;
14+
use Magento\Integration\Model\CompositeTokenReader;
15+
use PHPUnit\Framework\TestCase;
16+
17+
class CompositeTokenReaderTest extends TestCase
18+
{
19+
public function testCompositeReaderReturnsFirstToken()
20+
{
21+
$token1 = $this->createMock(UserToken::class);
22+
$reader1 = $this->createMock(UserTokenReaderInterface::class);
23+
$reader1->method('read')
24+
->with('abc')
25+
->willReturn($token1);
26+
27+
$token2 = $this->createMock(UserToken::class);
28+
$reader2 = $this->createMock(UserTokenReaderInterface::class);
29+
$reader2->method('read')
30+
->with('abc')
31+
->willReturn($token2);
32+
33+
$composite = new CompositeTokenReader([$reader1, $reader2]);
34+
35+
self::assertSame($token1, $composite->read('abc'));
36+
}
37+
38+
public function testCompositeReaderReturnsNextTokenOnError()
39+
{
40+
$reader1 = $this->createMock(UserTokenReaderInterface::class);
41+
$reader1->method('read')
42+
->with('abc')
43+
->willThrowException(new UserTokenException('Fail'));
44+
45+
$token2 = $this->createMock(UserToken::class);
46+
$reader2 = $this->createMock(UserTokenReaderInterface::class);
47+
$reader2->method('read')
48+
->with('abc')
49+
->willReturn($token2);
50+
51+
$composite = new CompositeTokenReader([$reader1, $reader1, $reader2]);
52+
53+
self::assertSame($token2, $composite->read('abc'));
54+
}
55+
56+
public function testCompositeReaderFailsWhenNoTokensFound()
57+
{
58+
$this->expectExceptionMessage('Composite reader could not read a token');
59+
$this->expectException(UserTokenException::class);
60+
61+
$reader1 = $this->createMock(UserTokenReaderInterface::class);
62+
$reader1->method('read')
63+
->with('abc')
64+
->willThrowException(new UserTokenException('Fail'));
65+
66+
$composite = new CompositeTokenReader([$reader1, $reader1, $reader1]);
67+
$composite->read('abc');
68+
}
69+
}

app/code/Magento/Integration/Test/Unit/Model/UserToken/ExpirationValidatorTest.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace Magento\Integration\Test\Unit\Model\UserToken;
1010

11+
use Magento\Authorization\Model\UserContextInterface;
1112
use Magento\Framework\Exception\AuthorizationException;
1213
use Magento\Integration\Api\Data\UserToken;
1314
use Magento\Integration\Api\Data\UserTokenDataInterface;
@@ -62,10 +63,22 @@ public function getUserTokens(): array
6263
->willReturn(\DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2021-04-07 16:00:00'));
6364
$futureToken->method('getData')->willReturn($futureData);
6465

66+
$integrationToken = $this->createMock(UserToken::class);
67+
$userContext = $this->createMock(UserContextInterface::class);
68+
$userContext->method('getUserType')
69+
->willReturn(UserContextInterface::USER_TYPE_INTEGRATION);
70+
$integrationData = $this->createMock(UserTokenDataInterface::class);
71+
$integrationData->method('getExpires')
72+
->willReturn(\DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2021-04-07 12:00:00'));
73+
$integrationToken->method('getData')->willReturn($pastData);
74+
$integrationToken->method('getUserContext')
75+
->willReturn($userContext);
76+
6577
return [
6678
'past' => [$pastToken, false, $currentTs],
6779
'exact' => [$exactToken, false, $currentTs],
68-
'future' => [$futureToken, true, $currentTs]
80+
'future' => [$futureToken, true, $currentTs],
81+
'integration' => [$integrationToken, true, $currentTs],
6982
];
7083
}
7184

app/code/Magento/Integration/etc/di.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@
4545
<preference for="Magento\Integration\Api\Data\UserTokenParametersInterface" type="Magento\Integration\Model\UserToken\UserTokenParameters" />
4646
<preference for="Magento\Integration\Api\UserTokenValidatorInterface" type="Magento\Integration\Model\CompositeUserTokenValidator" />
4747
<preference for="Magento\Integration\Api\UserTokenIssuerInterface" type="Magento\Integration\Model\OpaqueToken\Issuer" />
48-
<preference for="Magento\Integration\Api\UserTokenReaderInterface" type="Magento\Integration\Model\OpaqueToken\Reader" />
48+
<preference for="Magento\Integration\Api\UserTokenReaderInterface" type="Magento\Integration\Model\CompositeTokenReader" />
4949
<preference for="Magento\Integration\Api\UserTokenRevokerInterface" type="Magento\Integration\Model\OpaqueToken\Revoker" />
50+
<type name="Magento\Integration\Model\CompositeTokenReader">
51+
<arguments>
52+
<argument name="readers" xsi:type="array">
53+
<item name="5" xsi:type="object">Magento\Integration\Model\OpaqueToken\Reader</item>
54+
</argument>
55+
</arguments>
56+
</type>
5057
</config>

app/code/Magento/JwtUserToken/etc/di.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
-->
88
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
99
<preference for="Magento\Integration\Api\UserTokenIssuerInterface" type="Magento\JwtUserToken\Model\Issuer" />
10-
<preference for="Magento\Integration\Api\UserTokenReaderInterface" type="Magento\JwtUserToken\Model\Reader" />
10+
<preference for="Magento\Integration\Api\UserTokenReaderInterface" type="Magento\Integration\Model\CompositeTokenReader" />
1111
<preference for="Magento\Integration\Api\UserTokenRevokerInterface" type="Magento\JwtUserToken\Model\Revoker" />
1212
<preference for="Magento\JwtUserToken\Model\JwtSettingsProviderInterface" type="Magento\JwtUserToken\Model\ConfigurableJwtSettingsProvider" />
1313
<preference for="Magento\JwtUserToken\Api\ConfigReaderInterface" type="Magento\JwtUserToken\Model\Config\ConfigReader" />
@@ -24,4 +24,11 @@
2424
</argument>
2525
</arguments>
2626
</type>
27+
<type name="Magento\Integration\Model\CompositeTokenReader">
28+
<arguments>
29+
<argument name="readers" xsi:type="array">
30+
<item name="10" xsi:type="object">Magento\JwtUserToken\Model\Reader</item>
31+
</argument>
32+
</arguments>
33+
</type>
2734
</config>

0 commit comments

Comments
 (0)