Skip to content

Commit cde232d

Browse files
author
ogorkun
committed
MC-38539: Introduce JWT wrapper
1 parent 64c2f94 commit cde232d

File tree

10 files changed

+744
-19
lines changed

10 files changed

+744
-19
lines changed

dev/tests/integration/testsuite/Magento/Framework/Jwt/JwtManagerTest.php

Lines changed: 111 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88

99
namespace Magento\Framework\Jwt;
1010

11+
use Magento\Framework\Jwt\Claim\ExpirationTime;
12+
use Magento\Framework\Jwt\Claim\IssuedAt;
13+
use Magento\Framework\Jwt\Claim\Issuer;
14+
use Magento\Framework\Jwt\Claim\JwtId;
1115
use Magento\Framework\Jwt\Claim\PrivateClaim;
16+
use Magento\Framework\Jwt\Claim\Subject;
1217
use Magento\Framework\Jwt\Header\Critical;
1318
use Magento\Framework\Jwt\Header\PrivateHeaderParameter;
1419
use Magento\Framework\Jwt\Header\PublicHeaderParameter;
@@ -58,7 +63,9 @@ public function testCreateRead(
5863
$recreated = $this->manager->read($token, $readEncryption);
5964

6065
//Verifying header
61-
$this->verifyHeader($jwt->getHeader(), $recreated->getHeader());
66+
if ((!$jwt instanceof JwsInterface && !$jwt instanceof JweInterface) || count($jwt->getProtectedHeaders()) == 1) {
67+
$this->verifyAgainstHeaders([$jwt->getHeader()], $recreated->getHeader());
68+
}
6269
//Verifying payload
6370
$this->assertEquals($jwt->getPayload()->getContent(), $recreated->getPayload()->getContent());
6471
if ($jwt->getPayload() instanceof ClaimsPayloadInterface) {
@@ -76,9 +83,9 @@ public function testCreateRead(
7683
$this->assertNull($recreated->getUnprotectedHeaders());
7784
} else {
7885
$this->assertTrue(count($recreated->getUnprotectedHeaders()) >= 1);
79-
$this->verifyHeader($jwt->getUnprotectedHeaders()[0], $recreated->getUnprotectedHeaders()[0]);
86+
$this->verifyAgainstHeaders($jwt->getUnprotectedHeaders(), $recreated->getUnprotectedHeaders()[0]);
8087
}
81-
$this->verifyHeader($jwt->getProtectedHeaders()[0], $recreated->getProtectedHeaders()[0]);
88+
$this->verifyAgainstHeaders($jwt->getProtectedHeaders(), $recreated->getProtectedHeaders()[0]);
8289
}
8390
if ($jwt instanceof JweInterface) {
8491
$this->assertInstanceOf(JweInterface::class, $recreated);
@@ -107,13 +114,14 @@ public function getTokenVariants(): array
107114
[
108115
new PrivateClaim('custom-claim', 'value'),
109116
new PrivateClaim('custom-claim2', 'value2'),
110-
new PrivateClaim('custom-claim3', 'value3')
117+
new PrivateClaim('custom-claim3', 'value3'),
118+
new IssuedAt(new \DateTimeImmutable()),
119+
new Issuer('magento.com')
111120
]
112121
),
113122
null
114123
);
115-
116-
$flatJwsWithUnprotectedHeader = new Jws(
124+
$jwsWithUnprotectedHeader = new Jws(
117125
[
118126
new JwsHeader(
119127
[
@@ -125,7 +133,8 @@ public function getTokenVariants(): array
125133
new ClaimsPayload(
126134
[
127135
new PrivateClaim('custom-claim', 'value'),
128-
new PrivateClaim('custom-claim2', 'value2')
136+
new PrivateClaim('custom-claim2', 'value2'),
137+
new ExpirationTime(new \DateTimeImmutable())
129138
]
130139
),
131140
[
@@ -136,15 +145,42 @@ public function getTokenVariants(): array
136145
)
137146
]
138147
);
148+
$compactJws = new Jws(
149+
[
150+
new JwsHeader(
151+
[
152+
new PrivateHeaderParameter('test', true),
153+
new PublicHeaderParameter('test2', 'magento', 'value')
154+
]
155+
),
156+
new JwsHeader(
157+
[
158+
new PrivateHeaderParameter('test3', true),
159+
new PublicHeaderParameter('test4', 'magento', 'value-another')
160+
]
161+
)
162+
],
163+
new ClaimsPayload([
164+
new Issuer('magento.com'),
165+
new JwtId(),
166+
new Subject('stuff')
167+
]),
168+
[
169+
new JwsHeader([new PrivateHeaderParameter('public', 'header1')]),
170+
new JwsHeader([new PrivateHeaderParameter('public2', 'header')])
171+
]
172+
);
173+
139174
$rsaPrivateResource = openssl_pkey_new(['private_key_bites' => 512, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
140175
if ($rsaPrivateResource === false) {
141176
throw new \RuntimeException('Failed to create RSA keypair');
142177
}
143178
$rsaPublic = openssl_pkey_get_details($rsaPrivateResource)['key'];
144-
if (!openssl_pkey_export($rsaPrivateResource, $rsaPrivate)) {
179+
if (!openssl_pkey_export($rsaPrivateResource, $rsaPrivate, 'pass')) {
145180
throw new \RuntimeException('Failed to read RSA private key');
146181
}
147182
openssl_free_key($rsaPrivateResource);
183+
$sharedSecret = random_bytes(128);
148184

149185
return [
150186
'jws-HS256' => [
@@ -158,29 +194,85 @@ public function getTokenVariants(): array
158194
[$enc]
159195
],
160196
'jws-HS512' => [
161-
$flatJwsWithUnprotectedHeader,
197+
$jwsWithUnprotectedHeader,
162198
$enc = new JwsSignatureJwks($jwkFactory->createHs512(random_bytes(128))),
163199
[$enc]
164200
],
165201
'jws-RS256' => [
166202
$flatJws,
167-
new JwsSignatureJwks($jwkFactory->createSignRs256($rsaPrivate, null)),
203+
new JwsSignatureJwks($jwkFactory->createSignRs256($rsaPrivate, 'pass')),
204+
[new JwsSignatureJwks($jwkFactory->createVerifyRs256($rsaPublic))]
205+
],
206+
'jws-RS384' => [
207+
$flatJws,
208+
new JwsSignatureJwks($jwkFactory->createSignRs384($rsaPrivate, 'pass')),
209+
[new JwsSignatureJwks($jwkFactory->createVerifyRs384($rsaPublic))]
210+
],
211+
'jws-RS512' => [
212+
$jwsWithUnprotectedHeader,
213+
new JwsSignatureJwks($jwkFactory->createSignRs512($rsaPrivate, 'pass')),
214+
[new JwsSignatureJwks($jwkFactory->createVerifyRs512($rsaPublic))]
215+
],
216+
'jws-compact-multiple-signatures' => [
217+
$compactJws,
218+
new JwsSignatureJwks(
219+
new JwkSet(
220+
[
221+
$jwkFactory->createHs384($sharedSecret),
222+
$jwkFactory->createSignRs256($rsaPrivate, 'pass')
223+
]
224+
)
225+
),
226+
[
227+
new JwsSignatureJwks(
228+
new JwkSet(
229+
[$jwkFactory->createHs384($sharedSecret), $jwkFactory->createVerifyRs256($rsaPublic)]
230+
)
231+
)
232+
]
233+
],
234+
'jws-compact-multiple-signatures-one-read' => [
235+
$compactJws,
236+
new JwsSignatureJwks(
237+
new JwkSet(
238+
[
239+
$jwkFactory->createHs384($sharedSecret),
240+
$jwkFactory->createSignRs256($rsaPrivate, 'pass')
241+
]
242+
)
243+
),
168244
[new JwsSignatureJwks($jwkFactory->createVerifyRs256($rsaPublic))]
169245
]
170246
];
171247
}
172248

173-
private function verifyHeader(HeaderInterface $expected, HeaderInterface $actual): void
249+
private function validateHeader(HeaderInterface $expected, HeaderInterface $actual): void
174250
{
175-
$this->assertTrue(
176-
count($expected->getParameters()) <= count($actual->getParameters())
177-
);
251+
if (count($expected->getParameters()) > count($actual->getParameters())) {
252+
throw new \InvalidArgumentException('Missing header parameters');
253+
}
178254
foreach ($expected->getParameters() as $parameter) {
179-
$this->assertNotNull($actual->getParameter($parameter->getName()));
180-
$this->assertEquals(
181-
$parameter->getValue(),
182-
$actual->getParameter($parameter->getName())->getValue()
183-
);
255+
if ($actual->getParameter($parameter->getName()) === null) {
256+
throw new \InvalidArgumentException('Missing header parameters');
257+
}
258+
if ($actual->getParameter($parameter->getName())->getValue() !== $parameter->getValue()) {
259+
throw new \InvalidArgumentException('Invalid header data');
260+
}
261+
}
262+
}
263+
264+
private function verifyAgainstHeaders(array $expected, HeaderInterface $actual): void
265+
{
266+
$oneIsValid = false;
267+
foreach ($expected as $item) {
268+
try {
269+
$this->validateHeader($item, $actual);
270+
$oneIsValid = true;
271+
break;
272+
} catch (\InvalidArgumentException $ex) {
273+
$oneIsValid = false;
274+
}
184275
}
276+
$this->assertTrue($oneIsValid);
185277
}
186278
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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\Framework\Jwt\Claim;
10+
11+
use Magento\Framework\Jwt\ClaimInterface;
12+
13+
/**
14+
* "aud" claim.
15+
*/
16+
class Audience implements ClaimInterface
17+
{
18+
/**
19+
* @var string[]
20+
*/
21+
private $value;
22+
23+
/**
24+
* @var bool
25+
*/
26+
private $duplicate;
27+
28+
/**
29+
* @param string[] $value
30+
* @param bool $duplicate
31+
*/
32+
public function __construct(array $value, bool $duplicate = false)
33+
{
34+
if (!$value) {
35+
throw new \InvalidArgumentException("Audience list cannot be empty");
36+
}
37+
$this->value = $value;
38+
$this->duplicate = $duplicate;
39+
}
40+
41+
/**
42+
* @inheritDoc
43+
*/
44+
public function getName(): string
45+
{
46+
return 'aud';
47+
}
48+
49+
/**
50+
* @inheritDoc
51+
*/
52+
public function getValue()
53+
{
54+
return json_encode($this->value);
55+
}
56+
57+
/**
58+
* @inheritDoc
59+
*/
60+
public function getClass(): ?string
61+
{
62+
return self::CLASS_REGISTERED;
63+
}
64+
65+
/**
66+
* @inheritDoc
67+
*/
68+
public function isHeaderDuplicated(): bool
69+
{
70+
return $this->duplicate;
71+
}
72+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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\Framework\Jwt\Claim;
10+
11+
use Magento\Framework\Jwt\ClaimInterface;
12+
13+
/**
14+
* "exp" claim.
15+
*/
16+
class ExpirationTime implements ClaimInterface
17+
{
18+
/**
19+
* @var string
20+
*/
21+
private $value;
22+
23+
/**
24+
* @var bool
25+
*/
26+
private $duplicate;
27+
28+
/**
29+
* @param \DateTimeInterface $value
30+
* @param bool $duplicate
31+
*/
32+
public function __construct(\DateTimeInterface $value, bool $duplicate = false)
33+
{
34+
if ($value instanceof \DateTimeImmutable) {
35+
$value = \DateTime::createFromImmutable($value);
36+
}
37+
$value->setTimezone(new \DateTimeZone('UTC'));
38+
$this->value = $value->format('Y-m-d\TH:i:s\Z UTC');
39+
$this->duplicate = $duplicate;
40+
}
41+
42+
/**
43+
* @inheritDoc
44+
*/
45+
public function getName(): string
46+
{
47+
return 'exp';
48+
}
49+
50+
/**
51+
* @inheritDoc
52+
*/
53+
public function getValue()
54+
{
55+
return $this->value;
56+
}
57+
58+
/**
59+
* @inheritDoc
60+
*/
61+
public function getClass(): ?string
62+
{
63+
return self::CLASS_REGISTERED;
64+
}
65+
66+
/**
67+
* @inheritDoc
68+
*/
69+
public function isHeaderDuplicated(): bool
70+
{
71+
return $this->duplicate;
72+
}
73+
}

0 commit comments

Comments
 (0)