Skip to content

Commit 0c9035b

Browse files
author
ogorkun
committed
MC-38539: Introduce JWT wrapper
1 parent 2d9c0c6 commit 0c9035b

File tree

8 files changed

+408
-2
lines changed

8 files changed

+408
-2
lines changed

app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ class JwsAlgorithmManagerFactory
2727
\Jose\Component\Signature\Algorithm\ES256::class,
2828
\Jose\Component\Signature\Algorithm\ES384::class,
2929
\Jose\Component\Signature\Algorithm\ES512::class,
30-
\Jose\Component\Signature\Algorithm\EdDSA::class
30+
\Jose\Component\Signature\Algorithm\EdDSA::class,
31+
\Jose\Component\Signature\Algorithm\None::class
3132
];
3233

3334
public function create(): AlgorithmManager

app/code/Magento/JwtFrameworkAdapter/Model/JwtManager.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace Magento\JwtFrameworkAdapter\Model;
1010

1111
use Magento\Framework\Jwt\EncryptionSettingsInterface;
12+
use Magento\Framework\Jwt\Exception\EncryptionException;
1213
use Magento\Framework\Jwt\Exception\JwtException;
1314
use Magento\Framework\Jwt\Exception\MalformedTokenException;
1415
use Magento\Framework\Jwt\Jwe\JweEncryptionSettingsInterface;
@@ -18,6 +19,7 @@
1819
use Magento\Framework\Jwt\Jws\JwsSignatureSettingsInterface;
1920
use Magento\Framework\Jwt\JwtInterface;
2021
use Magento\Framework\Jwt\JwtManagerInterface;
22+
use Magento\Framework\Jwt\Unsecured\NoEncryption;
2123
use Magento\Framework\Jwt\Unsecured\UnsecuredJwtInterface;
2224

2325
/**
@@ -75,14 +77,20 @@ class JwtManager implements JwtManagerInterface
7577
*/
7678
private $jweManager;
7779

80+
/**
81+
* @var UnsecuredJwtManager
82+
*/
83+
private $unsecuredManager;
84+
7885
/**
7986
* @param JwsManager $jwsManager
8087
* @param JweManager $jweManager
8188
*/
82-
public function __construct(JwsManager $jwsManager, JweManager $jweManager)
89+
public function __construct(JwsManager $jwsManager, JweManager $jweManager, UnsecuredJwtManager $unsecuredManager)
8390
{
8491
$this->jwsManager = $jwsManager;
8592
$this->jweManager = $jweManager;
93+
$this->unsecuredManager = $unsecuredManager;
8694
}
8795

8896
/**
@@ -100,6 +108,13 @@ public function create(JwtInterface $jwt, EncryptionSettingsInterface $encryptio
100108
if ($jwt instanceof JweInterface) {
101109
return $this->jweManager->build($jwt, $encryption);
102110
}
111+
if ($jwt instanceof UnsecuredJwtInterface) {
112+
if (!$encryption instanceof NoEncryption) {
113+
throw new EncryptionException('Unsecured JWTs can only work with no encryption settings');
114+
}
115+
116+
return $this->unsecuredManager->build($jwt);
117+
}
103118
} catch (\Throwable $exception) {
104119
if (!$exception instanceof JwtException) {
105120
$exception = new JwtException('Failed to generate a JWT', 0, $exception);
@@ -126,6 +141,9 @@ public function read(string $token, array $acceptableEncryption): JwtInterface
126141
case self::JWT_TYPE_JWE:
127142
$read = $this->jweManager->read($token, $encryptionSettings);
128143
break;
144+
case self::JWT_TYPE_UNSECURED:
145+
$read = $this->unsecuredManager->read($token);
146+
break;
129147
}
130148
} catch (\Throwable $exception) {
131149
if (!$exception instanceof JwtException) {
@@ -149,6 +167,9 @@ private function detectJwtType(EncryptionSettingsInterface $encryptionSettings):
149167
if ($encryptionSettings instanceof JweEncryptionSettingsInterface) {
150168
return self::JWT_TYPE_JWE;
151169
}
170+
if ($encryptionSettings instanceof NoEncryption) {
171+
return self::JWT_TYPE_UNSECURED;
172+
}
152173

153174
if ($encryptionSettings->getAlgorithmName() === Jwk::ALGORITHM_NONE) {
154175
return self::JWT_TYPE_UNSECURED;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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\JwtFrameworkAdapter\Model;
10+
11+
use Magento\Framework\Jwt\Jws\Jws;
12+
use Magento\Framework\Jwt\Jws\JwsHeader;
13+
use Magento\Framework\Jwt\Payload\ArbitraryPayload;
14+
use Magento\Framework\Jwt\Payload\ClaimsPayload;
15+
use Magento\Framework\Jwt\Payload\NestedPayload;
16+
use Magento\Framework\Jwt\Payload\NestedPayloadInterface;
17+
use Magento\Framework\Jwt\Unsecured\UnsecuredJwt;
18+
use Magento\Framework\Jwt\Unsecured\UnsecuredJwtInterface;
19+
use Magento\JwtFrameworkAdapter\Model\Data\Claim;
20+
use Magento\JwtFrameworkAdapter\Model\Data\Header;
21+
22+
/**
23+
* Creates unsecure JWT DTOs.
24+
*/
25+
class UnsecuredJwtFactory
26+
{
27+
public function create(
28+
array $protectedHeaderMaps,
29+
?array $unprotectedHeaderMaps,
30+
string $payload
31+
): UnsecuredJwtInterface {
32+
$cty = null;
33+
$protectedHeaders = [];
34+
foreach ($protectedHeaderMaps as $protectedHeaderMap) {
35+
$parameters = [];
36+
foreach ($protectedHeaderMap as $header => $headerValue) {
37+
$parameters[] = new Header($header, $headerValue, null);
38+
if ($header === 'cty') {
39+
$cty = $headerValue;
40+
}
41+
}
42+
$protectedHeaders[] = new JwsHeader($parameters);
43+
}
44+
$publicHeaders = null;
45+
if ($unprotectedHeaderMaps) {
46+
$publicHeaders = [];
47+
foreach ($unprotectedHeaderMaps as $unprotectedHeaderMap) {
48+
$parameters = [];
49+
foreach ($unprotectedHeaderMap as $header => $headerValue) {
50+
$parameters[] = new Header($header, $headerValue, null);
51+
if ($header === 'cty') {
52+
$cty = $headerValue;
53+
}
54+
}
55+
$publicHeaders[] = new JwsHeader($parameters);
56+
}
57+
}
58+
if ($cty) {
59+
if ($cty === NestedPayloadInterface::CONTENT_TYPE) {
60+
$payload = new NestedPayload($payload);
61+
} else {
62+
$payload = new ArbitraryPayload($payload);
63+
}
64+
} else {
65+
$claimData = json_decode($payload, true);
66+
$claims = [];
67+
foreach ($claimData as $name => $value) {
68+
$claims[] = new Claim($name, $value, null);
69+
}
70+
$payload = new ClaimsPayload($claims);
71+
}
72+
73+
return new UnsecuredJwt(
74+
$protectedHeaders,
75+
$payload,
76+
$publicHeaders
77+
);
78+
}
79+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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\JwtFrameworkAdapter\Model;
10+
11+
use Jose\Component\Signature\JWSBuilder;
12+
use Jose\Component\Signature\JWSLoader;
13+
use Jose\Component\Signature\Serializer\JWSSerializerManager;
14+
use Magento\Framework\Jwt\Exception\JwtException;
15+
use Magento\Framework\Jwt\Exception\MalformedTokenException;
16+
use Magento\Framework\Jwt\HeaderInterface;
17+
use Magento\Framework\Jwt\Jwk;
18+
use Jose\Component\Core\JWK as AdapterJwk;
19+
use Magento\Framework\Jwt\Unsecured\UnsecuredJwtInterface;
20+
21+
/**
22+
* Works with Unsecured JWT.
23+
*/
24+
class UnsecuredJwtManager
25+
{
26+
/**
27+
* @var JWSBuilder
28+
*/
29+
private $jwsBuilder;
30+
31+
/**
32+
* @var JWSLoader
33+
*/
34+
private $jwsLoader;
35+
36+
/**
37+
* @var JWSSerializerManager
38+
*/
39+
private $jwsSerializer;
40+
41+
/**
42+
* @var UnsecuredJwtFactory
43+
*/
44+
private $jwtFactory;
45+
46+
/**
47+
* @param JwsBuilderFactory $builderFactory
48+
* @param JwsSerializerPoolFactory $serializerPoolFactory
49+
* @param JwsLoaderFactory $jwsLoaderFactory
50+
* @param UnsecuredJwtFactory $jwtFactory
51+
*/
52+
public function __construct(
53+
JwsBuilderFactory $builderFactory,
54+
JwsSerializerPoolFactory $serializerPoolFactory,
55+
JwsLoaderFactory $jwsLoaderFactory,
56+
UnsecuredJwtFactory $jwtFactory
57+
) {
58+
$this->jwsBuilder = $builderFactory->create();
59+
$this->jwsSerializer = $serializerPoolFactory->create();
60+
$this->jwsLoader = $jwsLoaderFactory->create();
61+
$this->jwtFactory = $jwtFactory;
62+
}
63+
64+
/**
65+
* Generate unsecured JWT token.
66+
*
67+
* @param UnsecuredJwtInterface $jwt
68+
* @return string
69+
* @throws JwtException
70+
*/
71+
public function build(UnsecuredJwtInterface $jwt): string
72+
{
73+
$signaturesCount = count($jwt->getProtectedHeaders());
74+
if ($jwt->getUnprotectedHeaders()
75+
&& count($jwt->getUnprotectedHeaders()) !== $signaturesCount
76+
) {
77+
throw new MalformedTokenException('There must be an equal number of protected and unprotected headers.');
78+
}
79+
$builder = $this->jwsBuilder->create();
80+
$builder = $builder->withPayload($jwt->getPayload()->getContent());
81+
for ($i = 0; $i < $signaturesCount; $i++) {
82+
$protected = [];
83+
if ($jwt->getPayload()->getContentType()) {
84+
$protected['cty'] = $jwt->getPayload()->getContentType();
85+
}
86+
if ($jwt->getProtectedHeaders()) {
87+
$protected = $this->extractHeaderData($jwt->getProtectedHeaders()[$i]);
88+
}
89+
$protected['alg'] = Jwk::ALGORITHM_NONE;
90+
$unprotected = [];
91+
if ($jwt->getUnprotectedHeaders()) {
92+
$unprotected = $this->extractHeaderData($jwt->getUnprotectedHeaders()[$i]);
93+
}
94+
$builder = $builder->addSignature(
95+
new AdapterJwk(['kty' => 'none', 'alg' => 'none']),
96+
$protected,
97+
$unprotected
98+
);
99+
}
100+
$jwsCreated = $builder->build();
101+
102+
if ($signaturesCount > 1) {
103+
return $this->jwsSerializer->serialize('jws_json_general', $jwsCreated);
104+
}
105+
if ($jwt->getUnprotectedHeaders()) {
106+
return $this->jwsSerializer->serialize('jws_json_flattened', $jwsCreated);
107+
}
108+
return $this->jwsSerializer->serialize('jws_compact', $jwsCreated);
109+
}
110+
111+
/**
112+
* Read unsecured JWT token.
113+
*
114+
* @param string $token
115+
* @return UnsecuredJwtInterface
116+
* @throws JwtException
117+
*/
118+
public function read(string $token): UnsecuredJwtInterface
119+
{
120+
try {
121+
$jws = $this->jwsLoader->loadAndVerifyWithKey(
122+
$token,
123+
new AdapterJwk(['kty' => 'none', 'alg' => 'none']),
124+
$signature,
125+
null
126+
);
127+
} catch (\Throwable $exception) {
128+
throw new MalformedTokenException('Failed to read JWT token', 0, $exception);
129+
}
130+
if ($jws->isPayloadDetached()) {
131+
throw new JwtException('Detached payload is not supported');
132+
}
133+
$protectedHeaders = [];
134+
$publicHeaders = [];
135+
foreach ($jws->getSignatures() as $signature) {
136+
$protectedHeaders[] = $signature->getProtectedHeader();
137+
if ($signature->getHeader()) {
138+
$publicHeaders[] = $signature->getHeader();
139+
}
140+
}
141+
142+
return $this->jwtFactory->create(
143+
$protectedHeaders,
144+
$publicHeaders,
145+
$jws->getPayload()
146+
);
147+
}
148+
149+
/**
150+
* Extract JOSE header data.
151+
*
152+
* @param HeaderInterface $header
153+
* @return array
154+
*/
155+
private function extractHeaderData(HeaderInterface $header): array
156+
{
157+
$data = [];
158+
foreach ($header->getParameters() as $parameter) {
159+
$data[$parameter->getName()] = $parameter->getValue();
160+
}
161+
162+
return $data;
163+
}
164+
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
use Magento\Framework\Jwt\Payload\ClaimsPayload;
3131
use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface;
3232
use Magento\Framework\Jwt\Payload\NestedPayloadInterface;
33+
use Magento\Framework\Jwt\Unsecured\NoEncryption;
34+
use Magento\Framework\Jwt\Unsecured\UnsecuredJwt;
3335
use Magento\Framework\Jwt\Unsecured\UnsecuredJwtInterface;
3436
use Magento\TestFramework\Helper\Bootstrap;
3537
use PHPUnit\Framework\TestCase;
@@ -130,6 +132,14 @@ public function testCreateRead(
130132
}
131133
if ($jwt instanceof UnsecuredJwtInterface) {
132134
$this->assertInstanceOf(UnsecuredJwtInterface::class, $recreated);
135+
/** @var UnsecuredJwt $recreated */
136+
if (!$jwt->getUnprotectedHeaders()) {
137+
$this->assertNull($recreated->getUnprotectedHeaders());
138+
} else {
139+
$this->assertTrue(count($recreated->getUnprotectedHeaders()) >= 1);
140+
$this->verifyAgainstHeaders($jwt->getUnprotectedHeaders(), $recreated->getUnprotectedHeaders()[0]);
141+
}
142+
$this->verifyAgainstHeaders($jwt->getProtectedHeaders(), $recreated->getProtectedHeaders()[0]);
133143
}
134144

135145
}
@@ -322,6 +332,26 @@ public function getTokenVariants(): array
322332
]
323333
)
324334
);
335+
$flatUnsecured = new UnsecuredJwt(
336+
[
337+
new JwsHeader(
338+
[
339+
new PrivateHeaderParameter('test', true),
340+
new PublicHeaderParameter('test2', 'magento', 'value')
341+
]
342+
)
343+
],
344+
new ClaimsPayload(
345+
[
346+
new PrivateClaim('custom-claim', 'value'),
347+
new PrivateClaim('custom-claim2', 'value2', true),
348+
new PrivateClaim('custom-claim3', 'value3'),
349+
new IssuedAt(new \DateTimeImmutable()),
350+
new Issuer('magento.com')
351+
]
352+
),
353+
null
354+
);
325355

326356
//Keys
327357
[$rsaPrivate, $rsaPublic] = $this->createRsaKeys();
@@ -676,6 +706,11 @@ public function getTokenVariants(): array
676706
JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM
677707
)
678708
]
709+
],
710+
'unsecured-jwt' => [
711+
$flatUnsecured,
712+
new NoEncryption(),
713+
[new NoEncryption()]
679714
]
680715
];
681716
}

0 commit comments

Comments
 (0)