Skip to content

Commit 5e84a82

Browse files
author
ogorkun
committed
MC-38539: Introduce JWT wrapper
1 parent 7339e69 commit 5e84a82

File tree

5 files changed

+332
-0
lines changed

5 files changed

+332
-0
lines changed

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717
use Magento\Framework\Jwt\Exception\MalformedTokenException;
1818
use Magento\Framework\Jwt\HeaderInterface;
1919
use Magento\Framework\Jwt\Jwe\JweEncryptionJwks;
20+
use Magento\Framework\Jwt\Jwe\JweHeader;
2021
use Magento\Framework\Jwt\Jwe\JweInterface;
2122
use Jose\Component\Core\JWK as AdapterJwk;
2223
use Jose\Component\Core\JWKSet as AdapterJwkSet;
2324
use Magento\Framework\Jwt\Jwk;
25+
use Magento\Framework\Jwt\Jws\JwsHeader;
2426
use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface;
27+
use Magento\JwtFrameworkAdapter\Model\Data\Header;
2528

2629
/**
2730
* Works with JWE
@@ -170,6 +173,45 @@ function (Jwk $jwk) {
170173
);
171174
}
172175

176+
/**
177+
* Read JWS headers.
178+
*
179+
* @param string $token
180+
* @return HeaderInterface[]
181+
*/
182+
public function readHeaders(string $token): array
183+
{
184+
try {
185+
$jwe = $this->serializer->unserialize($token);
186+
} catch (\Throwable $exception) {
187+
throw new JwtException('Failed to read JWE headers');
188+
}
189+
$headers = [];
190+
$headersValues = [];
191+
if ($jwe->getSharedHeader()) {
192+
$headersValues[] = $jwe->getSharedHeader();
193+
}
194+
if ($jwe->getSharedProtectedHeader()) {
195+
$headersValues[] = $jwe->getSharedProtectedHeader();
196+
}
197+
foreach ($jwe->getRecipients() as $recipient) {
198+
if ($recipient->getHeader()) {
199+
$headersValues[] = $recipient->getHeader();
200+
}
201+
}
202+
foreach ($headersValues as $headerValues) {
203+
$params = [];
204+
foreach ($headerValues as $header => $value) {
205+
$params[] = new Header($header, $value, null);
206+
}
207+
if ($params) {
208+
$headers[] = new JweHeader($params);
209+
}
210+
}
211+
212+
return $headers;
213+
}
214+
173215
private function validateJweSettings(JweInterface $jwe, EncryptionSettingsInterface $encryptionSettings): void
174216
{
175217
if (!$encryptionSettings instanceof JweEncryptionJwks) {

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
use Magento\Framework\Jwt\Exception\MalformedTokenException;
1818
use Magento\Framework\Jwt\HeaderInterface;
1919
use Magento\Framework\Jwt\Jwk;
20+
use Magento\Framework\Jwt\Jws\JwsHeader;
2021
use Magento\Framework\Jwt\Jws\JwsInterface;
2122
use Magento\Framework\Jwt\Jws\JwsSignatureJwks;
2223
use Jose\Component\Core\JWK as AdapterJwk;
2324
use Jose\Component\Core\JWKSet as AdapterJwkSet;
25+
use Magento\JwtFrameworkAdapter\Model\Data\Header;
2426

2527
/**
2628
* Works with JWS.
@@ -167,6 +169,42 @@ function (Jwk $jwk) {
167169
);
168170
}
169171

172+
/**
173+
* Read JWS headers.
174+
*
175+
* @param string $token
176+
* @return HeaderInterface[]
177+
*/
178+
public function readHeaders(string $token): array
179+
{
180+
try {
181+
$jws = $this->jwsSerializer->unserialize($token);
182+
} catch (\Throwable $exception) {
183+
throw new JwtException('Failed to read JWS headers');
184+
}
185+
$headers = [];
186+
$headersValues = [];
187+
foreach ($jws->getSignatures() as $signature) {
188+
if ($signature->getProtectedHeader()) {
189+
$headersValues[] = $signature->getProtectedHeader();
190+
}
191+
if ($signature->getHeader()) {
192+
$headersValues[] = $signature->getHeader();
193+
}
194+
}
195+
foreach ($headersValues as $headerValues) {
196+
$params = [];
197+
foreach ($headerValues as $header => $value) {
198+
$params[] = new Header($header, $value, null);
199+
}
200+
if ($params) {
201+
$headers[] = new JwsHeader($params);
202+
}
203+
}
204+
205+
return $headers;
206+
}
207+
170208
/**
171209
* Extract JOSE header data.
172210
*

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Magento\Framework\Jwt\Exception\EncryptionException;
1313
use Magento\Framework\Jwt\Exception\JwtException;
1414
use Magento\Framework\Jwt\Exception\MalformedTokenException;
15+
use Magento\Framework\Jwt\HeaderInterface;
1516
use Magento\Framework\Jwt\Jwe\JweEncryptionSettingsInterface;
1617
use Magento\Framework\Jwt\Jwe\JweInterface;
1718
use Magento\Framework\Jwt\Jwk;
@@ -159,6 +160,18 @@ public function read(string $token, array $acceptableEncryption): JwtInterface
159160
return $read;
160161
}
161162

163+
/**
164+
* @inheritDoc
165+
*/
166+
public function readHeaders(string $token): array
167+
{
168+
try {
169+
return $this->jwsManager->readHeaders($token);
170+
} catch (JwtException $exception) {
171+
return $this->jweManager->readHeaders($token);
172+
}
173+
}
174+
162175
private function detectJwtType(EncryptionSettingsInterface $encryptionSettings): int
163176
{
164177
if ($encryptionSettings instanceof JwsSignatureSettingsInterface) {

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

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,237 @@ public function getTokenVariants(): array
715715
];
716716
}
717717

718+
/**
719+
* Test reading headers.
720+
*
721+
* @param JwtInterface $tokenData
722+
* @param EncryptionSettingsInterface $settings
723+
* @return void
724+
*
725+
* @dataProvider getJwtsForHeaders
726+
*/
727+
public function testReadHeaders(JwtInterface $tokenData, EncryptionSettingsInterface $settings): void
728+
{
729+
$token = $this->manager->create($tokenData, $settings);
730+
$headers = $this->manager->readHeaders($token);
731+
/** @var HeaderInterface[] $expectedHeaders */
732+
$expectedHeaders = [];
733+
if ($tokenData instanceof JwsInterface) {
734+
$expectedHeaders = $tokenData->getProtectedHeaders();
735+
if ($tokenData->getUnprotectedHeaders()) {
736+
$expectedHeaders = array_merge($expectedHeaders, $tokenData->getUnprotectedHeaders());
737+
}
738+
} elseif ($tokenData instanceof JweInterface) {
739+
$expectedHeaders[] = $tokenData->getProtectedHeader();
740+
if ($tokenData->getSharedUnprotectedHeader()) {
741+
$expectedHeaders[] = $tokenData->getSharedUnprotectedHeader();
742+
}
743+
if ($tokenData->getPerRecipientUnprotectedHeaders()) {
744+
$expectedHeaders = array_merge($expectedHeaders, $tokenData->getPerRecipientUnprotectedHeaders());
745+
}
746+
} elseif ($tokenData instanceof UnsecuredJwtInterface) {
747+
$expectedHeaders = $tokenData->getProtectedHeaders();
748+
if ($tokenData->getUnprotectedHeaders()) {
749+
$expectedHeaders = array_merge($expectedHeaders, $tokenData->getUnprotectedHeaders());
750+
}
751+
}
752+
753+
foreach ($headers as $header) {
754+
$this->verifyAgainstHeaders($expectedHeaders, $header);
755+
}
756+
}
757+
758+
public function getJwtsForHeaders(): array
759+
{
760+
761+
/** @var JwkFactory $jwkFactory */
762+
$jwkFactory = Bootstrap::getObjectManager()->get(JwkFactory::class);
763+
764+
$flatJws = new Jws(
765+
[
766+
new JwsHeader(
767+
[
768+
new PrivateHeaderParameter('custom-header', 'value'),
769+
new PrivateHeaderParameter('another-custom-header', 'value2')
770+
]
771+
)
772+
],
773+
new ClaimsPayload(
774+
[
775+
new PrivateClaim('custom-claim', 'value'),
776+
new PrivateClaim('custom-claim2', 'value2'),
777+
new PrivateClaim('custom-claim3', 'value3'),
778+
new IssuedAt(new \DateTimeImmutable()),
779+
new Issuer('magento.com')
780+
]
781+
),
782+
null
783+
);
784+
$flatJsonJws = new Jws(
785+
[
786+
new JwsHeader(
787+
[
788+
new PrivateHeaderParameter('custom-header', 'value'),
789+
new Critical(['magento'])
790+
]
791+
)
792+
],
793+
new ClaimsPayload(
794+
[
795+
new PrivateClaim('custom-claim', 'value'),
796+
new PrivateClaim('custom-claim2', 'value2'),
797+
new ExpirationTime(new \DateTimeImmutable())
798+
]
799+
),
800+
[
801+
new JwsHeader(
802+
[
803+
new PublicHeaderParameter('public-header', 'magento', 'public-value')
804+
]
805+
)
806+
]
807+
);
808+
$jsonJws = new Jws(
809+
[
810+
new JwsHeader(
811+
[
812+
new PrivateHeaderParameter('test', true),
813+
new PublicHeaderParameter('test2', 'magento', 'value')
814+
]
815+
),
816+
new JwsHeader(
817+
[
818+
new PrivateHeaderParameter('test3', true),
819+
new PublicHeaderParameter('test4', 'magento', 'value-another')
820+
]
821+
)
822+
],
823+
new ClaimsPayload([
824+
new Issuer('magento.com'),
825+
new JwtId(),
826+
new Subject('stuff')
827+
]),
828+
[
829+
new JwsHeader([new PrivateHeaderParameter('public', 'header1')]),
830+
new JwsHeader([new PrivateHeaderParameter('public2', 'header')])
831+
]
832+
);
833+
$flatJwe = new Jwe(
834+
new JweHeader(
835+
[
836+
new PrivateHeaderParameter('test', true),
837+
new PublicHeaderParameter('test2', 'magento', 'value')
838+
]
839+
),
840+
null,
841+
null,
842+
new ClaimsPayload(
843+
[
844+
new PrivateClaim('custom-claim', 'value'),
845+
new PrivateClaim('custom-claim2', 'value2', true),
846+
new PrivateClaim('custom-claim3', 'value3'),
847+
new IssuedAt(new \DateTimeImmutable()),
848+
new Issuer('magento.com')
849+
]
850+
)
851+
);
852+
$jsonFlatJwe = new Jwe(
853+
new JweHeader(
854+
[
855+
new PrivateHeaderParameter('test', true),
856+
new PublicHeaderParameter('test2', 'magento', 'value')
857+
]
858+
),
859+
null,
860+
[
861+
new JweHeader(
862+
[
863+
new PrivateHeaderParameter('mage', 'test')
864+
]
865+
)
866+
],
867+
new ClaimsPayload(
868+
[
869+
new PrivateClaim('custom-claim', 'value'),
870+
new PrivateClaim('custom-claim2', 'value2', true),
871+
new PrivateClaim('custom-claim3', 'value3'),
872+
new IssuedAt(new \DateTimeImmutable()),
873+
new Issuer('magento.com')
874+
]
875+
)
876+
);
877+
$jsonJwe = new Jwe(
878+
new JweHeader(
879+
[
880+
new PrivateHeaderParameter('test', true),
881+
new PublicHeaderParameter('test2', 'magento', 'value')
882+
]
883+
),
884+
new JweHeader(
885+
[
886+
new PrivateHeaderParameter('mage', 'test')
887+
]
888+
),
889+
[
890+
new JweHeader([new PrivateHeaderParameter('tst', 2)]),
891+
new JweHeader([new PrivateHeaderParameter('test2', 3)])
892+
],
893+
new ClaimsPayload(
894+
[
895+
new PrivateClaim('custom-claim', 'value'),
896+
new PrivateClaim('custom-claim2', 'value2', true),
897+
new PrivateClaim('custom-claim3', 'value3'),
898+
new IssuedAt(new \DateTimeImmutable()),
899+
new Issuer('magento.com')
900+
]
901+
)
902+
);
903+
$flatUnsecured = new UnsecuredJwt(
904+
[
905+
new JwsHeader(
906+
[
907+
new PrivateHeaderParameter('test', true),
908+
new PublicHeaderParameter('test2', 'magento', 'value')
909+
]
910+
)
911+
],
912+
new ClaimsPayload(
913+
[
914+
new PrivateClaim('custom-claim', 'value'),
915+
new PrivateClaim('custom-claim2', 'value2', true),
916+
new PrivateClaim('custom-claim3', 'value3'),
917+
new IssuedAt(new \DateTimeImmutable()),
918+
new Issuer('magento.com')
919+
]
920+
),
921+
null
922+
);
923+
924+
$sharedSecret = random_bytes(2048);
925+
$jwsJwk = $jwkFactory->createHs256($sharedSecret);
926+
$jweJwk = $jwkFactory->createA128KW($sharedSecret);
927+
$jwsSettings = new JwsSignatureJwks($jwsJwk);
928+
$jsonJwsSettings = new JwsSignatureJwks(new JwkSet([$jwsJwk, $jwsJwk]));
929+
$jweJwkSettings = new JweEncryptionJwks(
930+
$jweJwk,
931+
JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM
932+
);
933+
$jsonJweSettings = new JweEncryptionJwks(
934+
new JwkSet([$jweJwk, $jweJwk]),
935+
JweEncryptionSettingsInterface::CONTENT_ENCRYPTION_ALGO_A128GCM
936+
);
937+
938+
return [
939+
'jws' => [$flatJws, $jwsSettings],
940+
'flat-jws' => [$flatJsonJws, $jwsSettings],
941+
'json-jws' => [$jsonJws, $jsonJwsSettings],
942+
'jwe' => [$flatJwe, $jweJwkSettings],
943+
'flat-jwe' => [$jsonFlatJwe, $jweJwkSettings],
944+
'json-jwe' => [$jsonJwe, $jsonJweSettings],
945+
'none-jws' => [$flatUnsecured, new NoEncryption()]
946+
];
947+
}
948+
718949
private function validateHeader(HeaderInterface $expected, HeaderInterface $actual): void
719950
{
720951
if (count($expected->getParameters()) > count($actual->getParameters())) {

lib/internal/Magento/Framework/Jwt/JwtManagerInterface.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,12 @@ public function create(JwtInterface $jwt, EncryptionSettingsInterface $encryptio
3434
* @throws JwtException
3535
*/
3636
public function read(string $token, array $acceptableEncryption): JwtInterface;
37+
38+
/**
39+
* Read unprotected headers.
40+
*
41+
* @param string $token
42+
* @return HeaderInterface[]
43+
*/
44+
public function readHeaders(string $token): array;
3745
}

0 commit comments

Comments
 (0)