├── LICENSE ├── composer.json └── src ├── Builder.php ├── ClaimsFormatter.php ├── Configuration.php ├── Decoder.php ├── Encoder.php ├── Encoding ├── CannotDecodeContent.php ├── CannotEncodeContent.php ├── ChainedFormatter.php ├── JoseEncoder.php ├── MicrosecondBasedDateConversion.php ├── UnifyAudience.php └── UnixTimestampDates.php ├── Exception.php ├── JwtFacade.php ├── Parser.php ├── Signer.php ├── Signer ├── Blake2b.php ├── CannotSignPayload.php ├── Ecdsa.php ├── Ecdsa │ ├── ConversionFailed.php │ ├── MultibyteStringConverter.php │ ├── Sha256.php │ ├── Sha384.php │ ├── Sha512.php │ └── SignatureConverter.php ├── Eddsa.php ├── Hmac.php ├── Hmac │ ├── Sha256.php │ ├── Sha384.php │ └── Sha512.php ├── InvalidKeyProvided.php ├── Key.php ├── Key │ ├── FileCouldNotBeRead.php │ └── InMemory.php ├── OpenSSL.php ├── Rsa.php └── Rsa │ ├── Sha256.php │ ├── Sha384.php │ └── Sha512.php ├── SodiumBase64Polyfill.php ├── Token.php ├── Token ├── Builder.php ├── DataSet.php ├── InvalidTokenStructure.php ├── Parser.php ├── Plain.php ├── RegisteredClaimGiven.php ├── RegisteredClaims.php ├── Signature.php └── UnsupportedHeaderFound.php ├── UnencryptedToken.php ├── Validation ├── Constraint.php ├── Constraint │ ├── CannotValidateARegisteredClaim.php │ ├── HasClaim.php │ ├── HasClaimWithValue.php │ ├── IdentifiedBy.php │ ├── IssuedBy.php │ ├── LeewayCannotBeNegative.php │ ├── LooseValidAt.php │ ├── PermittedFor.php │ ├── RelatedTo.php │ ├── SignedWith.php │ ├── SignedWithOneInSet.php │ ├── SignedWithUntilDate.php │ └── StrictValidAt.php ├── ConstraintViolation.php ├── NoConstraintsGiven.php ├── RequiredConstraintsViolated.php ├── SignedWith.php ├── ValidAt.php └── Validator.php └── Validator.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Luís Cobucci 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lcobucci/jwt", 3 | "description": "A simple library to work with JSON Web Token and JSON Web Signature", 4 | "license": [ 5 | "BSD-3-Clause" 6 | ], 7 | "type": "library", 8 | "keywords": [ 9 | "JWT", 10 | "JWS" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Luís Cobucci", 15 | "email": "lcobucci@gmail.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "~8.3.0 || ~8.4.0", 21 | "ext-openssl": "*", 22 | "ext-sodium": "*", 23 | "psr/clock": "^1.0" 24 | }, 25 | "require-dev": { 26 | "infection/infection": "^0.29.10", 27 | "lcobucci/clock": "^3.3.1", 28 | "lcobucci/coding-standard": "^11.1", 29 | "phpbench/phpbench": "^1.4", 30 | "phpstan/extension-installer": "^1.4.3", 31 | "phpstan/phpstan": "^2.1.2", 32 | "phpstan/phpstan-deprecation-rules": "^2.0.1", 33 | "phpstan/phpstan-phpunit": "^2.0.4", 34 | "phpstan/phpstan-strict-rules": "^2.0.3", 35 | "phpunit/phpunit": "^12.0.0" 36 | }, 37 | "suggest": { 38 | "lcobucci/clock": ">= 3.2" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Lcobucci\\JWT\\": "src" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Lcobucci\\JWT\\Tests\\": "tests" 48 | } 49 | }, 50 | "config": { 51 | "allow-plugins": { 52 | "dealerdirect/phpcodesniffer-composer-installer": true, 53 | "infection/extension-installer": true, 54 | "ocramius/package-versions": true, 55 | "phpstan/extension-installer": true 56 | }, 57 | "preferred-install": "dist", 58 | "sort-packages": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | $claims 10 | * 11 | * @return array 12 | */ 13 | public function formatClaims(array $claims): array; 14 | } 15 | -------------------------------------------------------------------------------- /src/Configuration.php: -------------------------------------------------------------------------------- 1 | parser = $parser ?? new Token\Parser($decoder); 42 | $this->validator = $validator ?? new Validation\Validator(); 43 | 44 | $this->builderFactory = $builderFactory 45 | ?? static function (ClaimsFormatter $claimFormatter) use ($encoder): Builder { 46 | return Token\Builder::new($encoder, $claimFormatter); 47 | }; 48 | 49 | $this->validationConstraints = $validationConstraints; 50 | } 51 | 52 | public static function forAsymmetricSigner( 53 | Signer $signer, 54 | Key $signingKey, 55 | Key $verificationKey, 56 | Encoder $encoder = new JoseEncoder(), 57 | Decoder $decoder = new JoseEncoder(), 58 | ): self { 59 | return new self( 60 | $signer, 61 | $signingKey, 62 | $verificationKey, 63 | $encoder, 64 | $decoder, 65 | null, 66 | null, 67 | null, 68 | ); 69 | } 70 | 71 | public static function forSymmetricSigner( 72 | Signer $signer, 73 | Key $key, 74 | Encoder $encoder = new JoseEncoder(), 75 | Decoder $decoder = new JoseEncoder(), 76 | ): self { 77 | return new self( 78 | $signer, 79 | $key, 80 | $key, 81 | $encoder, 82 | $decoder, 83 | null, 84 | null, 85 | null, 86 | ); 87 | } 88 | 89 | /** @param callable(ClaimsFormatter): Builder $builderFactory */ 90 | public function withBuilderFactory(callable $builderFactory): self 91 | { 92 | return new self( 93 | $this->signer, 94 | $this->signingKey, 95 | $this->verificationKey, 96 | $this->encoder, 97 | $this->decoder, 98 | $this->parser, 99 | $this->validator, 100 | $builderFactory(...), 101 | ...$this->validationConstraints, 102 | ); 103 | } 104 | 105 | public function builder(?ClaimsFormatter $claimFormatter = null): Builder 106 | { 107 | return ($this->builderFactory)($claimFormatter ?? ChainedFormatter::default()); 108 | } 109 | 110 | public function parser(): Parser 111 | { 112 | return $this->parser; 113 | } 114 | 115 | public function withParser(Parser $parser): self 116 | { 117 | return new self( 118 | $this->signer, 119 | $this->signingKey, 120 | $this->verificationKey, 121 | $this->encoder, 122 | $this->decoder, 123 | $parser, 124 | $this->validator, 125 | $this->builderFactory, 126 | ...$this->validationConstraints, 127 | ); 128 | } 129 | 130 | public function signer(): Signer 131 | { 132 | return $this->signer; 133 | } 134 | 135 | public function signingKey(): Key 136 | { 137 | return $this->signingKey; 138 | } 139 | 140 | public function verificationKey(): Key 141 | { 142 | return $this->verificationKey; 143 | } 144 | 145 | public function validator(): Validator 146 | { 147 | return $this->validator; 148 | } 149 | 150 | public function withValidator(Validator $validator): self 151 | { 152 | return new self( 153 | $this->signer, 154 | $this->signingKey, 155 | $this->verificationKey, 156 | $this->encoder, 157 | $this->decoder, 158 | $this->parser, 159 | $validator, 160 | $this->builderFactory, 161 | ...$this->validationConstraints, 162 | ); 163 | } 164 | 165 | /** @return Constraint[] */ 166 | public function validationConstraints(): array 167 | { 168 | return $this->validationConstraints; 169 | } 170 | 171 | public function withValidationConstraints(Constraint ...$validationConstraints): self 172 | { 173 | return new self( 174 | $this->signer, 175 | $this->signingKey, 176 | $this->verificationKey, 177 | $this->encoder, 178 | $this->decoder, 179 | $this->parser, 180 | $this->validator, 181 | $this->builderFactory, 182 | ...$validationConstraints, 183 | ); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Decoder.php: -------------------------------------------------------------------------------- 1 | */ 11 | private array $formatters; 12 | 13 | public function __construct(ClaimsFormatter ...$formatters) 14 | { 15 | $this->formatters = $formatters; 16 | } 17 | 18 | public static function default(): self 19 | { 20 | return new self(new UnifyAudience(), new MicrosecondBasedDateConversion()); 21 | } 22 | 23 | public static function withUnixTimestampDates(): self 24 | { 25 | return new self(new UnifyAudience(), new UnixTimestampDates()); 26 | } 27 | 28 | /** @inheritdoc */ 29 | public function formatClaims(array $claims): array 30 | { 31 | foreach ($this->formatters as $formatter) { 32 | $claims = $formatter->formatClaims($claims); 33 | } 34 | 35 | return $claims; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Encoding/JoseEncoder.php: -------------------------------------------------------------------------------- 1 | convertDate($claims[$claim]); 23 | } 24 | 25 | return $claims; 26 | } 27 | 28 | private function convertDate(DateTimeImmutable $date): int|float 29 | { 30 | if ($date->format('u') === '000000') { 31 | return (int) $date->format('U'); 32 | } 33 | 34 | return (float) $date->format('U.u'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Encoding/UnifyAudience.php: -------------------------------------------------------------------------------- 1 | convertDate($claims[$claim]); 23 | } 24 | 25 | return $claims; 26 | } 27 | 28 | private function convertDate(DateTimeImmutable $date): int 29 | { 30 | return $date->getTimestamp(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | clock = $clock ?? new class implements Clock { 28 | public function now(): DateTimeImmutable 29 | { 30 | return new DateTimeImmutable(); 31 | } 32 | }; 33 | } 34 | 35 | /** @param Closure(Builder, DateTimeImmutable):Builder $customiseBuilder */ 36 | public function issue( 37 | Signer $signer, 38 | Key $signingKey, 39 | Closure $customiseBuilder, 40 | ): UnencryptedToken { 41 | $builder = Token\Builder::new(new JoseEncoder(), ChainedFormatter::withUnixTimestampDates()); 42 | 43 | $now = $this->clock->now(); 44 | $builder = $builder 45 | ->issuedAt($now) 46 | ->canOnlyBeUsedAfter($now) 47 | ->expiresAt($now->modify('+5 minutes')); 48 | 49 | return $customiseBuilder($builder, $now)->getToken($signer, $signingKey); 50 | } 51 | 52 | /** @param non-empty-string $jwt */ 53 | public function parse( 54 | string $jwt, 55 | SignedWith $signedWith, 56 | ValidAt $validAt, 57 | Constraint ...$constraints, 58 | ): UnencryptedToken { 59 | $token = $this->parser->parse($jwt); 60 | assert($token instanceof UnencryptedToken); 61 | 62 | (new Validator())->assert( 63 | $token, 64 | $signedWith, 65 | $validAt, 66 | ...$constraints, 67 | ); 68 | 69 | return $token; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | contents()); 24 | 25 | if ($actualKeyLength < self::MINIMUM_KEY_LENGTH_IN_BITS) { 26 | throw InvalidKeyProvided::tooShort(self::MINIMUM_KEY_LENGTH_IN_BITS, $actualKeyLength); 27 | } 28 | 29 | return sodium_crypto_generichash($payload, $key->contents()); 30 | } 31 | 32 | public function verify(string $expected, string $payload, Key $key): bool 33 | { 34 | return hash_equals($expected, $this->sign($payload, $key)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Signer/CannotSignPayload.php: -------------------------------------------------------------------------------- 1 | converter->fromAsn1( 21 | $this->createSignature($key, $payload), 22 | $this->pointLength(), 23 | ); 24 | } 25 | 26 | final public function verify(string $expected, string $payload, Key $key): bool 27 | { 28 | return $this->verifySignature( 29 | $this->converter->toAsn1($expected, $this->pointLength()), 30 | $payload, 31 | $key, 32 | ); 33 | } 34 | 35 | /** {@inheritDoc} */ 36 | final protected function guardAgainstIncompatibleKey(int $type, int $lengthInBits): void 37 | { 38 | if ($type !== OPENSSL_KEYTYPE_EC) { 39 | throw InvalidKeyProvided::incompatibleKeyType( 40 | self::KEY_TYPE_MAP[OPENSSL_KEYTYPE_EC], 41 | self::KEY_TYPE_MAP[$type], 42 | ); 43 | } 44 | 45 | $expectedKeyLength = $this->expectedKeyLength(); 46 | 47 | if ($lengthInBits !== $expectedKeyLength) { 48 | throw InvalidKeyProvided::incompatibleKeyLength($expectedKeyLength, $lengthInBits); 49 | } 50 | } 51 | 52 | /** 53 | * @internal 54 | * 55 | * @return positive-int 56 | */ 57 | abstract public function expectedKeyLength(): int; 58 | 59 | /** 60 | * Returns the length of each point in the signature, so that we can calculate and verify R and S points properly 61 | * 62 | * @internal 63 | * 64 | * @return positive-int 65 | */ 66 | abstract public function pointLength(): int; 67 | } 68 | -------------------------------------------------------------------------------- /src/Signer/Ecdsa/ConversionFailed.php: -------------------------------------------------------------------------------- 1 | self::ASN1_MAX_SINGLE_BYTE ? self::ASN1_LENGTH_2BYTES : ''; 60 | 61 | $asn1 = hex2bin( 62 | self::ASN1_SEQUENCE 63 | . $lengthPrefix . dechex($totalLength) 64 | . self::ASN1_INTEGER . dechex($lengthR) . $pointR 65 | . self::ASN1_INTEGER . dechex($lengthS) . $pointS, 66 | ); 67 | assert(is_string($asn1)); 68 | assert($asn1 !== ''); 69 | 70 | return $asn1; 71 | } 72 | 73 | private static function octetLength(string $data): int 74 | { 75 | return (int) (strlen($data) / self::BYTE_SIZE); 76 | } 77 | 78 | private static function preparePositiveInteger(string $data): string 79 | { 80 | if (substr($data, 0, self::BYTE_SIZE) > self::ASN1_BIG_INTEGER_LIMIT) { 81 | return self::ASN1_NEGATIVE_INTEGER . $data; 82 | } 83 | 84 | while ( 85 | substr($data, 0, self::BYTE_SIZE) === self::ASN1_NEGATIVE_INTEGER 86 | && substr($data, 2, self::BYTE_SIZE) <= self::ASN1_BIG_INTEGER_LIMIT 87 | ) { 88 | $data = substr($data, 2, null); 89 | } 90 | 91 | return $data; 92 | } 93 | 94 | public function fromAsn1(string $signature, int $length): string 95 | { 96 | $message = bin2hex($signature); 97 | $position = 0; 98 | 99 | if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_SEQUENCE) { 100 | throw ConversionFailed::incorrectStartSequence(); 101 | } 102 | 103 | // @phpstan-ignore-next-line 104 | if (self::readAsn1Content($message, $position, self::BYTE_SIZE) === self::ASN1_LENGTH_2BYTES) { 105 | $position += self::BYTE_SIZE; 106 | } 107 | 108 | $pointR = self::retrievePositiveInteger(self::readAsn1Integer($message, $position)); 109 | $pointS = self::retrievePositiveInteger(self::readAsn1Integer($message, $position)); 110 | 111 | $points = hex2bin(str_pad($pointR, $length, '0', STR_PAD_LEFT) . str_pad($pointS, $length, '0', STR_PAD_LEFT)); 112 | assert(is_string($points)); 113 | assert($points !== ''); 114 | 115 | return $points; 116 | } 117 | 118 | private static function readAsn1Content(string $message, int &$position, int $length): string 119 | { 120 | $content = substr($message, $position, $length); 121 | $position += $length; 122 | 123 | return $content; 124 | } 125 | 126 | private static function readAsn1Integer(string $message, int &$position): string 127 | { 128 | if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_INTEGER) { 129 | throw ConversionFailed::integerExpected(); 130 | } 131 | 132 | $length = (int) hexdec(self::readAsn1Content($message, $position, self::BYTE_SIZE)); 133 | 134 | return self::readAsn1Content($message, $position, $length * self::BYTE_SIZE); 135 | } 136 | 137 | private static function retrievePositiveInteger(string $data): string 138 | { 139 | while ( 140 | substr($data, 0, self::BYTE_SIZE) === self::ASN1_NEGATIVE_INTEGER 141 | && substr($data, 2, self::BYTE_SIZE) > self::ASN1_BIG_INTEGER_LIMIT 142 | ) { 143 | $data = substr($data, 2, null); 144 | } 145 | 146 | return $data; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Signer/Ecdsa/Sha256.php: -------------------------------------------------------------------------------- 1 | contents()); 23 | } catch (SodiumException $sodiumException) { 24 | throw new InvalidKeyProvided($sodiumException->getMessage(), 0, $sodiumException); 25 | } 26 | } 27 | 28 | public function verify(string $expected, string $payload, Key $key): bool 29 | { 30 | try { 31 | return sodium_crypto_sign_verify_detached($expected, $payload, $key->contents()); 32 | } catch (SodiumException $sodiumException) { 33 | throw new InvalidKeyProvided($sodiumException->getMessage(), 0, $sodiumException); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Signer/Hmac.php: -------------------------------------------------------------------------------- 1 | contents()); 17 | $expectedKeyLength = $this->minimumBitsLengthForKey(); 18 | 19 | if ($actualKeyLength < $expectedKeyLength) { 20 | throw InvalidKeyProvided::tooShort($expectedKeyLength, $actualKeyLength); 21 | } 22 | 23 | return hash_hmac($this->algorithm(), $payload, $key->contents(), true); 24 | } 25 | 26 | final public function verify(string $expected, string $payload, Key $key): bool 27 | { 28 | return hash_equals($expected, $this->sign($payload, $key)); 29 | } 30 | 31 | /** 32 | * @internal 33 | * 34 | * @return non-empty-string 35 | */ 36 | abstract public function algorithm(): string; 37 | 38 | /** 39 | * @internal 40 | * 41 | * @return positive-int 42 | */ 43 | abstract public function minimumBitsLengthForKey(): int; 44 | } 45 | -------------------------------------------------------------------------------- /src/Signer/Hmac/Sha256.php: -------------------------------------------------------------------------------- 1 | getSize(); 73 | $contents = $fileSize > 0 ? $file->fread($file->getSize()) : ''; 74 | assert(is_string($contents)); 75 | 76 | self::guardAgainstEmptyKey($contents); 77 | 78 | return new self($contents, $passphrase); 79 | } 80 | 81 | /** @phpstan-assert non-empty-string $contents */ 82 | private static function guardAgainstEmptyKey(string $contents): void 83 | { 84 | if ($contents === '') { 85 | throw InvalidKeyProvided::cannotBeEmpty(); 86 | } 87 | } 88 | 89 | public function contents(): string 90 | { 91 | return $this->contents; 92 | } 93 | 94 | public function passphrase(): string 95 | { 96 | return $this->passphrase; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Signer/OpenSSL.php: -------------------------------------------------------------------------------- 1 | 'RSA', 31 | OPENSSL_KEYTYPE_DSA => 'DSA', 32 | OPENSSL_KEYTYPE_DH => 'DH', 33 | OPENSSL_KEYTYPE_EC => 'EC', 34 | ]; 35 | 36 | /** 37 | * @return non-empty-string 38 | * 39 | * @throws CannotSignPayload 40 | * @throws InvalidKeyProvided 41 | */ 42 | final protected function createSignature( 43 | Key $key, 44 | string $payload, 45 | ): string { 46 | $opensslKey = $this->getPrivateKey($key); 47 | 48 | $signature = ''; 49 | 50 | if (! openssl_sign($payload, $signature, $opensslKey, $this->algorithm())) { 51 | throw CannotSignPayload::errorHappened($this->fullOpenSSLErrorString()); 52 | } 53 | 54 | return $signature; 55 | } 56 | 57 | /** @throws CannotSignPayload */ 58 | private function getPrivateKey( 59 | Key $key, 60 | ): OpenSSLAsymmetricKey { 61 | return $this->validateKey(openssl_pkey_get_private($key->contents(), $key->passphrase())); 62 | } 63 | 64 | /** @throws InvalidKeyProvided */ 65 | final protected function verifySignature( 66 | string $expected, 67 | string $payload, 68 | Key $key, 69 | ): bool { 70 | $opensslKey = $this->getPublicKey($key); 71 | $result = openssl_verify($payload, $expected, $opensslKey, $this->algorithm()); 72 | 73 | return $result === 1; 74 | } 75 | 76 | /** @throws InvalidKeyProvided */ 77 | private function getPublicKey(Key $key): OpenSSLAsymmetricKey 78 | { 79 | return $this->validateKey(openssl_pkey_get_public($key->contents())); 80 | } 81 | 82 | /** 83 | * Raises an exception when the key type is not the expected type 84 | * 85 | * @throws InvalidKeyProvided 86 | */ 87 | private function validateKey(OpenSSLAsymmetricKey|bool $key): OpenSSLAsymmetricKey 88 | { 89 | if (is_bool($key)) { 90 | throw InvalidKeyProvided::cannotBeParsed($this->fullOpenSSLErrorString()); 91 | } 92 | 93 | $details = openssl_pkey_get_details($key); 94 | assert(is_array($details)); 95 | 96 | assert(array_key_exists('bits', $details)); 97 | assert(is_int($details['bits'])); 98 | assert(array_key_exists('type', $details)); 99 | assert(is_int($details['type'])); 100 | 101 | $this->guardAgainstIncompatibleKey($details['type'], $details['bits']); 102 | 103 | return $key; 104 | } 105 | 106 | private function fullOpenSSLErrorString(): string 107 | { 108 | $error = ''; 109 | 110 | while ($msg = openssl_error_string()) { 111 | $error .= PHP_EOL . '* ' . $msg; 112 | } 113 | 114 | return $error; 115 | } 116 | 117 | /** @throws InvalidKeyProvided */ 118 | abstract protected function guardAgainstIncompatibleKey(int $type, int $lengthInBits): void; 119 | 120 | /** 121 | * Returns which algorithm to be used to create/verify the signature (using OpenSSL constants) 122 | * 123 | * @internal 124 | */ 125 | abstract public function algorithm(): int; 126 | } 127 | -------------------------------------------------------------------------------- /src/Signer/Rsa.php: -------------------------------------------------------------------------------- 1 | createSignature($key, $payload); 15 | } 16 | 17 | final public function verify(string $expected, string $payload, Key $key): bool 18 | { 19 | return $this->verifySignature($expected, $payload, $key); 20 | } 21 | 22 | final protected function guardAgainstIncompatibleKey(int $type, int $lengthInBits): void 23 | { 24 | if ($type !== OPENSSL_KEYTYPE_RSA) { 25 | throw InvalidKeyProvided::incompatibleKeyType( 26 | self::KEY_TYPE_MAP[OPENSSL_KEYTYPE_RSA], 27 | self::KEY_TYPE_MAP[$type], 28 | ); 29 | } 30 | 31 | if ($lengthInBits < self::MINIMUM_KEY_LENGTH) { 32 | throw InvalidKeyProvided::tooShort(self::MINIMUM_KEY_LENGTH, $lengthInBits); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Signer/Rsa/Sha256.php: -------------------------------------------------------------------------------- 1 | $headers 24 | * @param array $claims 25 | */ 26 | private function __construct( 27 | private Encoder $encoder, 28 | private ClaimsFormatter $claimFormatter, 29 | private array $headers = ['typ' => 'JWT', 'alg' => null], 30 | private array $claims = [], 31 | ) { 32 | } 33 | 34 | public static function new(Encoder $encoder, ClaimsFormatter $claimFormatter): self 35 | { 36 | return new self($encoder, $claimFormatter); 37 | } 38 | 39 | public function permittedFor(string ...$audiences): BuilderInterface 40 | { 41 | $configured = $this->claims[RegisteredClaims::AUDIENCE] ?? []; 42 | $toAppend = array_diff($audiences, $configured); 43 | 44 | return $this->newWithClaim(RegisteredClaims::AUDIENCE, array_merge($configured, $toAppend)); 45 | } 46 | 47 | public function expiresAt(DateTimeImmutable $expiration): BuilderInterface 48 | { 49 | return $this->newWithClaim(RegisteredClaims::EXPIRATION_TIME, $expiration); 50 | } 51 | 52 | public function identifiedBy(string $id): BuilderInterface 53 | { 54 | return $this->newWithClaim(RegisteredClaims::ID, $id); 55 | } 56 | 57 | public function issuedAt(DateTimeImmutable $issuedAt): BuilderInterface 58 | { 59 | return $this->newWithClaim(RegisteredClaims::ISSUED_AT, $issuedAt); 60 | } 61 | 62 | public function issuedBy(string $issuer): BuilderInterface 63 | { 64 | return $this->newWithClaim(RegisteredClaims::ISSUER, $issuer); 65 | } 66 | 67 | public function canOnlyBeUsedAfter(DateTimeImmutable $notBefore): BuilderInterface 68 | { 69 | return $this->newWithClaim(RegisteredClaims::NOT_BEFORE, $notBefore); 70 | } 71 | 72 | public function relatedTo(string $subject): BuilderInterface 73 | { 74 | return $this->newWithClaim(RegisteredClaims::SUBJECT, $subject); 75 | } 76 | 77 | public function withHeader(string $name, mixed $value): BuilderInterface 78 | { 79 | $headers = $this->headers; 80 | $headers[$name] = $value; 81 | 82 | return new self( 83 | $this->encoder, 84 | $this->claimFormatter, 85 | $headers, 86 | $this->claims, 87 | ); 88 | } 89 | 90 | public function withClaim(string $name, mixed $value): BuilderInterface 91 | { 92 | if (in_array($name, RegisteredClaims::ALL, true)) { 93 | throw RegisteredClaimGiven::forClaim($name); 94 | } 95 | 96 | return $this->newWithClaim($name, $value); 97 | } 98 | 99 | /** @param non-empty-string $name */ 100 | private function newWithClaim(string $name, mixed $value): BuilderInterface 101 | { 102 | $claims = $this->claims; 103 | $claims[$name] = $value; 104 | 105 | return new self( 106 | $this->encoder, 107 | $this->claimFormatter, 108 | $this->headers, 109 | $claims, 110 | ); 111 | } 112 | 113 | /** 114 | * @param array $items 115 | * 116 | * @throws CannotEncodeContent When data cannot be converted to JSON. 117 | */ 118 | private function encode(array $items): string 119 | { 120 | return $this->encoder->base64UrlEncode( 121 | $this->encoder->jsonEncode($items), 122 | ); 123 | } 124 | 125 | public function getToken(Signer $signer, Key $key): UnencryptedToken 126 | { 127 | $headers = $this->headers; 128 | $headers['alg'] = $signer->algorithmId(); 129 | 130 | $encodedHeaders = $this->encode($headers); 131 | $encodedClaims = $this->encode($this->claimFormatter->formatClaims($this->claims)); 132 | 133 | $signature = $signer->sign($encodedHeaders . '.' . $encodedClaims, $key); 134 | $encodedSignature = $this->encoder->base64UrlEncode($signature); 135 | 136 | return new Plain( 137 | new DataSet($headers, $encodedHeaders), 138 | new DataSet($this->claims, $encodedClaims), 139 | new Signature($signature, $encodedSignature), 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Token/DataSet.php: -------------------------------------------------------------------------------- 1 | $data */ 11 | public function __construct(private array $data, private string $encoded) 12 | { 13 | } 14 | 15 | /** @param non-empty-string $name */ 16 | public function get(string $name, mixed $default = null): mixed 17 | { 18 | return $this->data[$name] ?? $default; 19 | } 20 | 21 | /** @param non-empty-string $name */ 22 | public function has(string $name): bool 23 | { 24 | return array_key_exists($name, $this->data); 25 | } 26 | 27 | /** @return array */ 28 | public function all(): array 29 | { 30 | return $this->data; 31 | } 32 | 33 | public function toString(): string 34 | { 35 | return $this->encoded; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Token/InvalidTokenStructure.php: -------------------------------------------------------------------------------- 1 | splitJwt($jwt); 29 | 30 | if ($encodedHeaders === '') { 31 | throw InvalidTokenStructure::missingHeaderPart(); 32 | } 33 | 34 | if ($encodedClaims === '') { 35 | throw InvalidTokenStructure::missingClaimsPart(); 36 | } 37 | 38 | if ($encodedSignature === '') { 39 | throw InvalidTokenStructure::missingSignaturePart(); 40 | } 41 | 42 | $header = $this->parseHeader($encodedHeaders); 43 | 44 | return new Plain( 45 | new DataSet($header, $encodedHeaders), 46 | new DataSet($this->parseClaims($encodedClaims), $encodedClaims), 47 | $this->parseSignature($encodedSignature), 48 | ); 49 | } 50 | 51 | /** 52 | * Splits the JWT string into an array 53 | * 54 | * @param non-empty-string $jwt 55 | * 56 | * @return string[] 57 | * 58 | * @throws InvalidTokenStructure When JWT doesn't have all parts. 59 | */ 60 | private function splitJwt(string $jwt): array 61 | { 62 | $data = explode('.', $jwt); 63 | 64 | if (count($data) !== 3) { 65 | throw InvalidTokenStructure::missingOrNotEnoughSeparators(); 66 | } 67 | 68 | return $data; 69 | } 70 | 71 | /** 72 | * Parses the header from a string 73 | * 74 | * @param non-empty-string $data 75 | * 76 | * @return array 77 | * 78 | * @throws UnsupportedHeaderFound When an invalid header is informed. 79 | * @throws InvalidTokenStructure When parsed content isn't an array. 80 | */ 81 | private function parseHeader(string $data): array 82 | { 83 | $header = $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); 84 | 85 | if (! is_array($header)) { 86 | throw InvalidTokenStructure::arrayExpected('headers'); 87 | } 88 | 89 | $this->guardAgainstEmptyStringKeys($header, 'headers'); 90 | 91 | if (array_key_exists('enc', $header)) { 92 | throw UnsupportedHeaderFound::encryption(); 93 | } 94 | 95 | if (! array_key_exists('typ', $header)) { 96 | $header['typ'] = 'JWT'; 97 | } 98 | 99 | return $header; 100 | } 101 | 102 | /** 103 | * Parses the claim set from a string 104 | * 105 | * @param non-empty-string $data 106 | * 107 | * @return array 108 | * 109 | * @throws InvalidTokenStructure When parsed content isn't an array or contains non-parseable dates. 110 | */ 111 | private function parseClaims(string $data): array 112 | { 113 | $claims = $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); 114 | 115 | if (! is_array($claims)) { 116 | throw InvalidTokenStructure::arrayExpected('claims'); 117 | } 118 | 119 | $this->guardAgainstEmptyStringKeys($claims, 'claims'); 120 | 121 | if (array_key_exists(RegisteredClaims::AUDIENCE, $claims)) { 122 | $claims[RegisteredClaims::AUDIENCE] = (array) $claims[RegisteredClaims::AUDIENCE]; 123 | } 124 | 125 | foreach (RegisteredClaims::DATE_CLAIMS as $claim) { 126 | if (! array_key_exists($claim, $claims)) { 127 | continue; 128 | } 129 | 130 | $claims[$claim] = $this->convertDate($claims[$claim]); 131 | } 132 | 133 | return $claims; 134 | } 135 | 136 | /** 137 | * @param array $array 138 | * @param non-empty-string $part 139 | * 140 | * @phpstan-assert array $array 141 | */ 142 | private function guardAgainstEmptyStringKeys(array $array, string $part): void 143 | { 144 | foreach ($array as $key => $value) { 145 | if ($key === '') { 146 | throw InvalidTokenStructure::arrayExpected($part); 147 | } 148 | } 149 | } 150 | 151 | /** @throws InvalidTokenStructure */ 152 | private function convertDate(int|float|string $timestamp): DateTimeImmutable 153 | { 154 | if (! is_numeric($timestamp)) { 155 | throw InvalidTokenStructure::dateIsNotParseable($timestamp); 156 | } 157 | 158 | $normalizedTimestamp = number_format((float) $timestamp, self::MICROSECOND_PRECISION, '.', ''); 159 | 160 | $date = DateTimeImmutable::createFromFormat('U.u', $normalizedTimestamp); 161 | 162 | if ($date === false) { 163 | throw InvalidTokenStructure::dateIsNotParseable($normalizedTimestamp); 164 | } 165 | 166 | return $date; 167 | } 168 | 169 | /** 170 | * Returns the signature from given data 171 | * 172 | * @param non-empty-string $data 173 | */ 174 | private function parseSignature(string $data): Signature 175 | { 176 | $hash = $this->decoder->base64UrlDecode($data); 177 | 178 | return new Signature($hash, $data); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Token/Plain.php: -------------------------------------------------------------------------------- 1 | headers; 23 | } 24 | 25 | public function claims(): DataSet 26 | { 27 | return $this->claims; 28 | } 29 | 30 | public function signature(): Signature 31 | { 32 | return $this->signature; 33 | } 34 | 35 | public function payload(): string 36 | { 37 | return $this->headers->toString() . '.' . $this->claims->toString(); 38 | } 39 | 40 | public function isPermittedFor(string $audience): bool 41 | { 42 | return in_array($audience, $this->claims->get(RegisteredClaims::AUDIENCE, []), true); 43 | } 44 | 45 | public function isIdentifiedBy(string $id): bool 46 | { 47 | return $this->claims->get(RegisteredClaims::ID) === $id; 48 | } 49 | 50 | public function isRelatedTo(string $subject): bool 51 | { 52 | return $this->claims->get(RegisteredClaims::SUBJECT) === $subject; 53 | } 54 | 55 | public function hasBeenIssuedBy(string ...$issuers): bool 56 | { 57 | return in_array($this->claims->get(RegisteredClaims::ISSUER), $issuers, true); 58 | } 59 | 60 | public function hasBeenIssuedBefore(DateTimeInterface $now): bool 61 | { 62 | return $now >= $this->claims->get(RegisteredClaims::ISSUED_AT); 63 | } 64 | 65 | public function isMinimumTimeBefore(DateTimeInterface $now): bool 66 | { 67 | return $now >= $this->claims->get(RegisteredClaims::NOT_BEFORE); 68 | } 69 | 70 | public function isExpired(DateTimeInterface $now): bool 71 | { 72 | if (! $this->claims->has(RegisteredClaims::EXPIRATION_TIME)) { 73 | return false; 74 | } 75 | 76 | return $now >= $this->claims->get(RegisteredClaims::EXPIRATION_TIME); 77 | } 78 | 79 | public function toString(): string 80 | { 81 | return $this->headers->toString() . '.' 82 | . $this->claims->toString() . '.' 83 | . $this->signature->toString(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Token/RegisteredClaimGiven.php: -------------------------------------------------------------------------------- 1 | hash; 20 | } 21 | 22 | /** 23 | * Returns the encoded version of the signature 24 | * 25 | * @return non-empty-string 26 | */ 27 | public function toString(): string 28 | { 29 | return $this->encoded; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Token/UnsupportedHeaderFound.php: -------------------------------------------------------------------------------- 1 | claims(); 30 | 31 | if (! $claims->has($this->claim)) { 32 | throw ConstraintViolation::error('The token does not have the claim "' . $this->claim . '"', $this); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Validation/Constraint/HasClaimWithValue.php: -------------------------------------------------------------------------------- 1 | claims(); 30 | 31 | if (! $claims->has($this->claim)) { 32 | throw ConstraintViolation::error('The token does not have the claim "' . $this->claim . '"', $this); 33 | } 34 | 35 | if ($claims->get($this->claim) !== $this->expectedValue) { 36 | throw ConstraintViolation::error( 37 | 'The claim "' . $this->claim . '" does not have the expected value', 38 | $this, 39 | ); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Validation/Constraint/IdentifiedBy.php: -------------------------------------------------------------------------------- 1 | isIdentifiedBy($this->id)) { 20 | throw ConstraintViolation::error( 21 | 'The token is not identified with the expected ID', 22 | $this, 23 | ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Validation/Constraint/IssuedBy.php: -------------------------------------------------------------------------------- 1 | issuers = $issuers; 19 | } 20 | 21 | public function assert(Token $token): void 22 | { 23 | if (! $token->hasBeenIssuedBy(...$this->issuers)) { 24 | throw ConstraintViolation::error( 25 | 'The token was not issued by the given issuers', 26 | $this, 27 | ); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Validation/Constraint/LeewayCannotBeNegative.php: -------------------------------------------------------------------------------- 1 | leeway = $this->guardLeeway($leeway); 20 | } 21 | 22 | private function guardLeeway(?DateInterval $leeway): DateInterval 23 | { 24 | if ($leeway === null) { 25 | return new DateInterval('PT0S'); 26 | } 27 | 28 | if ($leeway->invert === 1) { 29 | throw LeewayCannotBeNegative::create(); 30 | } 31 | 32 | return $leeway; 33 | } 34 | 35 | public function assert(Token $token): void 36 | { 37 | $now = $this->clock->now(); 38 | 39 | $this->assertIssueTime($token, $now->add($this->leeway)); 40 | $this->assertMinimumTime($token, $now->add($this->leeway)); 41 | $this->assertExpiration($token, $now->sub($this->leeway)); 42 | } 43 | 44 | /** @throws ConstraintViolation */ 45 | private function assertExpiration(Token $token, DateTimeInterface $now): void 46 | { 47 | if ($token->isExpired($now)) { 48 | throw ConstraintViolation::error('The token is expired', $this); 49 | } 50 | } 51 | 52 | /** @throws ConstraintViolation */ 53 | private function assertMinimumTime(Token $token, DateTimeInterface $now): void 54 | { 55 | if (! $token->isMinimumTimeBefore($now)) { 56 | throw ConstraintViolation::error('The token cannot be used yet', $this); 57 | } 58 | } 59 | 60 | /** @throws ConstraintViolation */ 61 | private function assertIssueTime(Token $token, DateTimeInterface $now): void 62 | { 63 | if (! $token->hasBeenIssuedBefore($now)) { 64 | throw ConstraintViolation::error('The token was issued in the future', $this); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Validation/Constraint/PermittedFor.php: -------------------------------------------------------------------------------- 1 | isPermittedFor($this->audience)) { 20 | throw ConstraintViolation::error( 21 | 'The token is not allowed to be used by this audience', 22 | $this, 23 | ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Validation/Constraint/RelatedTo.php: -------------------------------------------------------------------------------- 1 | isRelatedTo($this->subject)) { 20 | throw ConstraintViolation::error( 21 | 'The token is not related to the expected subject', 22 | $this, 23 | ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Validation/Constraint/SignedWith.php: -------------------------------------------------------------------------------- 1 | headers()->get('alg') !== $this->signer->algorithmId()) { 25 | throw ConstraintViolation::error('Token signer mismatch', $this); 26 | } 27 | 28 | if (! $this->signer->verify($token->signature()->hash(), $token->payload(), $this->key)) { 29 | throw ConstraintViolation::error('Token signature mismatch', $this); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Validation/Constraint/SignedWithOneInSet.php: -------------------------------------------------------------------------------- 1 | */ 15 | private array $constraints; 16 | 17 | public function __construct(SignedWithUntilDate ...$constraints) 18 | { 19 | $this->constraints = $constraints; 20 | } 21 | 22 | public function assert(Token $token): void 23 | { 24 | $errorMessage = 'It was not possible to verify the signature of the token, reasons:'; 25 | 26 | foreach ($this->constraints as $constraint) { 27 | try { 28 | $constraint->assert($token); 29 | 30 | return; 31 | } catch (ConstraintViolation $violation) { 32 | $errorMessage .= PHP_EOL . '- ' . $violation->getMessage(); 33 | } 34 | } 35 | 36 | throw ConstraintViolation::error($errorMessage, $this); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Validation/Constraint/SignedWithUntilDate.php: -------------------------------------------------------------------------------- 1 | verifySignature = new SignedWith($signer, $key); 26 | 27 | $this->clock = $clock ?? new class () implements ClockInterface { 28 | public function now(): DateTimeImmutable 29 | { 30 | return new DateTimeImmutable(); 31 | } 32 | }; 33 | } 34 | 35 | public function assert(Token $token): void 36 | { 37 | if ($this->validUntil < $this->clock->now()) { 38 | throw ConstraintViolation::error( 39 | 'This constraint was only usable until ' 40 | . $this->validUntil->format(DateTimeInterface::RFC3339), 41 | $this, 42 | ); 43 | } 44 | 45 | $this->verifySignature->assert($token); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Validation/Constraint/StrictValidAt.php: -------------------------------------------------------------------------------- 1 | leeway = $this->guardLeeway($leeway); 21 | } 22 | 23 | private function guardLeeway(?DateInterval $leeway): DateInterval 24 | { 25 | if ($leeway === null) { 26 | return new DateInterval('PT0S'); 27 | } 28 | 29 | if ($leeway->invert === 1) { 30 | throw LeewayCannotBeNegative::create(); 31 | } 32 | 33 | return $leeway; 34 | } 35 | 36 | public function assert(Token $token): void 37 | { 38 | if (! $token instanceof UnencryptedToken) { 39 | throw ConstraintViolation::error('You should pass a plain token', $this); 40 | } 41 | 42 | $now = $this->clock->now(); 43 | 44 | $this->assertIssueTime($token, $now->add($this->leeway)); 45 | $this->assertMinimumTime($token, $now->add($this->leeway)); 46 | $this->assertExpiration($token, $now->sub($this->leeway)); 47 | } 48 | 49 | /** @throws ConstraintViolation */ 50 | private function assertExpiration(UnencryptedToken $token, DateTimeInterface $now): void 51 | { 52 | if (! $token->claims()->has(Token\RegisteredClaims::EXPIRATION_TIME)) { 53 | throw ConstraintViolation::error('"Expiration Time" claim missing', $this); 54 | } 55 | 56 | if ($token->isExpired($now)) { 57 | throw ConstraintViolation::error('The token is expired', $this); 58 | } 59 | } 60 | 61 | /** @throws ConstraintViolation */ 62 | private function assertMinimumTime(UnencryptedToken $token, DateTimeInterface $now): void 63 | { 64 | if (! $token->claims()->has(Token\RegisteredClaims::NOT_BEFORE)) { 65 | throw ConstraintViolation::error('"Not Before" claim missing', $this); 66 | } 67 | 68 | if (! $token->isMinimumTimeBefore($now)) { 69 | throw ConstraintViolation::error('The token cannot be used yet', $this); 70 | } 71 | } 72 | 73 | /** @throws ConstraintViolation */ 74 | private function assertIssueTime(UnencryptedToken $token, DateTimeInterface $now): void 75 | { 76 | if (! $token->claims()->has(Token\RegisteredClaims::ISSUED_AT)) { 77 | throw ConstraintViolation::error('"Issued At" claim missing', $this); 78 | } 79 | 80 | if (! $token->hasBeenIssuedBefore($now)) { 81 | throw ConstraintViolation::error('The token was issued in the future', $this); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Validation/ConstraintViolation.php: -------------------------------------------------------------------------------- 1 | |null $constraint */ 12 | public function __construct( 13 | string $message = '', 14 | public readonly ?string $constraint = null, 15 | ) { 16 | parent::__construct($message); 17 | } 18 | 19 | /** @param non-empty-string $message */ 20 | public static function error(string $message, Constraint $constraint): self 21 | { 22 | return new self(message: $message, constraint: $constraint::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Validation/NoConstraintsGiven.php: -------------------------------------------------------------------------------- 1 | getMessage(); 33 | }, 34 | $violations, 35 | ); 36 | 37 | $message = "The token violates some mandatory constraints, details:\n"; 38 | $message .= implode("\n", $violations); 39 | 40 | return $message; 41 | } 42 | 43 | /** @return ConstraintViolation[] */ 44 | public function violations(): array 45 | { 46 | return $this->violations; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Validation/SignedWith.php: -------------------------------------------------------------------------------- 1 | checkConstraint($constraint, $token, $violations); 20 | } 21 | 22 | if ($violations !== []) { 23 | throw RequiredConstraintsViolated::fromViolations(...$violations); 24 | } 25 | } 26 | 27 | /** @param ConstraintViolation[] $violations */ 28 | private function checkConstraint( 29 | Constraint $constraint, 30 | Token $token, 31 | array &$violations, 32 | ): void { 33 | try { 34 | $constraint->assert($token); 35 | } catch (ConstraintViolation $e) { 36 | $violations[] = $e; 37 | } 38 | } 39 | 40 | public function validate(Token $token, Constraint ...$constraints): bool 41 | { 42 | if ($constraints === []) { 43 | throw new NoConstraintsGiven('No constraint given.'); 44 | } 45 | 46 | try { 47 | foreach ($constraints as $constraint) { 48 | $constraint->assert($token); 49 | } 50 | 51 | return true; 52 | } catch (ConstraintViolation) { 53 | return false; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 |