Skip to content

Commit 676a315

Browse files
committed
Merge branch 'lynx-319' of github.com:sivaschenko/magento2ce into LYNX-311-DELIVERY
2 parents ad7dfa8 + 4463a29 commit 676a315

File tree

7 files changed

+344
-11
lines changed

7 files changed

+344
-11
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2023 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ***********************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\CustomerGraphQl\Controller\HttpRequestValidator;
20+
21+
use Magento\Framework\App\HttpRequestInterface;
22+
use Magento\Framework\Exception\AuthorizationException;
23+
use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException;
24+
use Magento\GraphQl\Controller\HttpRequestValidatorInterface;
25+
use Magento\Integration\Api\Exception\UserTokenException;
26+
use Magento\Integration\Api\UserTokenReaderInterface;
27+
use Magento\Integration\Api\UserTokenValidatorInterface;
28+
29+
/**
30+
* Validate the token if it is present in headers
31+
*/
32+
class AuthorizationRequestValidator implements HttpRequestValidatorInterface
33+
{
34+
/**
35+
* @param UserTokenReaderInterface $tokenReader
36+
* @param UserTokenValidatorInterface $tokenValidator
37+
*/
38+
public function __construct(
39+
private readonly UserTokenReaderInterface $tokenReader,
40+
private readonly UserTokenValidatorInterface $tokenValidator
41+
) {
42+
}
43+
44+
/**
45+
* Validate the authorization header bearer token if it is set
46+
*
47+
* @param HttpRequestInterface $request
48+
* @return void
49+
* @throws GraphQlAuthenticationException
50+
*/
51+
public function validate(HttpRequestInterface $request): void
52+
{
53+
$authorizationHeaderValue = $request->getHeader('Authorization');
54+
if (!$authorizationHeaderValue) {
55+
return;
56+
}
57+
58+
$headerPieces = explode(' ', $authorizationHeaderValue);
59+
if (count($headerPieces) !== 2) {
60+
return;
61+
}
62+
63+
if (strtolower(reset($headerPieces)) !== 'bearer') {
64+
return;
65+
}
66+
67+
$bearerToken = end($headerPieces);
68+
try {
69+
$token = $this->userTokenReader->read($bearerToken);
70+
} catch (UserTokenException $exception) {
71+
throw new GraphQlAuthenticationException(__($exception->getMessage()));
72+
}
73+
74+
try {
75+
$this->userTokenValidator->validate($token);
76+
} catch (AuthorizationException $exception) {
77+
throw new GraphQlAuthenticationException(__($exception->getMessage()));
78+
}
79+
}
80+
}

app/code/Magento/CustomerGraphQl/etc/graphql/di.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,11 @@
214214
</argument>
215215
</arguments>
216216
</type>
217+
<type name="Magento\GraphQl\Controller\HttpRequestProcessor">
218+
<arguments>
219+
<argument name="requestValidators" xsi:type="array">
220+
<item name="authorizationValidator" xsi:type="object">Magento\CustomerGraphQl\Controller\HttpRequestValidator\AuthorizationRequestValidator</item>
221+
</argument>
222+
</arguments>
223+
</type>
217224
</config>

app/code/Magento/GraphQl/Controller/GraphQl.php

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
use Magento\Framework\App\ResponseInterface;
2020
use Magento\Framework\Controller\Result\JsonFactory;
2121
use Magento\Framework\GraphQl\Exception\ExceptionFormatter;
22+
use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException;
23+
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
2224
use Magento\Framework\GraphQl\Query\Fields as QueryFields;
2325
use Magento\Framework\GraphQl\Query\QueryParser;
2426
use Magento\Framework\GraphQl\Query\QueryProcessor;
@@ -181,10 +183,9 @@ public function dispatch(RequestInterface $request): ResponseInterface
181183
{
182184
$this->areaList->getArea(Area::AREA_GRAPHQL)->load(Area::PART_TRANSLATE);
183185

184-
$statusCode = 200;
185186
$jsonResult = $this->jsonFactory->create();
186187
$data = $this->getDataFromRequest($request);
187-
$result = [];
188+
$result = ['errors' => []];
188189

189190
$schema = null;
190191
try {
@@ -205,8 +206,14 @@ public function dispatch(RequestInterface $request): ResponseInterface
205206
$this->contextFactory->create(),
206207
$data['variables'] ?? []
207208
);
209+
$statusCode = $this->getHttpResponseCode($result);
210+
} catch (GraphQlAuthenticationException $error) {
211+
$result['errors'][] = $this->graphQlError->create($error);
212+
$statusCode = 401;
213+
} catch (GraphQlAuthorizationException $error) {
214+
$result['errors'][] = $this->graphQlError->create($error);
215+
$statusCode = 403;
208216
} catch (\Exception $error) {
209-
$result['errors'] = isset($result['errors']) ? $result['errors'] : [];
210217
$result['errors'][] = $this->graphQlError->create($error);
211218
$statusCode = ExceptionFormatter::HTTP_GRAPH_QL_SCHEMA_ERROR_STATUS;
212219
}
@@ -224,6 +231,32 @@ public function dispatch(RequestInterface $request): ResponseInterface
224231
return $this->httpResponse;
225232
}
226233

234+
/**
235+
* Retrieve http response code based on the error categories
236+
*
237+
* @param array $result
238+
* @return int
239+
*/
240+
private function getHttpResponseCode(array $result): int
241+
{
242+
if (empty($result['errors'])) {
243+
return 200;
244+
}
245+
foreach ($result['errors'] as $error) {
246+
if (!isset($error['extensions']['category'])) {
247+
continue;
248+
}
249+
switch ($error['extensions']['category']) {
250+
case GraphQlAuthenticationException::EXCEPTION_CATEGORY:
251+
return 401;
252+
case GraphQlAuthorizationException::EXCEPTION_CATEGORY:
253+
return 403;
254+
}
255+
}
256+
257+
return 200;
258+
}
259+
227260
/**
228261
* Get data from request body or query string
229262
*
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2023 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ***********************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\GraphQl\Customer;
20+
21+
use Magento\Customer\Api\CustomerRepositoryInterface;
22+
use Magento\Customer\Api\Data\CustomerInterface;
23+
use Magento\Customer\Test\Fixture\Customer;
24+
use Magento\Integration\Api\CustomerTokenServiceInterface;
25+
use Magento\TestFramework\Fixture\DataFixture;
26+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
27+
use Magento\TestFramework\Helper\Bootstrap;
28+
use Magento\TestFramework\TestCase\GraphQlAbstract;
29+
use Magento\TestFramework\TestCase\HttpClient\CurlClient;
30+
31+
/**
32+
* Test customer authentication responses
33+
*/
34+
class AuthenticationTest extends GraphQlAbstract
35+
{
36+
private const QUERY_ACCESSIBLE_BY_GUEST = <<<QUERY
37+
{
38+
isEmailAvailable(email: "[email protected]") {
39+
is_email_available
40+
}
41+
}
42+
QUERY;
43+
44+
private const QUERY_REQUIRE_AUTHENTICATION = <<<QUERY
45+
{
46+
customer {
47+
email
48+
}
49+
}
50+
QUERY;
51+
52+
/**
53+
* @var CustomerTokenServiceInterface
54+
*/
55+
private $tokenService;
56+
57+
protected function setUp(): void
58+
{
59+
$this->tokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class);
60+
}
61+
62+
public function testNoToken()
63+
{
64+
$response = $this->graphQlQuery(self::QUERY_ACCESSIBLE_BY_GUEST);
65+
66+
self::assertArrayHasKey('isEmailAvailable', $response);
67+
self::assertArrayHasKey('is_email_available', $response['isEmailAvailable']);
68+
}
69+
70+
public function testInvalidToken()
71+
{
72+
$this->expectExceptionCode(401);
73+
Bootstrap::getObjectManager()->get(CurlClient::class)->get(
74+
rtrim(TESTS_BASE_URL, '/') . '/graphql',
75+
[
76+
'query' => self::QUERY_ACCESSIBLE_BY_GUEST
77+
],
78+
[
79+
'Authorization: Bearer invalid_token'
80+
]
81+
);
82+
}
83+
84+
#[
85+
DataFixture(Customer::class, as: 'customer'),
86+
]
87+
public function testRevokedTokenPublicQuery()
88+
{
89+
/** @var CustomerInterface $customer */
90+
$customer = DataFixtureStorageManager::getStorage()->get('customer');
91+
$token = $this->tokenService->createCustomerAccessToken($customer->getEmail(), 'password');
92+
93+
$response = $this->graphQlQuery(
94+
self::QUERY_ACCESSIBLE_BY_GUEST,
95+
[],
96+
'',
97+
[
98+
'Authorization' => 'Bearer ' . $token
99+
]
100+
);
101+
102+
self::assertArrayHasKey('isEmailAvailable', $response);
103+
self::assertArrayHasKey('is_email_available', $response['isEmailAvailable']);
104+
105+
$this->tokenService->revokeCustomerAccessToken($customer->getId());
106+
107+
$this->expectExceptionCode(401);
108+
Bootstrap::getObjectManager()->get(CurlClient::class)->get(
109+
rtrim(TESTS_BASE_URL, '/') . '/graphql',
110+
[
111+
'query' => self::QUERY_ACCESSIBLE_BY_GUEST
112+
],
113+
[
114+
'Authorization: Bearer ' . $token
115+
]
116+
);
117+
}
118+
119+
#[
120+
DataFixture(Customer::class, as: 'customer'),
121+
]
122+
public function testRevokedTokenProtectedQuery()
123+
{
124+
/** @var CustomerInterface $customer */
125+
$customer = DataFixtureStorageManager::getStorage()->get('customer');
126+
$token = $this->tokenService->createCustomerAccessToken($customer->getEmail(), 'password');
127+
128+
$response = $this->graphQlQuery(
129+
self::QUERY_REQUIRE_AUTHENTICATION,
130+
[],
131+
'',
132+
[
133+
'Authorization' => 'Bearer ' . $token
134+
]
135+
);
136+
137+
self::assertEquals(
138+
[
139+
'customer' => [
140+
'email' => $customer->getEmail()
141+
]
142+
],
143+
$response
144+
);
145+
146+
$this->tokenService->revokeCustomerAccessToken($customer->getId());
147+
148+
$this->expectExceptionCode(401);
149+
Bootstrap::getObjectManager()->get(CurlClient::class)->get(
150+
rtrim(TESTS_BASE_URL, '/') . '/graphql',
151+
[
152+
'query' => self::QUERY_REQUIRE_AUTHENTICATION
153+
],
154+
[
155+
'Authorization: Bearer ' . $token
156+
]
157+
);
158+
}
159+
160+
#[
161+
DataFixture(Customer::class, as: 'customer'),
162+
DataFixture(
163+
Customer::class,
164+
[
165+
'addresses' => [
166+
[
167+
'country_id' => 'US',
168+
'region_id' => 32,
169+
'city' => 'Boston',
170+
'street' => ['10 Milk Street'],
171+
'postcode' => '02108',
172+
'telephone' => '1234567890',
173+
'default_billing' => true,
174+
'default_shipping' => true
175+
]
176+
]
177+
],
178+
as: 'customer2'
179+
),
180+
]
181+
public function testForbidden()
182+
{
183+
/** @var CustomerInterface $customer2 */
184+
$customer2Data = DataFixtureStorageManager::getStorage()->get('customer2');
185+
$customer2 = Bootstrap::getObjectManager()
186+
->get(CustomerRepositoryInterface::class)
187+
->get($customer2Data->getEmail());
188+
$addressId = $customer2->getDefaultBilling();
189+
$mutation
190+
= <<<MUTATION
191+
mutation {
192+
deleteCustomerAddress(id: {$addressId})
193+
}
194+
MUTATION;
195+
196+
/** @var CustomerInterface $customer */
197+
$customer = DataFixtureStorageManager::getStorage()->get('customer');
198+
$token = $this->tokenService->createCustomerAccessToken($customer->getEmail(), 'password');
199+
200+
$this->expectExceptionCode(403);
201+
Bootstrap::getObjectManager()->get(CurlClient::class)->post(
202+
rtrim(TESTS_BASE_URL, '/') . '/graphql',
203+
json_encode(['query' => $mutation]),
204+
[
205+
'Authorization: Bearer ' . $token,
206+
'Accept: application/json',
207+
'Content-Type: application/json'
208+
]
209+
);
210+
}
211+
}

dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerGraphQl/Model/Resolver/CustomerTest.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
use Magento\TestFramework\Fixture\DataFixture;
2727
use Magento\TestFramework\Helper\Bootstrap;
2828
use Magento\TestFramework\TestCase\GraphQl\ResolverCacheAbstract;
29-
use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException;
3029

3130
/**
3231
* Test for customer resolver cache
@@ -542,7 +541,7 @@ public function testGuestQueryingCustomerDoesNotGenerateResolverCacheEntry()
542541
$query
543542
);
544543
$this->fail('Expected exception not thrown');
545-
} catch (ResponseContainsErrorsException $e) {
544+
} catch (\Exception $e) {
546545
// expected exception
547546
}
548547

0 commit comments

Comments
 (0)