Skip to content

Commit 6ee7f3b

Browse files
author
ogorkun
committed
MC-38539: Introduce JWT wrapper
1 parent d198c2e commit 6ee7f3b

File tree

6 files changed

+397
-246
lines changed

6 files changed

+397
-246
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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\Core\AlgorithmManager;
12+
use Jose\Component\Signature\JWSBuilder;
13+
use Jose\Easy\AlgorithmProvider;
14+
15+
class JwsBuilderFactory
16+
{
17+
public function create(): JWSBuilder
18+
{
19+
$jwsAlgorithms = [
20+
\Jose\Component\Signature\Algorithm\HS256::class,
21+
\Jose\Component\Signature\Algorithm\HS384::class,
22+
\Jose\Component\Signature\Algorithm\HS512::class,
23+
\Jose\Component\Signature\Algorithm\RS256::class,
24+
\Jose\Component\Signature\Algorithm\RS384::class,
25+
\Jose\Component\Signature\Algorithm\RS512::class,
26+
\Jose\Component\Signature\Algorithm\PS256::class,
27+
\Jose\Component\Signature\Algorithm\PS384::class,
28+
\Jose\Component\Signature\Algorithm\PS512::class,
29+
\Jose\Component\Signature\Algorithm\ES256::class,
30+
\Jose\Component\Signature\Algorithm\ES384::class,
31+
\Jose\Component\Signature\Algorithm\ES512::class,
32+
\Jose\Component\Signature\Algorithm\EdDSA::class,
33+
];
34+
$jwsAlgorithmProvider = new AlgorithmProvider($jwsAlgorithms);
35+
$algorithmManager = new AlgorithmManager($jwsAlgorithmProvider->getAvailableAlgorithms());
36+
37+
return new JWSBuilder($algorithmManager);
38+
}
39+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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\Jws\JwsInterface;
14+
use Magento\Framework\Jwt\Payload\ArbitraryPayload;
15+
use Magento\Framework\Jwt\Payload\ClaimsPayload;
16+
use Magento\Framework\Jwt\Payload\NestedPayload;
17+
use Magento\Framework\Jwt\Payload\NestedPayloadInterface;
18+
use Magento\JwtFrameworkAdapter\Model\Data\Claim;
19+
use Magento\JwtFrameworkAdapter\Model\Data\Header;
20+
21+
/**
22+
* Create JWS data object.
23+
*/
24+
class JwsFactory
25+
{
26+
public function create(
27+
array $protectedHeadersMap,
28+
string $payload,
29+
?array $unprotectedHeadersMap
30+
): JwsInterface {
31+
$protectedHeaders = [];
32+
foreach ($protectedHeadersMap as $header => $headerValue) {
33+
$protectedHeaders[] = new Header($header, $headerValue, null);
34+
}
35+
$publicHeaders = null;
36+
if ($unprotectedHeadersMap) {
37+
$publicHeaders = [];
38+
foreach ($unprotectedHeadersMap as $header => $headerValue) {
39+
$publicHeaders[] = new Header($header, $headerValue, null);
40+
}
41+
}
42+
$headersMap = array_merge($unprotectedHeadersMap ?? [], $protectedHeadersMap);
43+
if (array_key_exists('cty', $headersMap)) {
44+
if ($headersMap['cty'] === NestedPayloadInterface::CONTENT_TYPE) {
45+
$payload = new NestedPayload($payload);
46+
} else {
47+
$payload = new ArbitraryPayload($payload);
48+
}
49+
} else {
50+
$claimData = json_decode($payload, true);
51+
$claims = [];
52+
foreach ($claimData as $name => $value) {
53+
$claims[] = new Claim($name, $value, null);
54+
}
55+
$payload = new ClaimsPayload($claims);
56+
}
57+
58+
return new Jws(
59+
[new JwsHeader($protectedHeaders)],
60+
$payload,
61+
$publicHeaders ? [new JwsHeader($publicHeaders)] : null
62+
);
63+
}
64+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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\Core\AlgorithmManagerFactory;
12+
use Jose\Component\Signature\JWSVerifierFactory;
13+
use Jose\Component\Signature\Serializer\CompactSerializer;
14+
use Jose\Component\Signature\Serializer\JSONFlattenedSerializer as JwsFlatSerializer;
15+
use Jose\Component\Signature\Serializer\JSONGeneralSerializer as JwsJsonSerializer;
16+
use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory;
17+
use Jose\Easy\AlgorithmProvider;
18+
19+
class JwsLoaderFactory
20+
{
21+
public function create()
22+
{
23+
$jwsAlgorithms = [
24+
\Jose\Component\Signature\Algorithm\HS256::class,
25+
\Jose\Component\Signature\Algorithm\HS384::class,
26+
\Jose\Component\Signature\Algorithm\HS512::class,
27+
\Jose\Component\Signature\Algorithm\RS256::class,
28+
\Jose\Component\Signature\Algorithm\RS384::class,
29+
\Jose\Component\Signature\Algorithm\RS512::class,
30+
\Jose\Component\Signature\Algorithm\PS256::class,
31+
\Jose\Component\Signature\Algorithm\PS384::class,
32+
\Jose\Component\Signature\Algorithm\PS512::class,
33+
\Jose\Component\Signature\Algorithm\ES256::class,
34+
\Jose\Component\Signature\Algorithm\ES384::class,
35+
\Jose\Component\Signature\Algorithm\ES512::class,
36+
\Jose\Component\Signature\Algorithm\EdDSA::class,
37+
];
38+
$jwsAlgorithmProvider = new AlgorithmProvider($jwsAlgorithms);
39+
$jwsAlgorithmFactory = new AlgorithmManagerFactory();
40+
foreach ($jwsAlgorithmProvider->getAvailableAlgorithms() as $algorithm) {
41+
$jwsAlgorithmFactory->add($algorithm->name(), $algorithm);
42+
}
43+
$jwsSerializerFactory = new JWSSerializerManagerFactory();
44+
$jwsSerializerFactory->add(new CompactSerializer());
45+
$jwsSerializerFactory->add(new JwsJsonSerializer());
46+
$jwsSerializerFactory->add(new JwsFlatSerializer());
47+
48+
return new \Jose\Component\Signature\JWSLoaderFactory(
49+
$jwsSerializerFactory,
50+
new JWSVerifierFactory($jwsAlgorithmFactory),
51+
null
52+
);
53+
}
54+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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\JWSLoaderFactory as LoaderFactory;
13+
use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory as JWSSerializerPool;
14+
use Magento\Framework\Jwt\EncryptionSettingsInterface;
15+
use Magento\Framework\Jwt\Exception\EncryptionException;
16+
use Magento\Framework\Jwt\Exception\JwtException;
17+
use Magento\Framework\Jwt\Exception\MalformedTokenException;
18+
use Magento\Framework\Jwt\HeaderInterface;
19+
use Magento\Framework\Jwt\Jwk;
20+
use Magento\Framework\Jwt\Jws\JwsInterface;
21+
use Magento\Framework\Jwt\Jws\JwsSignatureJwks;
22+
use Jose\Component\Core\JWK as AdapterJwk;
23+
use Jose\Component\Core\JWKSet as AdapterJwkSet;
24+
25+
/**
26+
* Works with JWS.
27+
*/
28+
class JwsManager
29+
{
30+
/**
31+
* @var JWSBuilder
32+
*/
33+
private $jwsBuilder;
34+
35+
/**
36+
* @var LoaderFactory
37+
*/
38+
private $jwsLoaderFactory;
39+
40+
/**
41+
* @var JWSSerializerPool
42+
*/
43+
private $jwsSerializerFactory;
44+
45+
/**
46+
* @var JwsFactory
47+
*/
48+
private $jwsFactory;
49+
50+
/**
51+
* @param JwsBuilderFactory $builderFactory
52+
* @param JwsSerializerPoolFactory $serializerPoolFactory
53+
* @param JwsLoaderFactory $jwsLoaderFactory
54+
* @param JwsFactory $jwsFactory
55+
*/
56+
public function __construct(
57+
JwsBuilderFactory $builderFactory,
58+
JwsSerializerPoolFactory $serializerPoolFactory,
59+
JwsLoaderFactory $jwsLoaderFactory,
60+
JwsFactory $jwsFactory
61+
) {
62+
$this->jwsBuilder = $builderFactory->create();
63+
$this->jwsSerializerFactory = $serializerPoolFactory->create();
64+
$this->jwsLoaderFactory = $jwsLoaderFactory->create();
65+
$this->jwsFactory = $jwsFactory;
66+
}
67+
68+
/**
69+
* Generate JWS token.
70+
*
71+
* @param JwsInterface $jws
72+
* @param EncryptionSettingsInterface|JwsSignatureJwks $encryptionSettings
73+
* @return string
74+
* @throws JwtException
75+
*/
76+
public function build(JwsInterface $jws, EncryptionSettingsInterface $encryptionSettings): string
77+
{
78+
if (!$encryptionSettings instanceof JwsSignatureJwks) {
79+
throw new JwtException('Can only work with JWK encryption settings for JWS tokens');
80+
}
81+
$signaturesCount = count($encryptionSettings->getJwkSet()->getKeys());
82+
if ($jws->getProtectedHeaders() && count($jws->getProtectedHeaders()) !== $signaturesCount) {
83+
throw new MalformedTokenException('Number of headers must equal to number of JWKs');
84+
}
85+
if ($jws->getUnprotectedHeaders()
86+
&& count($jws->getUnprotectedHeaders()) !== $signaturesCount
87+
) {
88+
throw new MalformedTokenException('There must be an equal number of protected and unprotected headers.');
89+
}
90+
$builder = $this->jwsBuilder->create();
91+
$builder = $builder->withPayload($jws->getPayload()->getContent());
92+
for ($i = 0; $i < $signaturesCount; $i++) {
93+
$jwk = $encryptionSettings->getJwkSet()->getKeys()[$i];
94+
$alg = $jwk->getAlgorithm();
95+
if (!$alg) {
96+
throw new EncryptionException('Algorithm is required for JWKs');
97+
}
98+
$protected = [];
99+
if ($jws->getPayload()->getContentType()) {
100+
$protected['cty'] = $jws->getPayload()->getContentType();
101+
}
102+
if ($jws->getProtectedHeaders()) {
103+
$protected = $this->extractHeaderData($jws->getProtectedHeaders()[$i]);
104+
}
105+
$protected['alg'] = $alg;
106+
$unprotected = [];
107+
if ($jws->getUnprotectedHeaders()) {
108+
$unprotected = $this->extractHeaderData($jws->getUnprotectedHeaders()[$i]);
109+
}
110+
$builder = $builder->addSignature(new AdapterJwk($jwk->getJsonData()), $protected, $unprotected);
111+
}
112+
$jwsCreated = $builder->build();
113+
114+
if ($signaturesCount > 1) {
115+
return $this->jwsSerializerFactory->all()['jws_json_general']->serialize($jwsCreated);
116+
}
117+
if ($jws->getUnprotectedHeaders()) {
118+
return $this->jwsSerializerFactory->all()['jws_json_flattened']->serialize($jwsCreated);
119+
}
120+
return $this->jwsSerializerFactory->all()['jws_compact']->serialize($jwsCreated);
121+
}
122+
123+
/**
124+
* Read and verify JWS token.
125+
*
126+
* @param string $token
127+
* @param EncryptionSettingsInterface|JwsSignatureJwks $encryptionSettings
128+
* @return JwsInterface
129+
* @throws JwtException
130+
*/
131+
public function read(string $token, EncryptionSettingsInterface $encryptionSettings): JwsInterface
132+
{
133+
if (!$encryptionSettings instanceof JwsSignatureJwks) {
134+
throw new JwtException('Can only work with JWK settings for JWS tokens');
135+
}
136+
137+
$loader = $this->jwsLoaderFactory->create(
138+
['jws_compact', 'jws_json_flattened', 'jws_json_general'],
139+
array_map(
140+
function (Jwk $jwk) {
141+
return $jwk->getAlgorithm();
142+
},
143+
$encryptionSettings->getJwkSet()->getKeys()
144+
)
145+
);
146+
$jwkSet = new AdapterJwkSet(
147+
array_map(
148+
function (Jwk $jwk) {
149+
return new AdapterJwk($jwk->getJsonData());
150+
},
151+
$encryptionSettings->getJwkSet()->getKeys()
152+
)
153+
);
154+
try {
155+
$jws = $loader->loadAndVerifyWithKeySet(
156+
$token,
157+
$jwkSet,
158+
$signature,
159+
null
160+
);
161+
} catch (\Throwable $exception) {
162+
throw new MalformedTokenException('Failed to read JWS token', 0, $exception);
163+
}
164+
if ($signature === null) {
165+
throw new EncryptionException('Failed to verify a JWS token');
166+
}
167+
$headers = $jws->getSignature($signature);
168+
if ($jws->isPayloadDetached()) {
169+
throw new JwtException('Detached payload is not supported');
170+
}
171+
172+
return $this->jwsFactory->create(
173+
$headers->getProtectedHeader(),
174+
$jws->getPayload(),
175+
$headers->getHeader() ? $headers->getHeader() : null
176+
);
177+
}
178+
179+
/**
180+
* Extract JOSE header data.
181+
*
182+
* @param HeaderInterface $header
183+
* @return array
184+
*/
185+
private function extractHeaderData(HeaderInterface $header): array
186+
{
187+
$data = [];
188+
foreach ($header->getParameters() as $parameter) {
189+
$data[$parameter->getName()] = $parameter->getValue();
190+
}
191+
192+
return $data;
193+
}
194+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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\Serializer\CompactSerializer;
12+
use Jose\Component\Signature\Serializer\JSONFlattenedSerializer;
13+
use Jose\Component\Signature\Serializer\JSONGeneralSerializer;
14+
use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory;
15+
16+
class JwsSerializerPoolFactory
17+
{
18+
public function create(): JWSSerializerManagerFactory
19+
{
20+
$jwsSerializerFactory = new JWSSerializerManagerFactory();
21+
$jwsSerializerFactory->add(new CompactSerializer());
22+
$jwsSerializerFactory->add(new JSONGeneralSerializer());
23+
$jwsSerializerFactory->add(new JSONFlattenedSerializer());
24+
25+
return $jwsSerializerFactory;
26+
}
27+
}

0 commit comments

Comments
 (0)