├── .github ├── CONTRIBUTING.md ├── FUNDING.yml └── PULL_REQUEST_TEMPLATE.md ├── Algorithm ├── MacAlgorithm.php └── SignatureAlgorithm.php ├── JWS.php ├── JWSBuilder.php ├── JWSBuilderFactory.php ├── JWSLoader.php ├── JWSLoaderFactory.php ├── JWSTokenSupport.php ├── JWSVerifier.php ├── JWSVerifierFactory.php ├── LICENSE ├── README.md ├── Serializer ├── CompactSerializer.php ├── JSONFlattenedSerializer.php ├── JSONGeneralSerializer.php ├── JWSSerializer.php ├── JWSSerializerManager.php ├── JWSSerializerManagerFactory.php └── Serializer.php ├── Signature.php └── composer.json /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY. 4 | Please do not submit any Pull Requests here. It will be automatically closed. 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: FlorentMorselli 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please do not submit any Pull Requests here. It will be automatically closed. 2 | 3 | You should submit it here: https://github.com/web-token/jwt-framework/pulls 4 | -------------------------------------------------------------------------------- /Algorithm/MacAlgorithm.php: -------------------------------------------------------------------------------- 1 | payload; 28 | } 29 | 30 | /** 31 | * Returns true if the payload is detached. 32 | */ 33 | public function isPayloadDetached(): bool 34 | { 35 | return $this->isPayloadDetached; 36 | } 37 | 38 | /** 39 | * Returns the Base64Url encoded payload. If the payload is detached, this method returns null. 40 | */ 41 | public function getEncodedPayload(): ?string 42 | { 43 | if ($this->isPayloadDetached() === true) { 44 | return null; 45 | } 46 | 47 | return $this->encodedPayload; 48 | } 49 | 50 | /** 51 | * Returns the signatures associated with the JWS. 52 | * 53 | * @return Signature[] 54 | */ 55 | public function getSignatures(): array 56 | { 57 | return $this->signatures; 58 | } 59 | 60 | /** 61 | * Returns the signature at the given index. 62 | */ 63 | public function getSignature(int $id): Signature 64 | { 65 | if (isset($this->signatures[$id])) { 66 | return $this->signatures[$id]; 67 | } 68 | 69 | throw new InvalidArgumentException('The signature does not exist.'); 70 | } 71 | 72 | /** 73 | * This method adds a signature to the JWS object. Its returns a new JWS object. 74 | * 75 | * @internal 76 | */ 77 | public function addSignature( 78 | string $signature, 79 | array $protectedHeader, 80 | ?string $encodedProtectedHeader, 81 | array $header = [] 82 | ): self { 83 | $jws = clone $this; 84 | $jws->signatures[] = new Signature($signature, $protectedHeader, $encodedProtectedHeader, $header); 85 | 86 | return $jws; 87 | } 88 | 89 | /** 90 | * Returns the number of signature associated with the JWS. 91 | */ 92 | public function countSignatures(): int 93 | { 94 | return count($this->signatures); 95 | } 96 | 97 | /** 98 | * This method splits the JWS into a list of JWSs. It is only useful when the JWS contains more than one signature 99 | * (JSON General Serialization). 100 | * 101 | * @return JWS[] 102 | */ 103 | public function split(): array 104 | { 105 | $result = []; 106 | foreach ($this->signatures as $signature) { 107 | $jws = new self($this->payload, $this->encodedPayload, $this->isPayloadDetached); 108 | $jws = $jws->addSignature( 109 | $signature->getSignature(), 110 | $signature->getProtectedHeader(), 111 | $signature->getEncodedProtectedHeader(), 112 | $signature->getHeader() 113 | ); 114 | 115 | $result[] = $jws; 116 | } 117 | 118 | return $result; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /JWSBuilder.php: -------------------------------------------------------------------------------- 1 | signatureAlgorithmManager; 44 | } 45 | 46 | /** 47 | * Reset the current data. 48 | */ 49 | public function create(): self 50 | { 51 | $this->payload = null; 52 | $this->isPayloadDetached = false; 53 | $this->signatures = []; 54 | $this->isPayloadEncoded = null; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Set the payload. This method will return a new JWSBuilder object. 61 | */ 62 | public function withPayload(string $payload, bool $isPayloadDetached = false): self 63 | { 64 | if (mb_detect_encoding($payload, 'UTF-8', true) === false) { 65 | throw new InvalidArgumentException('The payload must be encoded in UTF-8'); 66 | } 67 | $clone = clone $this; 68 | $clone->payload = $payload; 69 | $clone->isPayloadDetached = $isPayloadDetached; 70 | 71 | return $clone; 72 | } 73 | 74 | /** 75 | * Adds the information needed to compute the signature. This method will return a new JWSBuilder object. 76 | */ 77 | public function addSignature(JWK $signatureKey, array $protectedHeader, array $header = []): self 78 | { 79 | $this->checkB64AndCriticalHeader($protectedHeader); 80 | $isPayloadEncoded = $this->checkIfPayloadIsEncoded($protectedHeader); 81 | if ($this->isPayloadEncoded === null) { 82 | $this->isPayloadEncoded = $isPayloadEncoded; 83 | } elseif ($this->isPayloadEncoded !== $isPayloadEncoded) { 84 | throw new InvalidArgumentException('Foreign payload encoding detected.'); 85 | } 86 | $this->checkDuplicatedHeaderParameters($protectedHeader, $header); 87 | KeyChecker::checkKeyUsage($signatureKey, 'signature'); 88 | $algorithm = $this->findSignatureAlgorithm($signatureKey, $protectedHeader, $header); 89 | KeyChecker::checkKeyAlgorithm($signatureKey, $algorithm->name()); 90 | $clone = clone $this; 91 | $clone->signatures[] = [ 92 | 'signature_algorithm' => $algorithm, 93 | 'signature_key' => $signatureKey, 94 | 'protected_header' => $protectedHeader, 95 | 'header' => $header, 96 | ]; 97 | 98 | return $clone; 99 | } 100 | 101 | /** 102 | * Computes all signatures and return the expected JWS object. 103 | */ 104 | public function build(): JWS 105 | { 106 | if ($this->payload === null) { 107 | throw new RuntimeException('The payload is not set.'); 108 | } 109 | if (count($this->signatures) === 0) { 110 | throw new RuntimeException('At least one signature must be set.'); 111 | } 112 | 113 | $encodedPayload = $this->isPayloadEncoded === false ? $this->payload : Base64UrlSafe::encodeUnpadded( 114 | $this->payload 115 | ); 116 | $jws = new JWS($this->payload, $encodedPayload, $this->isPayloadDetached); 117 | foreach ($this->signatures as $signature) { 118 | /** @var MacAlgorithm|SignatureAlgorithm $algorithm */ 119 | $algorithm = $signature['signature_algorithm']; 120 | /** @var JWK $signatureKey */ 121 | $signatureKey = $signature['signature_key']; 122 | /** @var array $protectedHeader */ 123 | $protectedHeader = $signature['protected_header']; 124 | /** @var array $header */ 125 | $header = $signature['header']; 126 | $encodedProtectedHeader = count($protectedHeader) === 0 ? null : Base64UrlSafe::encodeUnpadded( 127 | JsonConverter::encode($protectedHeader) 128 | ); 129 | $input = sprintf('%s.%s', $encodedProtectedHeader, $encodedPayload); 130 | if ($algorithm instanceof SignatureAlgorithm) { 131 | $s = $algorithm->sign($signatureKey, $input); 132 | } else { 133 | $s = $algorithm->hash($signatureKey, $input); 134 | } 135 | $jws = $jws->addSignature($s, $protectedHeader, $encodedProtectedHeader, $header); 136 | } 137 | 138 | return $jws; 139 | } 140 | 141 | private function checkIfPayloadIsEncoded(array $protectedHeader): bool 142 | { 143 | return ! array_key_exists('b64', $protectedHeader) || $protectedHeader['b64'] === true; 144 | } 145 | 146 | private function checkB64AndCriticalHeader(array $protectedHeader): void 147 | { 148 | if (! array_key_exists('b64', $protectedHeader)) { 149 | return; 150 | } 151 | if (! array_key_exists('crit', $protectedHeader)) { 152 | throw new LogicException( 153 | 'The protected header parameter "crit" is mandatory when protected header parameter "b64" is set.' 154 | ); 155 | } 156 | if (! is_array($protectedHeader['crit'])) { 157 | throw new LogicException('The protected header parameter "crit" must be an array.'); 158 | } 159 | if (! in_array('b64', $protectedHeader['crit'], true)) { 160 | throw new LogicException( 161 | 'The protected header parameter "crit" must contain "b64" when protected header parameter "b64" is set.' 162 | ); 163 | } 164 | } 165 | 166 | /** 167 | * @return MacAlgorithm|SignatureAlgorithm 168 | */ 169 | private function findSignatureAlgorithm(JWK $key, array $protectedHeader, array $header): Algorithm 170 | { 171 | $completeHeader = array_merge($header, $protectedHeader); 172 | if (! array_key_exists('alg', $completeHeader)) { 173 | throw new InvalidArgumentException('No "alg" parameter set in the header.'); 174 | } 175 | if ($key->has('alg') && $key->get('alg') !== $completeHeader['alg']) { 176 | throw new InvalidArgumentException(sprintf( 177 | 'The algorithm "%s" is not allowed with this key.', 178 | $completeHeader['alg'] 179 | )); 180 | } 181 | 182 | $algorithm = $this->signatureAlgorithmManager->get($completeHeader['alg']); 183 | if (! $algorithm instanceof SignatureAlgorithm && ! $algorithm instanceof MacAlgorithm) { 184 | throw new InvalidArgumentException(sprintf('The algorithm "%s" is not supported.', $completeHeader['alg'])); 185 | } 186 | 187 | return $algorithm; 188 | } 189 | 190 | private function checkDuplicatedHeaderParameters(array $header1, array $header2): void 191 | { 192 | $inter = array_intersect_key($header1, $header2); 193 | if (count($inter) !== 0) { 194 | throw new InvalidArgumentException(sprintf( 195 | 'The header contains duplicated entries: %s.', 196 | implode(', ', array_keys($inter)) 197 | )); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /JWSBuilderFactory.php: -------------------------------------------------------------------------------- 1 | signatureAlgorithmManagerFactory->create($algorithms); 24 | 25 | return new JWSBuilder($algorithmManager); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /JWSLoader.php: -------------------------------------------------------------------------------- 1 | jwsVerifier; 29 | } 30 | 31 | /** 32 | * Returns the Header Checker Manager associated to the JWSLoader. 33 | */ 34 | public function getHeaderCheckerManager(): ?HeaderCheckerManager 35 | { 36 | return $this->headerCheckerManager; 37 | } 38 | 39 | /** 40 | * Returns the JWSSerializer associated to the JWSLoader. 41 | */ 42 | public function getSerializerManager(): JWSSerializerManager 43 | { 44 | return $this->serializerManager; 45 | } 46 | 47 | /** 48 | * This method will try to load and verify the token using the given key. It returns a JWS and will populate the 49 | * $signature variable in case of success, otherwise an exception is thrown. 50 | */ 51 | public function loadAndVerifyWithKey(string $token, JWK $key, ?int &$signature, ?string $payload = null): JWS 52 | { 53 | $keyset = new JWKSet([$key]); 54 | 55 | return $this->loadAndVerifyWithKeySet($token, $keyset, $signature, $payload); 56 | } 57 | 58 | /** 59 | * This method will try to load and verify the token using the given key set. It returns a JWS and will populate the 60 | * $signature variable in case of success, otherwise an exception is thrown. 61 | */ 62 | public function loadAndVerifyWithKeySet( 63 | string $token, 64 | JWKSet $keyset, 65 | ?int &$signature, 66 | ?string $payload = null 67 | ): JWS { 68 | try { 69 | $jws = $this->serializerManager->unserialize($token); 70 | $nbSignatures = $jws->countSignatures(); 71 | for ($i = 0; $i < $nbSignatures; ++$i) { 72 | if ($this->processSignature($jws, $keyset, $i, $payload)) { 73 | $signature = $i; 74 | 75 | return $jws; 76 | } 77 | } 78 | } catch (Throwable) { 79 | // Nothing to do. Exception thrown just after 80 | } 81 | 82 | throw new Exception('Unable to load and verify the token.'); 83 | } 84 | 85 | private function processSignature(JWS $jws, JWKSet $keyset, int $signature, ?string $payload): bool 86 | { 87 | try { 88 | if ($this->headerCheckerManager !== null) { 89 | $this->headerCheckerManager->check($jws, $signature); 90 | } 91 | 92 | return $this->jwsVerifier->verifyWithKeySet($jws, $keyset, $signature, $payload); 93 | } catch (Throwable) { 94 | return false; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /JWSLoaderFactory.php: -------------------------------------------------------------------------------- 1 | jwsSerializerManagerFactory->create($serializers); 26 | $jwsVerifier = $this->jwsVerifierFactory->create($algorithms); 27 | if ($this->headerCheckerManagerFactory !== null) { 28 | $headerCheckerManager = $this->headerCheckerManagerFactory->create($headerCheckers); 29 | } else { 30 | $headerCheckerManager = null; 31 | } 32 | 33 | return new JWSLoader($serializerManager, $jwsVerifier, $headerCheckerManager); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /JWSTokenSupport.php: -------------------------------------------------------------------------------- 1 | $jwt->countSignatures()) { 25 | throw new InvalidArgumentException('Unknown signature index.'); 26 | } 27 | $protectedHeader = $jwt->getSignature($index) 28 | ->getProtectedHeader() 29 | ; 30 | $unprotectedHeader = $jwt->getSignature($index) 31 | ->getHeader() 32 | ; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /JWSVerifier.php: -------------------------------------------------------------------------------- 1 | signatureAlgorithmManager; 31 | } 32 | 33 | /** 34 | * This method will try to verify the JWS object using the given key and for the given signature. It returns true if 35 | * the signature is verified, otherwise false. 36 | * 37 | * @return bool true if the verification of the signature succeeded, else false 38 | */ 39 | public function verifyWithKey(JWS $jws, JWK $jwk, int $signature, ?string $detachedPayload = null): bool 40 | { 41 | $jwkset = new JWKSet([$jwk]); 42 | 43 | return $this->verifyWithKeySet($jws, $jwkset, $signature, $detachedPayload); 44 | } 45 | 46 | /** 47 | * This method will try to verify the JWS object using the given key set and for the given signature. It returns 48 | * true if the signature is verified, otherwise false. 49 | * 50 | * @param JWS $jws A JWS object 51 | * @param JWKSet $jwkset The signature will be verified using keys in the key set 52 | * @param JWK $jwk The key used to verify the signature in case of success 53 | * @param string|null $detachedPayload If not null, the value must be the detached payload encoded in Base64 URL safe. If the input contains a payload, throws an exception. 54 | * 55 | * @return bool true if the verification of the signature succeeded, else false 56 | */ 57 | public function verifyWithKeySet( 58 | JWS $jws, 59 | JWKSet $jwkset, 60 | int $signatureIndex, 61 | ?string $detachedPayload = null, 62 | JWK &$jwk = null 63 | ): bool { 64 | if ($jwkset->count() === 0) { 65 | throw new InvalidArgumentException('There is no key in the key set.'); 66 | } 67 | if ($jws->countSignatures() === 0) { 68 | throw new InvalidArgumentException('The JWS does not contain any signature.'); 69 | } 70 | $this->checkPayload($jws, $detachedPayload); 71 | $signature = $jws->getSignature($signatureIndex); 72 | 73 | return $this->verifySignature($jws, $jwkset, $signature, $detachedPayload, $jwk); 74 | } 75 | 76 | private function verifySignature( 77 | JWS $jws, 78 | JWKSet $jwkset, 79 | Signature $signature, 80 | ?string $detachedPayload = null, 81 | JWK &$successJwk = null 82 | ): bool { 83 | $input = $this->getInputToVerify($jws, $signature, $detachedPayload); 84 | $algorithm = $this->getAlgorithm($signature); 85 | foreach ($jwkset->all() as $jwk) { 86 | try { 87 | KeyChecker::checkKeyUsage($jwk, 'verification'); 88 | KeyChecker::checkKeyAlgorithm($jwk, $algorithm->name()); 89 | if ($algorithm->verify($jwk, $input, $signature->getSignature()) === true) { 90 | $successJwk = $jwk; 91 | 92 | return true; 93 | } 94 | } catch (Throwable) { 95 | //We do nothing, we continue with other keys 96 | continue; 97 | } 98 | } 99 | 100 | return false; 101 | } 102 | 103 | private function getInputToVerify(JWS $jws, Signature $signature, ?string $detachedPayload): string 104 | { 105 | $payload = $jws->getPayload(); 106 | $isPayloadEmpty = $payload === null || $payload === ''; 107 | $encodedProtectedHeader = $signature->getEncodedProtectedHeader() ?? ''; 108 | $isPayloadBase64Encoded = ! $signature->hasProtectedHeaderParameter( 109 | 'b64' 110 | ) || $signature->getProtectedHeaderParameter('b64') === true; 111 | $encodedPayload = $jws->getEncodedPayload(); 112 | 113 | if ($isPayloadBase64Encoded && $encodedPayload !== null) { 114 | return sprintf('%s.%s', $encodedProtectedHeader, $encodedPayload); 115 | } 116 | 117 | $callable = $isPayloadBase64Encoded === true ? static fn (?string $p): string => Base64UrlSafe::encodeUnpadded( 118 | $p ?? '' 119 | ) 120 | : static fn (?string $p): string => $p ?? ''; 121 | 122 | $payloadToUse = $callable($isPayloadEmpty ? $detachedPayload : $payload); 123 | 124 | return sprintf('%s.%s', $encodedProtectedHeader, $payloadToUse); 125 | } 126 | 127 | private function checkPayload(JWS $jws, ?string $detachedPayload = null): void 128 | { 129 | $isPayloadEmpty = $this->isPayloadEmpty($jws->getPayload()); 130 | if ($detachedPayload !== null && ! $isPayloadEmpty) { 131 | throw new InvalidArgumentException('A detached payload is set, but the JWS already has a payload.'); 132 | } 133 | if ($isPayloadEmpty && $detachedPayload === null) { 134 | throw new InvalidArgumentException('The JWS has a detached payload, but no payload is provided.'); 135 | } 136 | } 137 | 138 | /** 139 | * @return MacAlgorithm|SignatureAlgorithm 140 | */ 141 | private function getAlgorithm(Signature $signature): Algorithm 142 | { 143 | $completeHeader = array_merge($signature->getProtectedHeader(), $signature->getHeader()); 144 | if (! isset($completeHeader['alg'])) { 145 | throw new InvalidArgumentException('No "alg" parameter set in the header.'); 146 | } 147 | 148 | $algorithm = $this->signatureAlgorithmManager->get($completeHeader['alg']); 149 | if (! $algorithm instanceof SignatureAlgorithm && ! $algorithm instanceof MacAlgorithm) { 150 | throw new InvalidArgumentException(sprintf( 151 | 'The algorithm "%s" is not supported or is not a signature or MAC algorithm.', 152 | $completeHeader['alg'] 153 | )); 154 | } 155 | 156 | return $algorithm; 157 | } 158 | 159 | private function isPayloadEmpty(?string $payload): bool 160 | { 161 | return $payload === null || $payload === ''; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /JWSVerifierFactory.php: -------------------------------------------------------------------------------- 1 | algorithmManagerFactory->create($algorithms); 24 | 25 | return new JWSVerifier($algorithmManager); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2019 Spomky-Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP JWT Signature Component 2 | =========================== 3 | 4 | This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY. 5 | 6 | **Please do not submit any Pull Request here.** 7 | You should go to [the main repository](https://github.com/web-token/jwt-framework) instead. 8 | 9 | # Documentation 10 | 11 | The official documentation is available as https://web-token.spomky-labs.com/ 12 | 13 | # Licence 14 | 15 | This software is release under [MIT licence](LICENSE). 16 | -------------------------------------------------------------------------------- /Serializer/CompactSerializer.php: -------------------------------------------------------------------------------- 1 | getSignature($signatureIndex); 36 | if (count($signature->getHeader()) !== 0) { 37 | throw new LogicException( 38 | 'The signature contains unprotected header parameters and cannot be converted into compact JSON.' 39 | ); 40 | } 41 | $isEmptyPayload = $jws->getEncodedPayload() === null || $jws->getEncodedPayload() === ''; 42 | if (! $isEmptyPayload && ! $this->isPayloadEncoded($signature->getProtectedHeader())) { 43 | if (preg_match('/^[\x{20}-\x{2d}|\x{2f}-\x{7e}]*$/u', $jws->getPayload() ?? '') !== 1) { 44 | throw new LogicException('Unable to convert the JWS with non-encoded payload.'); 45 | } 46 | } 47 | 48 | return sprintf( 49 | '%s.%s.%s', 50 | $signature->getEncodedProtectedHeader(), 51 | $jws->getEncodedPayload(), 52 | Base64UrlSafe::encodeUnpadded($signature->getSignature()) 53 | ); 54 | } 55 | 56 | public function unserialize(string $input): JWS 57 | { 58 | $parts = explode('.', $input); 59 | if (count($parts) !== 3) { 60 | throw new InvalidArgumentException('Unsupported input'); 61 | } 62 | 63 | try { 64 | $encodedProtectedHeader = $parts[0]; 65 | $protectedHeader = JsonConverter::decode(Base64UrlSafe::decode($parts[0])); 66 | if (! is_array($protectedHeader)) { 67 | throw new InvalidArgumentException('Bad protected header.'); 68 | } 69 | $hasPayload = $parts[1] !== ''; 70 | if (! $hasPayload) { 71 | $payload = null; 72 | $encodedPayload = null; 73 | } else { 74 | $encodedPayload = $parts[1]; 75 | $payload = $this->isPayloadEncoded($protectedHeader) ? Base64UrlSafe::decode( 76 | $encodedPayload 77 | ) : $encodedPayload; 78 | } 79 | $signature = Base64UrlSafe::decode($parts[2]); 80 | 81 | $jws = new JWS($payload, $encodedPayload, ! $hasPayload); 82 | 83 | return $jws->addSignature($signature, $protectedHeader, $encodedProtectedHeader); 84 | } catch (Throwable $throwable) { 85 | throw new InvalidArgumentException('Unsupported input', $throwable->getCode(), $throwable); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Serializer/JSONFlattenedSerializer.php: -------------------------------------------------------------------------------- 1 | getSignature($signatureIndex); 34 | 35 | $data = []; 36 | $encodedPayload = $jws->getEncodedPayload(); 37 | if ($encodedPayload !== null && $encodedPayload !== '') { 38 | $data['payload'] = $encodedPayload; 39 | } 40 | $encodedProtectedHeader = $signature->getEncodedProtectedHeader(); 41 | if ($encodedProtectedHeader !== null && $encodedProtectedHeader !== '') { 42 | $data['protected'] = $encodedProtectedHeader; 43 | } 44 | $header = $signature->getHeader(); 45 | if (count($header) !== 0) { 46 | $data['header'] = $header; 47 | } 48 | $data['signature'] = Base64UrlSafe::encodeUnpadded($signature->getSignature()); 49 | 50 | return JsonConverter::encode($data); 51 | } 52 | 53 | public function unserialize(string $input): JWS 54 | { 55 | $data = JsonConverter::decode($input); 56 | if (! is_array($data)) { 57 | throw new InvalidArgumentException('Unsupported input.'); 58 | } 59 | if (! isset($data['signature'])) { 60 | throw new InvalidArgumentException('Unsupported input.'); 61 | } 62 | $signature = Base64UrlSafe::decode($data['signature']); 63 | 64 | if (isset($data['protected'])) { 65 | $encodedProtectedHeader = $data['protected']; 66 | $protectedHeader = JsonConverter::decode(Base64UrlSafe::decode($data['protected'])); 67 | if (! is_array($protectedHeader)) { 68 | throw new InvalidArgumentException('Bad protected header.'); 69 | } 70 | } else { 71 | $encodedProtectedHeader = null; 72 | $protectedHeader = []; 73 | } 74 | if (isset($data['header'])) { 75 | if (! is_array($data['header'])) { 76 | throw new InvalidArgumentException('Bad header.'); 77 | } 78 | $header = $data['header']; 79 | } else { 80 | $header = []; 81 | } 82 | 83 | if (isset($data['payload'])) { 84 | $encodedPayload = $data['payload']; 85 | $payload = $this->isPayloadEncoded($protectedHeader) ? Base64UrlSafe::decode( 86 | $encodedPayload 87 | ) : $encodedPayload; 88 | } else { 89 | $payload = null; 90 | $encodedPayload = null; 91 | } 92 | 93 | $jws = new JWS($payload, $encodedPayload, $encodedPayload === null); 94 | 95 | return $jws->addSignature($signature, $protectedHeader, $encodedProtectedHeader, $header); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Serializer/JSONGeneralSerializer.php: -------------------------------------------------------------------------------- 1 | countSignatures() === 0) { 34 | throw new LogicException('No signature.'); 35 | } 36 | 37 | $data = []; 38 | $this->checkPayloadEncoding($jws); 39 | 40 | if ($jws->isPayloadDetached() === false) { 41 | $data['payload'] = $jws->getEncodedPayload(); 42 | } 43 | 44 | $data['signatures'] = []; 45 | foreach ($jws->getSignatures() as $signature) { 46 | $tmp = [ 47 | 'signature' => Base64UrlSafe::encodeUnpadded($signature->getSignature()), 48 | ]; 49 | $values = [ 50 | 'protected' => $signature->getEncodedProtectedHeader(), 51 | 'header' => $signature->getHeader(), 52 | ]; 53 | 54 | foreach ($values as $key => $value) { 55 | if ((is_string($value) && $value !== '') || (is_array($value) && count($value) !== 0)) { 56 | $tmp[$key] = $value; 57 | } 58 | } 59 | $data['signatures'][] = $tmp; 60 | } 61 | 62 | return JsonConverter::encode($data); 63 | } 64 | 65 | public function unserialize(string $input): JWS 66 | { 67 | $data = JsonConverter::decode($input); 68 | if (! is_array($data)) { 69 | throw new InvalidArgumentException('Unsupported input.'); 70 | } 71 | if (! isset($data['signatures'])) { 72 | throw new InvalidArgumentException('Unsupported input.'); 73 | } 74 | 75 | $isPayloadEncoded = null; 76 | $rawPayload = $data['payload'] ?? null; 77 | $signatures = []; 78 | foreach ($data['signatures'] as $signature) { 79 | if (! isset($signature['signature'])) { 80 | throw new InvalidArgumentException('Unsupported input.'); 81 | } 82 | [$encodedProtectedHeader, $protectedHeader, $header] = $this->processHeaders($signature); 83 | $signatures[] = [ 84 | 'signature' => Base64UrlSafe::decode($signature['signature']), 85 | 'protected' => $protectedHeader, 86 | 'encoded_protected' => $encodedProtectedHeader, 87 | 'header' => $header, 88 | ]; 89 | $isPayloadEncoded = $this->processIsPayloadEncoded($isPayloadEncoded, $protectedHeader); 90 | } 91 | 92 | $payload = $this->processPayload($rawPayload, $isPayloadEncoded); 93 | $jws = new JWS($payload, $rawPayload); 94 | foreach ($signatures as $signature) { 95 | $jws = $jws->addSignature( 96 | $signature['signature'], 97 | $signature['protected'], 98 | $signature['encoded_protected'], 99 | $signature['header'] 100 | ); 101 | } 102 | 103 | return $jws; 104 | } 105 | 106 | private function processIsPayloadEncoded(?bool $isPayloadEncoded, array $protectedHeader): bool 107 | { 108 | if ($isPayloadEncoded === null) { 109 | return $this->isPayloadEncoded($protectedHeader); 110 | } 111 | if ($this->isPayloadEncoded($protectedHeader) !== $isPayloadEncoded) { 112 | throw new InvalidArgumentException('Foreign payload encoding detected.'); 113 | } 114 | 115 | return $isPayloadEncoded; 116 | } 117 | 118 | private function processHeaders(array $signature): array 119 | { 120 | $encodedProtectedHeader = $signature['protected'] ?? null; 121 | $protectedHeader = $encodedProtectedHeader === null ? [] : JsonConverter::decode( 122 | Base64UrlSafe::decode($encodedProtectedHeader) 123 | ); 124 | $header = array_key_exists('header', $signature) ? $signature['header'] : []; 125 | 126 | return [$encodedProtectedHeader, $protectedHeader, $header]; 127 | } 128 | 129 | private function processPayload(?string $rawPayload, ?bool $isPayloadEncoded): ?string 130 | { 131 | if ($rawPayload === null) { 132 | return null; 133 | } 134 | 135 | return $isPayloadEncoded === false ? $rawPayload : Base64UrlSafe::decode($rawPayload); 136 | } 137 | 138 | private function checkPayloadEncoding(JWS $jws): void 139 | { 140 | if ($jws->isPayloadDetached()) { 141 | return; 142 | } 143 | $is_encoded = null; 144 | foreach ($jws->getSignatures() as $signature) { 145 | if ($is_encoded === null) { 146 | $is_encoded = $this->isPayloadEncoded($signature->getProtectedHeader()); 147 | } 148 | if ($is_encoded !== $this->isPayloadEncoded($signature->getProtectedHeader())) { 149 | throw new LogicException('Foreign payload encoding detected.'); 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Serializer/JWSSerializer.php: -------------------------------------------------------------------------------- 1 | add($serializer); 24 | } 25 | } 26 | 27 | /** 28 | * @return string[] 29 | */ 30 | public function list(): array 31 | { 32 | return array_keys($this->serializers); 33 | } 34 | 35 | /** 36 | * Converts a JWS into a string. 37 | */ 38 | public function serialize(string $name, JWS $jws, ?int $signatureIndex = null): string 39 | { 40 | if (! isset($this->serializers[$name])) { 41 | throw new InvalidArgumentException(sprintf('Unsupported serializer "%s".', $name)); 42 | } 43 | 44 | return $this->serializers[$name]->serialize($jws, $signatureIndex); 45 | } 46 | 47 | /** 48 | * Loads data and return a JWS object. 49 | * 50 | * @param string $input A string that represents a JWS 51 | * @param string|null $name the name of the serializer if the input is unserialized 52 | */ 53 | public function unserialize(string $input, ?string &$name = null): JWS 54 | { 55 | foreach ($this->serializers as $serializer) { 56 | try { 57 | $jws = $serializer->unserialize($input); 58 | $name = $serializer->name(); 59 | 60 | return $jws; 61 | } catch (InvalidArgumentException) { 62 | continue; 63 | } 64 | } 65 | 66 | throw new InvalidArgumentException('Unsupported input.'); 67 | } 68 | 69 | private function add(JWSSerializer $serializer): void 70 | { 71 | $this->serializers[$serializer->name()] = $serializer; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Serializer/JWSSerializerManagerFactory.php: -------------------------------------------------------------------------------- 1 | serializers[$name])) { 24 | throw new InvalidArgumentException(sprintf('Unsupported serializer "%s".', $name)); 25 | } 26 | $serializers[] = $this->serializers[$name]; 27 | } 28 | 29 | return new JWSSerializerManager($serializers); 30 | } 31 | 32 | /** 33 | * @return string[] 34 | */ 35 | public function names(): array 36 | { 37 | return array_keys($this->serializers); 38 | } 39 | 40 | /** 41 | * @return JWSSerializer[] 42 | */ 43 | public function all(): array 44 | { 45 | return $this->serializers; 46 | } 47 | 48 | public function add(JWSSerializer $serializer): void 49 | { 50 | $this->serializers[$serializer->name()] = $serializer; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Serializer/Serializer.php: -------------------------------------------------------------------------------- 1 | protectedHeader = $encodedProtectedHeader === null ? [] : $protectedHeader; 23 | $this->encodedProtectedHeader = $encodedProtectedHeader; 24 | } 25 | 26 | /** 27 | * The protected header associated with the signature. 28 | */ 29 | public function getProtectedHeader(): array 30 | { 31 | return $this->protectedHeader; 32 | } 33 | 34 | /** 35 | * The unprotected header associated with the signature. 36 | */ 37 | public function getHeader(): array 38 | { 39 | return $this->header; 40 | } 41 | 42 | /** 43 | * The protected header associated with the signature. 44 | */ 45 | public function getEncodedProtectedHeader(): ?string 46 | { 47 | return $this->encodedProtectedHeader; 48 | } 49 | 50 | /** 51 | * Returns the value of the protected header of the specified key. 52 | * 53 | * @param string $key The key 54 | * 55 | * @return mixed|null Header value 56 | */ 57 | public function getProtectedHeaderParameter(string $key) 58 | { 59 | if ($this->hasProtectedHeaderParameter($key)) { 60 | return $this->getProtectedHeader()[$key]; 61 | } 62 | 63 | throw new InvalidArgumentException(sprintf('The protected header "%s" does not exist', $key)); 64 | } 65 | 66 | /** 67 | * Returns true if the protected header has the given parameter. 68 | * 69 | * @param string $key The key 70 | */ 71 | public function hasProtectedHeaderParameter(string $key): bool 72 | { 73 | return array_key_exists($key, $this->getProtectedHeader()); 74 | } 75 | 76 | /** 77 | * Returns the value of the unprotected header of the specified key. 78 | * 79 | * @param string $key The key 80 | * 81 | * @return mixed|null Header value 82 | */ 83 | public function getHeaderParameter(string $key) 84 | { 85 | if ($this->hasHeaderParameter($key)) { 86 | return $this->header[$key]; 87 | } 88 | 89 | throw new InvalidArgumentException(sprintf('The header "%s" does not exist', $key)); 90 | } 91 | 92 | /** 93 | * Returns true if the unprotected header has the given parameter. 94 | * 95 | * @param string $key The key 96 | */ 97 | public function hasHeaderParameter(string $key): bool 98 | { 99 | return array_key_exists($key, $this->header); 100 | } 101 | 102 | /** 103 | * Returns the value of the signature. 104 | */ 105 | public function getSignature(): string 106 | { 107 | return $this->signature; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-token/jwt-signature", 3 | "description": "Signature component of the JWT Framework.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jot", "Jose", "RFC7515", "RFC7516", "RFC7517", "RFC7518", "RFC7519", "RFC7520", "Bundle", "Symfony"], 7 | "homepage": "https://github.com/web-token", 8 | "authors": [ 9 | { 10 | "name": "Florent Morselli", 11 | "homepage": "https://github.com/Spomky" 12 | },{ 13 | "name": "All contributors", 14 | "homepage": "https://github.com/web-token/jwt-signature/contributors" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "Jose\\Component\\Signature\\": "" 20 | } 21 | }, 22 | "require": { 23 | "php": ">=8.1", 24 | "web-token/jwt-core": "^3.0" 25 | }, 26 | "suggest": { 27 | "web-token/jwt-signature-algorithm-ecdsa": "ECDSA Based Signature Algorithms", 28 | "web-token/jwt-signature-algorithm-eddsa": "EdDSA Based Signature Algorithms", 29 | "web-token/jwt-signature-algorithm-hmac": "HMAC Based Signature Algorithms", 30 | "web-token/jwt-signature-algorithm-none": "None Signature Algorithm", 31 | "web-token/jwt-signature-algorithm-rsa": "RSA Based Signature Algorithms", 32 | "web-token/jwt-signature-algorithm-experimental": "Experimental Signature Algorithms" 33 | } 34 | } 35 | --------------------------------------------------------------------------------