From 9101e3489a31075eccf65fad1873c1d675836095 Mon Sep 17 00:00:00 2001 From: Milad Rahimi Date: Sat, 2 Jun 2018 23:13:30 +0430 Subject: [PATCH] first commit --- .gitignore | 3 + LICENSE | 21 + README.md | 222 ++++++++ composer.json | 36 ++ src/MiladRahimi/Jwt/Base64/Base64Parser.php | 39 ++ .../Jwt/Base64/Base64ParserInterface.php | 28 + .../Jwt/Cryptography/AbstractVerifier.php | 50 ++ .../Algorithms/Hmac/AbstractHmac.php | 101 ++++ .../Cryptography/Algorithms/Hmac/HS256.php | 14 + .../Cryptography/Algorithms/Hmac/HS384.php | 14 + .../Cryptography/Algorithms/Hmac/HS512.php | 14 + .../Cryptography/Algorithms/Rsa/Naming.php | 39 ++ .../Rsa/Signers/AbstractRsaSigner.php | 61 +++ .../Algorithms/Rsa/Signers/RS256.php | 14 + .../Algorithms/Rsa/Signers/RS384.php | 14 + .../Algorithms/Rsa/Signers/RS512.php | 14 + .../Rsa/Verifiers/AbstractRsaVerifier.php | 67 +++ .../Algorithms/Rsa/Verifiers/RS256.php | 14 + .../Algorithms/Rsa/Verifiers/RS384.php | 14 + .../Algorithms/Rsa/Verifiers/RS512.php | 14 + .../Jwt/Cryptography/Keys/PrivateKey.php | 42 ++ .../Jwt/Cryptography/Keys/PublicKey.php | 42 ++ src/MiladRahimi/Jwt/Cryptography/Signer.php | 27 + src/MiladRahimi/Jwt/Cryptography/Verifier.php | 24 + .../Jwt/Enums/PublicClaimNames.php | 20 + .../Jwt/Exceptions/InvalidJsonException.php | 14 + .../Jwt/Exceptions/InvalidKeyException.php | 16 + .../Exceptions/InvalidSignatureException.php | 15 + .../Jwt/Exceptions/InvalidTokenException.php | 16 + .../Jwt/Exceptions/ValidationException.php | 15 + src/MiladRahimi/Jwt/Json/JsonParser.php | 47 ++ .../Jwt/Json/JsonParserInterface.php | 31 ++ src/MiladRahimi/Jwt/JwtGenerator.php | 125 +++++ src/MiladRahimi/Jwt/JwtParser.php | 225 ++++++++ .../Jwt/Validator/DefaultValidator.php | 26 + src/MiladRahimi/Jwt/Validator/Rule.php | 21 + .../Validator/Rules/Optional/ConsistsOf.php | 35 ++ .../Jwt/Validator/Rules/Optional/EqualsTo.php | 35 ++ .../Validator/Rules/Optional/GreaterThan.php | 35 ++ .../Rules/Optional/GreaterThanOrEqualTo.php | 35 ++ .../Validator/Rules/Optional/IdenticalTo.php | 35 ++ .../Jwt/Validator/Rules/Optional/LessThan.php | 35 ++ .../Rules/Optional/LessThanOrEqualTo.php | 35 ++ .../Validator/Rules/Optional/NewerThan.php | 20 + .../Optional/NewerThanOrSameTimeWith.php | 20 + .../Validator/Rules/Optional/OlderThan.php | 20 + .../Optional/OlderThanOrSameTimeWith.php | 20 + .../Validator/Rules/Required/ConsistsOf.php | 35 ++ .../Jwt/Validator/Rules/Required/EqualsTo.php | 35 ++ .../Jwt/Validator/Rules/Required/Exists.php | 22 + .../Validator/Rules/Required/GreaterThan.php | 35 ++ .../Rules/Required/GreaterThanOrEqualTo.php | 35 ++ .../Validator/Rules/Required/IdenticalTo.php | 35 ++ .../Jwt/Validator/Rules/Required/LessThan.php | 35 ++ .../Rules/Required/LessThanOrEqualTo.php | 35 ++ .../Validator/Rules/Required/NewerThan.php | 20 + .../Required/NewerThanOrSameTimeWith.php | 20 + .../Jwt/Validator/Rules/Required/NotNull.php | 22 + .../Validator/Rules/Required/OlderThan.php | 20 + .../Required/OlderThanOrSameTimeWith.php | 20 + src/MiladRahimi/Jwt/Validator/Validator.php | 56 ++ .../Jwt/Validator/ValidatorInterface.php | 37 ++ tests/Base64ParserTest.php | 40 ++ tests/HSTest.php | 79 +++ tests/JsonParserTest.php | 70 +++ tests/OptionalValidationRulesTest.php | 426 ++++++++++++++ tests/RSTest.php | 105 ++++ tests/RequiredValidationRulesTest.php | 518 ++++++++++++++++++ tests/TestCase.php | 14 + tests/ValidationTest.php | 107 ++++ tests/keys/private.pem | 28 + tests/keys/public.pem | 9 + 72 files changed, 3682 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 src/MiladRahimi/Jwt/Base64/Base64Parser.php create mode 100644 src/MiladRahimi/Jwt/Base64/Base64ParserInterface.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/AbstractVerifier.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/AbstractHmac.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS256.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS384.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS512.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Naming.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/AbstractRsaSigner.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS256.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS384.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS512.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/AbstractRsaVerifier.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS256.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS384.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS512.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Keys/PrivateKey.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Keys/PublicKey.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Signer.php create mode 100644 src/MiladRahimi/Jwt/Cryptography/Verifier.php create mode 100644 src/MiladRahimi/Jwt/Enums/PublicClaimNames.php create mode 100644 src/MiladRahimi/Jwt/Exceptions/InvalidJsonException.php create mode 100644 src/MiladRahimi/Jwt/Exceptions/InvalidKeyException.php create mode 100644 src/MiladRahimi/Jwt/Exceptions/InvalidSignatureException.php create mode 100644 src/MiladRahimi/Jwt/Exceptions/InvalidTokenException.php create mode 100644 src/MiladRahimi/Jwt/Exceptions/ValidationException.php create mode 100644 src/MiladRahimi/Jwt/Json/JsonParser.php create mode 100644 src/MiladRahimi/Jwt/Json/JsonParserInterface.php create mode 100644 src/MiladRahimi/Jwt/JwtGenerator.php create mode 100644 src/MiladRahimi/Jwt/JwtParser.php create mode 100644 src/MiladRahimi/Jwt/Validator/DefaultValidator.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rule.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Optional/ConsistsOf.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Optional/EqualsTo.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Optional/GreaterThan.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Optional/GreaterThanOrEqualTo.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Optional/IdenticalTo.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Optional/LessThan.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Optional/LessThanOrEqualTo.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Optional/NewerThan.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Optional/NewerThanOrSameTimeWith.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Optional/OlderThan.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Optional/OlderThanOrSameTimeWith.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/ConsistsOf.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/EqualsTo.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/Exists.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/GreaterThan.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/GreaterThanOrEqualTo.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/IdenticalTo.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/LessThan.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/LessThanOrEqualTo.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/NewerThan.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/NewerThanOrSameTimeWith.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/NotNull.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/OlderThan.php create mode 100644 src/MiladRahimi/Jwt/Validator/Rules/Required/OlderThanOrSameTimeWith.php create mode 100644 src/MiladRahimi/Jwt/Validator/Validator.php create mode 100644 src/MiladRahimi/Jwt/Validator/ValidatorInterface.php create mode 100644 tests/Base64ParserTest.php create mode 100644 tests/HSTest.php create mode 100644 tests/JsonParserTest.php create mode 100644 tests/OptionalValidationRulesTest.php create mode 100644 tests/RSTest.php create mode 100644 tests/RequiredValidationRulesTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/ValidationTest.php create mode 100644 tests/keys/private.pem create mode 100644 tests/keys/public.pem diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf6509b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/vendor +/composer.lock +/.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..326e2ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Milad Rahimi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9fb14e4 --- /dev/null +++ b/README.md @@ -0,0 +1,222 @@ +# PHP-JWT +A PHP implementation of JWT (JSON Web Token) generator, parser, verifier, and validator + +## Overview +PHP JWT is a package written in PHP programming language for encoding (generating), decoding (parsing), verifying +and validating JWTs (JSON Web Tokens). + +## Documentation + +### Installation +Add the package to your project composer packages with the following command: + +``` +composer require miladrahimi/larajwt:0.2.* +``` + +Now, you are ready to use the package! + +### What is JWT +If you are unfamiliar with JWT you can read [Wikipedia](https://en.wikipedia.org/wiki/JSON_Web_Token) or +[JWT.io](https://jwt.io) website. + +### Getting Started +The following snippet demonstrates how to generate a JWT with the HS256 algorithm and a custom secret key. + +``` +use MiladRahimi\Jwt\Cryptography\Algorithms\Hmac\HS256; +use MiladRahimi\Jwt\JwtGenerator; +use MiladRahimi\Jwt\JwtParser; + +// ... + +$key = 'some random key'; +$signer = new HS256($key); + +$generator = new JwtGenerator($signer); + +$jwt = $generator->generate(['sub' => 1, 'jti' => 2]); + +$parser = new JwtParser($signer); + +$claims = $parser->parse($jwt); +``` + +### HMAC Algorithms +If you want to use a single key to both generate and parse tokens, +you should use HMAC algorithms (HS256, HS384, and HS512). +These algorithms use the same key to sign and verifying tokens. + +``` +use MiladRahimi\Jwt\Cryptography\Algorithms\Hmac\HS512; +use MiladRahimi\Jwt\JwtGenerator; +use MiladRahimi\Jwt\JwtParser; + +// ... + +$key = 'some random key'; + +$signer = $verifyer = new HS512($key); + +$generator = new JwtGenerator($signer); + +$jwt = $generator->generate(['sub' => 1, 'jti' => 2]); + +$parser = new JwtParser($verifyer); + +$claims = $parser->parse($jwt); +``` + +### RSA Algorithms +If you want to use asymmetric keys to generate and parse tokens, +you should use RSA algorithms (RS256, RS384, and RS512). +These algorithms use pair (public/private) keys and consequently, +different signer and verifier which the signer uses the private key and the verifier uses the public key. + +``` +use MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Signers\RS256 as RS256Signer; +use MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Verifiers\RS256 as RS256Verifier; +use MiladRahimi\Jwt\Cryptography\Keys\PrivateKey; +use MiladRahimi\Jwt\Cryptography\Keys\PublicKey; +use MiladRahimi\Jwt\JwtGenerator; +use MiladRahimi\Jwt\JwtParser; + +// ... + +$privateKey = new PrivateKey('keys/private.pem'); +$publicKey = new PublicKey('keys/public.pem'); + +$signer = new RS256Signer($privateKey); +$verifier = new RS256Verifier($publicKey); + +$generator = new JwtGenerator($signer); +$jwt = $generator->generate(['sub' => 1, 'jti' => 2]); + +$parser = new JwtParser($verifier); +$claims = $parser->parse($jwt); +``` + +You can read [this instruction](https://en.wikibooks.org/wiki/Cryptography/Generate_a_keypair_using_OpenSSL) +web page to learn how to generate a pair (public/private) key. + +### Token Generating +You can generate JWTs with `JwtGenerator::generate()` method. +This method needs a signer algorithm to sign tokens. +As you can see in preceding sections, you can use HMAC-based or RSA-based signers for generating tokens. +HMAC signers need a symmetric key. HMAC verifiers use the same key to verify the token, that's why we call it symmetric. +RSA signers need a private key to sign. +Both HMAC and RSA signers throw an `InvalidKeyException` exception if the provided key is not valid. + +``` +use MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Signers\RS256; +use MiladRahimi\Jwt\Cryptography\Keys\PrivateKey; +use MiladRahimi\Jwt\JwtGenerator; +use MiladRahimi\Jwt\Exceptions\InvalidKeyException; + +// ... + +try { + $privateKey = new PrivateKey('keys/private.pem'); +} catch(InvalidKeyException $e) { + // Your key is invalid. +} + +$signer = new RS256($privateKey); + +$generator = new JwtGenerator($signer); +$jwt = $generator->generate(['sub' => 1, 'jti' => 2]); +``` + +### Verification and Validation +Before extracting claims from tokens, the tokens must be verified and validated. +First, we verify the token by its signature so that we make sure that it is generated by the original issuer. +After signature verification, we must validate claims, +some public claims that should be validated are 'exp', 'iat', and 'nbf', of course, +you can add your custom claim validations. + +The `parse()` method verifies and validates claims automatically. +If you don't need to parse the token and only need to verify and validate it, you can use `validate()` method. +If you don't need to validate the token and only need to verify its signature you can use `verifySignature()` method. + +The `parse()` method verifies and validates claims automatically. +If you don't need to parse the token and only need to verify and validate it, you can use `validate()` method. +If you don't need to validate the token and only need to verify its signature you can use `verifySignature()` method. +All methods throw `InvalidSignatureException` exception when the token signature is invalid. +The `validate()` and `parse()` method throw `ValidationException` when the token claims are invalid +and `InvalidJsonException` exception when cannot parse JSON, `InvalidTokenException` +when the token format is invalid (it's not three part). +All these exceptions are also children of `InvalidTokenException` so +if you don't care about the failure reason you can only catch this exception. + +``` +use MiladRahimi\Jwt\Cryptography\Algorithms\Hmac\HS512; +use MiladRahimi\Jwt\JwtGenerator; +use MiladRahimi\Jwt\JwtParser; +use MiladRahimi\Jwt\Exceptions\InvalidTokenException + +// ... + +$jwt = // read from header... + +$verifyer = new HS512('some random key'); + +$parser = new JwtParser($verifyer); + +try { + $parser->validate($jwt); + + // token is valid... +} catch (InvalidTokenException $e) { + // token is not valid... +} +``` + +### Custom Validation +The `JwtParser` use the default validator (`DefaultValidator`) to validate tokens in `parse()` and `validate()` methods. +This validator would care about `exp`, `iat` and `nbf` claims if they were in the payload. +You can create an instance of `DefaultValidator` or `Validator` (empty with no rule) and add your own rules like this: + +``` +use MiladRahimi\Jwt\Cryptography\Algorithms\Hmac\HS512; +use MiladRahimi\Jwt\JwtGenerator; +use MiladRahimi\Jwt\JwtParser; +use MiladRahimi\Jwt\Exceptions\InvalidTokenException +use MiladRahimi\Jwt\Validator\Rules\Required\Exists; +use MiladRahimi\Jwt\Validator\Rules\Required\ConsistsOf; +use MiladRahimi\Jwt\Validator\Rules\Required\NewerThan; + +// ... + +$jwt = // read from header... + +$verifyer = new HS512('some random key'); + +$validator = new DefaultValidator(); +$validator->addRule('iss', new Exists()); +$validator->addRule('aud', new ConsistsOf('Company')); +$validator->addRule('future-time', new NewerThan(time())); + +$parser = new JwtParser($verifyer, $validator); + +try { + $claims = $parser->parse($jwt); + + // token is valid... +} catch (InvalidTokenException $e) { + // token is not valid... +} +``` + +As you can see in the snippet above, you can instantiate a validator and add your own rules to it. +To add a new rule, you must pass the claim name you are setting rule for and the rule +which is an object from the rule classes. +there are two categories of rules named optional and required. +The optional rules would be checked only when the claim did exist but +the required rules would fail the validation if the claim did not exist. + +### Contribute + +Any contribution will be appreciated :D + +## License +This package is released under the [MIT License](http://opensource.org/licenses/mit-license.php). \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..cda4d52 --- /dev/null +++ b/composer.json @@ -0,0 +1,36 @@ +{ + "name": "miladrahimi/php-jwt", + "description": "A PHP implementation of JWT (JSON Web Token) generator, parser, verifier, and validator", + "keywords": [ + "JWT", + "JSON Web Token", + "Token", + "Parser", + "Encoder", + "Decoder", + "Verifier", + "Validator" + ], + "homepage": "https://github.com/miladrahimi/php-jwt", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Milad Rahimi", + "email": "info@miladrahimi.com" + } + ], + "require": { + "php": ">=7.0", + "ext-openssl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^6" + }, + "autoload": { + "psr-4": { + "MiladRahimi\\Jwt\\": "src/MiladRahimi/Jwt/", + "MiladRahimi\\Jwt\\Tests\\": "tests/" + } + } +} diff --git a/src/MiladRahimi/Jwt/Base64/Base64Parser.php b/src/MiladRahimi/Jwt/Base64/Base64Parser.php new file mode 100644 index 0000000..e5e0377 --- /dev/null +++ b/src/MiladRahimi/Jwt/Base64/Base64Parser.php @@ -0,0 +1,39 @@ + + * Date: 5/13/2018 AD + * Time: 23:47 + */ + +namespace MiladRahimi\Jwt\Base64; + +class Base64Parser implements Base64ParserInterface +{ + /** + * Encode data by Base64 algorithm + * + * @param string $data + * @return string + */ + public function encode(string $data): string + { + return str_replace('=', '', strtr(base64_encode($data), '+/', '-_')); + } + + /** + * Decode Base64-encoded data to plain text + * + * @param string $data + * @return string + */ + public function decode(string $data): string + { + if ($remainder = strlen($data) % 4) { + $paddingLength = 4 - $remainder; + $data .= str_repeat('=', $paddingLength); + } + + return base64_decode(strtr($data, '-_', '+/')); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Base64/Base64ParserInterface.php b/src/MiladRahimi/Jwt/Base64/Base64ParserInterface.php new file mode 100644 index 0000000..e7eddb0 --- /dev/null +++ b/src/MiladRahimi/Jwt/Base64/Base64ParserInterface.php @@ -0,0 +1,28 @@ + + * Date: 5/14/2018 AD + * Time: 00:39 + */ + +namespace MiladRahimi\Jwt\Base64; + +interface Base64ParserInterface +{ + /** + * Encode data by Base64 algorithm + * + * @param string $data + * @return string + */ + public function encode(string $data): string; + + /** + * Decode Base64-encoded data to plain text + * + * @param string $data + * @return string + */ + public function decode(string $data): string; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/AbstractVerifier.php b/src/MiladRahimi/Jwt/Cryptography/AbstractVerifier.php new file mode 100644 index 0000000..c51cbec --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/AbstractVerifier.php @@ -0,0 +1,50 @@ + + * Date: 5/15/2018 AD + * Time: 23:25 + */ + +namespace MiladRahimi\Jwt\Cryptography; + +use MiladRahimi\Jwt\Base64\Base64Parser; +use MiladRahimi\Jwt\Base64\Base64ParserInterface; + +abstract class AbstractVerifier implements Verifier +{ + /** + * @var Base64ParserInterface + */ + protected $base64Parser; + + /** + * AbstractAlgorithm constructor. + * + * @param Base64ParserInterface|null $base64Parser + */ + public function __construct(Base64ParserInterface $base64Parser = null) + { + if ($base64Parser) { + $this->setBase64Parser($base64Parser); + } else { + $this->setBase64Parser(new Base64Parser()); + } + } + + /** + * @return Base64ParserInterface|null + */ + public function getBase64Parser(): Base64ParserInterface + { + return $this->base64Parser; + } + + /** + * @param Base64ParserInterface $base64Parser + */ + public function setBase64Parser(Base64ParserInterface $base64Parser): void + { + $this->base64Parser = $base64Parser; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/AbstractHmac.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/AbstractHmac.php new file mode 100644 index 0000000..bca8329 --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/AbstractHmac.php @@ -0,0 +1,101 @@ + + * Date: 5/14/2018 AD + * Time: 00:18 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Hmac; + +use MiladRahimi\Jwt\Base64\Base64ParserInterface; +use MiladRahimi\Jwt\Cryptography\AbstractVerifier; +use MiladRahimi\Jwt\Cryptography\Signer; +use MiladRahimi\Jwt\Exceptions\InvalidKeyException; +use MiladRahimi\Jwt\Exceptions\InvalidSignatureException; + +abstract class AbstractHmac extends AbstractVerifier implements Signer +{ + /** + * @var string + */ + protected $name; + + /** + * @var string + */ + protected $key; + + /** + * HS constructor. + * + * @param string $key + * @param Base64ParserInterface|null $base64Parser + * @throws InvalidKeyException + */ + public function __construct(string $key, Base64ParserInterface $base64Parser = null) + { + parent::__construct($base64Parser); + + $this->setKey($key); + } + + /** + * @inheritdoc + */ + public function verify(string $header, string $payload, string $signature): void + { + $tokenSignature = $this->base64Parser->encode($this->sign($header . '.' . $payload)); + + if ($tokenSignature != $signature) { + throw new InvalidSignatureException(); + } + } + + /** + * @inheritdoc + */ + public function sign(string $data): string + { + return hash_hmac($this->algorithmName(), $data, $this->key, true); + } + + /** + * Convert JWT algorithm name to hash function name + * + * @return string + */ + protected function algorithmName(): string + { + return 'sha' . substr($this->name, 2); + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * @param string $key + * @throws InvalidKeyException + */ + public function setKey(string $key): void + { + if (strlen($key) < 32 || strlen($key) > 6144) { + throw new InvalidKeyException(); + } + + $this->key = $key; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS256.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS256.php new file mode 100644 index 0000000..70d69b8 --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS256.php @@ -0,0 +1,14 @@ + + * Date: 5/14/2018 AD + * Time: 00:17 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Hmac; + +class HS256 extends AbstractHmac +{ + protected $name = 'HS256'; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS384.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS384.php new file mode 100644 index 0000000..2d775be --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS384.php @@ -0,0 +1,14 @@ + + * Date: 5/14/2018 AD + * Time: 00:17 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Hmac; + +class HS384 extends AbstractHmac +{ + protected $name = 'HS384'; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS512.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS512.php new file mode 100644 index 0000000..2072c2e --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Hmac/HS512.php @@ -0,0 +1,14 @@ + + * Date: 5/14/2018 AD + * Time: 00:17 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Hmac; + +class HS512 extends AbstractHmac +{ + protected $name = 'HS512'; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Naming.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Naming.php new file mode 100644 index 0000000..54c5b5b --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Naming.php @@ -0,0 +1,39 @@ + + * Date: 6/1/2018 AD + * Time: 21:49 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Rsa; + +trait Naming +{ + /** + * @var string + */ + protected $name; + + /** + * @inheritdoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return int + */ + protected function algorithmName() + { + $table = [ + 'RS256' => OPENSSL_ALGO_SHA256, + 'RS384' => OPENSSL_ALGO_SHA384, + 'RS512' => OPENSSL_ALGO_SHA512, + ]; + + return $table[$this->name]; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/AbstractRsaSigner.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/AbstractRsaSigner.php new file mode 100644 index 0000000..d3b16c2 --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/AbstractRsaSigner.php @@ -0,0 +1,61 @@ + + * Date: 5/14/2018 AD + * Time: 22:23 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Signers; + +use MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Naming; +use MiladRahimi\Jwt\Cryptography\Keys\PrivateKey; +use MiladRahimi\Jwt\Cryptography\Signer; + +abstract class AbstractRsaSigner implements Signer +{ + use Naming; + + /** + * @var PrivateKey + */ + protected $privateKey; + + /** + * AbstractRsaSigner constructor. + * + * @param PrivateKey $publicKey + */ + public function __construct(PrivateKey $publicKey) + { + $this->setPrivateKey($publicKey); + } + + /** + * @inheritdoc + */ + public function sign(string $data): string + { + $signature = ''; + + openssl_sign($data, $signature, $this->privateKey->getResource(), $this->algorithmName()); + + return $signature; + } + + /** + * @return PrivateKey + */ + public function getPrivateKey(): PrivateKey + { + return $this->privateKey; + } + + /** + * @param PrivateKey $privateKey + */ + public function setPrivateKey(PrivateKey $privateKey) + { + $this->privateKey = $privateKey; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS256.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS256.php new file mode 100644 index 0000000..25a9430 --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS256.php @@ -0,0 +1,14 @@ + + * Date: 5/14/2018 AD + * Time: 22:24 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Signers; + +class RS256 extends AbstractRsaSigner +{ + protected $name = 'RS256'; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS384.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS384.php new file mode 100644 index 0000000..37b0aeb --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS384.php @@ -0,0 +1,14 @@ + + * Date: 5/14/2018 AD + * Time: 22:24 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Signers; + +class RS384 extends AbstractRsaSigner +{ + protected $name = 'RS384'; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS512.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS512.php new file mode 100644 index 0000000..f80ba00 --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Signers/RS512.php @@ -0,0 +1,14 @@ + + * Date: 5/14/2018 AD + * Time: 22:24 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Signers; + +class RS512 extends AbstractRsaSigner +{ + protected $name = 'RS512'; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/AbstractRsaVerifier.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/AbstractRsaVerifier.php new file mode 100644 index 0000000..d0e1d56 --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/AbstractRsaVerifier.php @@ -0,0 +1,67 @@ + + * Date: 5/14/2018 AD + * Time: 22:23 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Verifiers; + +use MiladRahimi\Jwt\Base64\Base64ParserInterface; +use MiladRahimi\Jwt\Cryptography\AbstractVerifier; +use MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Naming; +use MiladRahimi\Jwt\Cryptography\Keys\PublicKey; +use MiladRahimi\Jwt\Exceptions\InvalidSignatureException; + +abstract class AbstractRsaVerifier extends AbstractVerifier +{ + use Naming; + + /** + * @var PublicKey + */ + protected $publicKey; + + /** + * AbstractRsaVerifier constructor. + * + * @param PublicKey $key + * @param Base64ParserInterface|null $base64Parser + */ + public function __construct(PublicKey $key, Base64ParserInterface $base64Parser = null) + { + $this->setPublicKey($key); + + parent::__construct($base64Parser); + } + + /** + * @inheritdoc + */ + public function verify(string $header, string $payload, string $signature): void + { + $data = $header . '.' . $payload; + $signature = $this->base64Parser->decode($signature); + + if (openssl_verify($data, $signature, $this->publicKey->getResource(), $this->algorithmName()) == false) { + throw new InvalidSignatureException(); + } + } + + /** + * @return PublicKey + */ + public function getPublicKey(): PublicKey + { + return $this->publicKey; + } + + /** + * @param PublicKey $publicKey + */ + public function setPublicKey(PublicKey $publicKey) + { + $this->publicKey = $publicKey; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS256.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS256.php new file mode 100644 index 0000000..4175531 --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS256.php @@ -0,0 +1,14 @@ + + * Date: 5/14/2018 AD + * Time: 22:24 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Verifiers; + +class RS256 extends AbstractRsaVerifier +{ + protected $name = 'RS256'; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS384.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS384.php new file mode 100644 index 0000000..272ebed --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS384.php @@ -0,0 +1,14 @@ + + * Date: 5/14/2018 AD + * Time: 22:24 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Verifiers; + +class RS384 extends AbstractRsaVerifier +{ + protected $name = 'RS384'; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS512.php b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS512.php new file mode 100644 index 0000000..fa4c9a7 --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Algorithms/Rsa/Verifiers/RS512.php @@ -0,0 +1,14 @@ + + * Date: 5/14/2018 AD + * Time: 22:24 + */ + +namespace MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Verifiers; + +class RS512 extends AbstractRsaVerifier +{ + protected $name = 'RS512'; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Keys/PrivateKey.php b/src/MiladRahimi/Jwt/Cryptography/Keys/PrivateKey.php new file mode 100644 index 0000000..15e3c2b --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Keys/PrivateKey.php @@ -0,0 +1,42 @@ + + * Date: 6/1/2018 AD + * Time: 19:16 + */ + +namespace MiladRahimi\Jwt\Cryptography\Keys; + +use MiladRahimi\Jwt\Exceptions\InvalidKeyException; + +class PrivateKey +{ + /** + * @var resource + */ + private $resource; + + /** + * PrivateKey constructor. + * + * @param string $fileFullPath + * @throws InvalidKeyException + */ + public function __construct(string $fileFullPath) + { + $this->resource = openssl_pkey_get_private('file:///' . $fileFullPath); + + if (empty($this->resource)) { + throw new InvalidKeyException(); + } + } + + /** + * @return resource + */ + public function getResource() + { + return $this->resource; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Keys/PublicKey.php b/src/MiladRahimi/Jwt/Cryptography/Keys/PublicKey.php new file mode 100644 index 0000000..2eef9d5 --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Keys/PublicKey.php @@ -0,0 +1,42 @@ + + * Date: 6/1/2018 AD + * Time: 19:16 + */ + +namespace MiladRahimi\Jwt\Cryptography\Keys; + +use MiladRahimi\Jwt\Exceptions\InvalidKeyException; + +class PublicKey +{ + /** + * @var resource + */ + private $resource; + + /** + * PublicKey constructor. + * + * @param string $fileFullPath + * @throws InvalidKeyException + */ + public function __construct(string $fileFullPath) + { + $this->resource = openssl_pkey_get_public('file:///' . $fileFullPath); + + if (empty($this->resource)) { + throw new InvalidKeyException(); + } + } + + /** + * @return resource + */ + public function getResource() + { + return $this->resource; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Signer.php b/src/MiladRahimi/Jwt/Cryptography/Signer.php new file mode 100644 index 0000000..8c6ea4b --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Signer.php @@ -0,0 +1,27 @@ + + * Date: 5/14/2018 AD + * Time: 00:13 + */ + +namespace MiladRahimi\Jwt\Cryptography; + +interface Signer +{ + /** + * Get algorithm name + * + * @return string + */ + public function getName(): string; + + /** + * Sign data + * + * @param string $data + * @return string + */ + public function sign(string $data): string; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Cryptography/Verifier.php b/src/MiladRahimi/Jwt/Cryptography/Verifier.php new file mode 100644 index 0000000..ab50ecd --- /dev/null +++ b/src/MiladRahimi/Jwt/Cryptography/Verifier.php @@ -0,0 +1,24 @@ + + * Date: 5/14/2018 AD + * Time: 00:13 + */ + +namespace MiladRahimi\Jwt\Cryptography; + +use MiladRahimi\Jwt\Exceptions\InvalidSignatureException; + +interface Verifier +{ + /** + * Verify token signature + * + * @param string $header + * @param string $payload + * @param string $signature + * @throws InvalidSignatureException + */ + public function verify(string $header, string $payload, string $signature): void; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Enums/PublicClaimNames.php b/src/MiladRahimi/Jwt/Enums/PublicClaimNames.php new file mode 100644 index 0000000..fc695c4 --- /dev/null +++ b/src/MiladRahimi/Jwt/Enums/PublicClaimNames.php @@ -0,0 +1,20 @@ + + * Date: 5/16/2018 AD + * Time: 20:35 + */ + +namespace MiladRahimi\Jwt\Enums; + +class PublicClaimNames +{ + const ISSUER = 'iss'; + const SUBJECT = 'sub'; + const AUDIENCE = 'aud'; + const EXPIRATION_TIME = 'exp'; + const NOT_BEFORE = 'nbf'; + const ISSUED_AT = 'iat'; + const ID = 'jti'; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Exceptions/InvalidJsonException.php b/src/MiladRahimi/Jwt/Exceptions/InvalidJsonException.php new file mode 100644 index 0000000..e760fa3 --- /dev/null +++ b/src/MiladRahimi/Jwt/Exceptions/InvalidJsonException.php @@ -0,0 +1,14 @@ + + * Date: 5/14/2018 AD + * Time: 00:31 + */ + +namespace MiladRahimi\Jwt\Exceptions; + +class InvalidJsonException extends InvalidTokenException +{ + // Nada! +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Exceptions/InvalidKeyException.php b/src/MiladRahimi/Jwt/Exceptions/InvalidKeyException.php new file mode 100644 index 0000000..51330ed --- /dev/null +++ b/src/MiladRahimi/Jwt/Exceptions/InvalidKeyException.php @@ -0,0 +1,16 @@ + + * Date: 5/14/2018 AD + * Time: 00:04 + */ + +namespace MiladRahimi\Jwt\Exceptions; + +use Exception; + +class InvalidKeyException extends Exception +{ + // Nada! +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Exceptions/InvalidSignatureException.php b/src/MiladRahimi/Jwt/Exceptions/InvalidSignatureException.php new file mode 100644 index 0000000..7d5857a --- /dev/null +++ b/src/MiladRahimi/Jwt/Exceptions/InvalidSignatureException.php @@ -0,0 +1,15 @@ + + * Date: 5/16/2018 AD + * Time: 21:08 + */ + +namespace MiladRahimi\Jwt\Exceptions; + + +class InvalidSignatureException extends InvalidTokenException +{ + // Nada! +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Exceptions/InvalidTokenException.php b/src/MiladRahimi/Jwt/Exceptions/InvalidTokenException.php new file mode 100644 index 0000000..3b868e4 --- /dev/null +++ b/src/MiladRahimi/Jwt/Exceptions/InvalidTokenException.php @@ -0,0 +1,16 @@ + + * Date: 5/15/2018 AD + * Time: 23:10 + */ + +namespace MiladRahimi\Jwt\Exceptions; + +use Exception; + +class InvalidTokenException extends Exception +{ + // Nada! +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Exceptions/ValidationException.php b/src/MiladRahimi/Jwt/Exceptions/ValidationException.php new file mode 100644 index 0000000..b7880ac --- /dev/null +++ b/src/MiladRahimi/Jwt/Exceptions/ValidationException.php @@ -0,0 +1,15 @@ + + * Date: 5/16/2018 AD + * Time: 21:08 + */ + +namespace MiladRahimi\Jwt\Exceptions; + + +class ValidationException extends InvalidTokenException +{ + // Nada! +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Json/JsonParser.php b/src/MiladRahimi/Jwt/Json/JsonParser.php new file mode 100644 index 0000000..cef98ba --- /dev/null +++ b/src/MiladRahimi/Jwt/Json/JsonParser.php @@ -0,0 +1,47 @@ + + * Date: 5/14/2018 AD + * Time: 00:27 + */ + +namespace MiladRahimi\Jwt\Json; + +use MiladRahimi\Jwt\Exceptions\InvalidJsonException; + +class JsonParser implements JsonParserInterface +{ + /** + * Encode JSON + * + * @param array $data + * @return string + */ + public function encode(array $data): string + { + return json_encode($data); + } + + /** + * Decode JSON + * + * @param string $data + * @return array + * @throws InvalidJsonException + */ + public function decode(string $data): array + { + $result = json_decode($data, true); + + if (json_last_error()) { + throw new InvalidJsonException(json_last_error_msg(), json_last_error()); + } + + if (is_array($result) == false) { + throw new InvalidJsonException(); + } + + return $result; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Json/JsonParserInterface.php b/src/MiladRahimi/Jwt/Json/JsonParserInterface.php new file mode 100644 index 0000000..f070dff --- /dev/null +++ b/src/MiladRahimi/Jwt/Json/JsonParserInterface.php @@ -0,0 +1,31 @@ + + * Date: 5/14/2018 AD + * Time: 00:37 + */ + +namespace MiladRahimi\Jwt\Json; + +use MiladRahimi\Jwt\Exceptions\InvalidJsonException; + +interface JsonParserInterface +{ + /** + * Encode JSON + * + * @param array $data + * @return string + */ + public function encode(array $data): string; + + /** + * Decode JSON + * + * @param string $data + * @return array + * @throws InvalidJsonException + */ + public function decode(string $data): array; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/JwtGenerator.php b/src/MiladRahimi/Jwt/JwtGenerator.php new file mode 100644 index 0000000..3a28676 --- /dev/null +++ b/src/MiladRahimi/Jwt/JwtGenerator.php @@ -0,0 +1,125 @@ + + * Date: 5/13/2018 AD + * Time: 23:38 + */ + +namespace MiladRahimi\Jwt; + +use MiladRahimi\Jwt\Base64\Base64Parser; +use MiladRahimi\Jwt\Base64\Base64ParserInterface; +use MiladRahimi\Jwt\Cryptography\Signer; +use MiladRahimi\Jwt\Json\JsonParser; +use MiladRahimi\Jwt\Json\JsonParserInterface; + +class JwtGenerator +{ + /** + * @var Signer + */ + private $signer; + + /** + * @var JsonParserInterface + */ + private $jsonParser; + + /** + * @var Base64ParserInterface + */ + private $base64Parser; + + /** + * JwtGenerator constructor. + * + * @param Signer $signer + * @param JsonParserInterface|null $jsonParser + * @param Base64ParserInterface|null $base64Parser + */ + public function __construct( + Signer $signer, + JsonParserInterface $jsonParser = null, + Base64ParserInterface $base64Parser = null + ) { + $this->setSigner($signer); + $this->setJsonParser($jsonParser ?: new JsonParser()); + $this->setBase64Parser($base64Parser ?: new Base64Parser()); + } + + /** + * Generate JWT from given claims + * + * @param array $claims + * @return string JWT + */ + public function generate(array $claims = []): string + { + $header = $this->base64Parser->encode($this->jsonParser->encode($this->generateHeader())); + + $payload = $this->base64Parser->encode($this->jsonParser->encode($claims)); + + $signature = $this->base64Parser->encode($this->signer->sign($header . '.' . $payload)); + + return $header . '.' . $payload . '.' . $signature; + } + + /** + * Generate JWT header + * + * @return string[] [alg, type] + */ + private function generateHeader(): array + { + return ['alg' => $this->signer->getName(), 'typ' => 'JWT']; + } + + /** + * @return JsonParserInterface + */ + public function getJsonParser(): JsonParserInterface + { + return $this->jsonParser; + } + + /** + * @param JsonParserInterface $jsonParser + */ + public function setJsonParser(JsonParserInterface $jsonParser): void + { + $this->jsonParser = $jsonParser; + } + + /** + * @return Base64ParserInterface + */ + public function getBase64Parser(): Base64ParserInterface + { + return $this->base64Parser; + } + + /** + * @param Base64ParserInterface $base64Parser + */ + public function setBase64Parser(Base64ParserInterface $base64Parser): void + { + $this->base64Parser = $base64Parser; + } + + /** + * @return Signer + */ + public function getSigner(): Signer + { + return $this->signer; + } + + /** + * @param Signer $signer + */ + public function setSigner(Signer $signer): void + { + $this->signer = $signer; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/JwtParser.php b/src/MiladRahimi/Jwt/JwtParser.php new file mode 100644 index 0000000..1294c08 --- /dev/null +++ b/src/MiladRahimi/Jwt/JwtParser.php @@ -0,0 +1,225 @@ + + * Date: 5/13/2018 AD + * Time: 23:38 + */ + +namespace MiladRahimi\Jwt; + +use MiladRahimi\Jwt\Base64\Base64Parser; +use MiladRahimi\Jwt\Base64\Base64ParserInterface; +use MiladRahimi\Jwt\Cryptography\Verifier; +use MiladRahimi\Jwt\Exceptions\InvalidJsonException; +use MiladRahimi\Jwt\Exceptions\InvalidSignatureException; +use MiladRahimi\Jwt\Exceptions\InvalidTokenException; +use MiladRahimi\Jwt\Exceptions\ValidationException; +use MiladRahimi\Jwt\Json\JsonParser; +use MiladRahimi\Jwt\Json\JsonParserInterface; +use MiladRahimi\Jwt\Validator\DefaultValidator; +use MiladRahimi\Jwt\Validator\Validator; +use MiladRahimi\Jwt\Validator\ValidatorInterface; + +class JwtParser +{ + /** + * @var Verifier + */ + private $verifier; + + /** + * @var JsonParserInterface + */ + private $jsonParser; + + /** + * @var Base64ParserInterface + */ + private $base64Parser; + + /** + * @var ValidatorInterface + */ + private $validator; + + /** + * JwtParser constructor. + * + * @param Verifier $verifier + * @param Validator|null $validator + * @param JsonParserInterface|null $jsonParser + * @param Base64ParserInterface|null $base64Parser + */ + public function __construct( + Verifier $verifier, + Validator $validator = null, + JsonParserInterface $jsonParser = null, + Base64ParserInterface $base64Parser = null + ) { + $this->setVerifier($verifier); + $this->setValidator($validator ?: new DefaultValidator()); + $this->setJsonParser($jsonParser ?: new JsonParser()); + $this->setBase64Parser($base64Parser ?: new Base64Parser()); + } + + /** + * Parse (verify and validate) JWT and retrieve claims + * + * @param string $jwt + * @return array[] + * @throws InvalidJsonException + * @throws InvalidSignatureException + * @throws InvalidTokenException + * @throws ValidationException + */ + public function parse(string $jwt): array + { + $this->verifySignature($jwt); + + $claims = $this->extractClaims($jwt); + $this->validateClaims($claims); + + return $claims; + } + + /** + * Verify JWT signature + * + * @param string $jwt + * @return void + * @throws InvalidSignatureException + * @throws InvalidTokenException + */ + public function verifySignature(string $jwt): void + { + list($header, $payload, $signature) = $this->explodeJwt($jwt); + + $this->verifier->verify($header, $payload, $signature); + } + + /** + * Explode jwt to its sections + * + * @param string $jwt + * @return string[] [header, payload, signature] + * @throws InvalidTokenException + */ + private function explodeJwt(string $jwt): array + { + $sections = explode('.', $jwt); + + if (count($sections) != 3) { + throw new InvalidTokenException('Token format is not valid'); + } + + return [$sections[0], $sections[1], $sections[2]]; + } + + /** + * Extract claims from JWT + * + * @param string $jwt + * @return array + * @throws InvalidJsonException + * @throws InvalidTokenException + */ + private function extractClaims(string $jwt): array + { + $payload = $this->explodeJwt($jwt)[1]; + + return $this->jsonParser->decode($this->base64Parser->decode($payload)); + } + + /** + * Validate claims + * + * @param array $claims + * @throws ValidationException + */ + public function validateClaims(array $claims): void + { + $this->validator->validate($claims); + } + + /** + * Validate JWT (verify signature and validate claims) + * + * @param string $jwt + * @throws InvalidJsonException + * @throws InvalidSignatureException + * @throws InvalidTokenException + * @throws ValidationException + */ + public function validate(string $jwt): void + { + $this->verifySignature($jwt); + + $claims = $this->extractClaims($jwt); + $this->validateClaims($claims); + } + + /** + * @return JsonParserInterface + */ + public function getJsonParser(): JsonParserInterface + { + return $this->jsonParser; + } + + /** + * @param JsonParserInterface $jsonParser + */ + public function setJsonParser(JsonParserInterface $jsonParser): void + { + $this->jsonParser = $jsonParser; + } + + /** + * @return Base64ParserInterface + */ + public function getBase64Parser(): Base64ParserInterface + { + return $this->base64Parser; + } + + /** + * @param Base64ParserInterface $base64Parser + */ + public function setBase64Parser(Base64ParserInterface $base64Parser): void + { + $this->base64Parser = $base64Parser; + } + + /** + * @return Verifier + */ + public function getVerifier(): Verifier + { + return $this->verifier; + } + + /** + * @param Verifier $verifier + */ + public function setVerifier(Verifier $verifier): void + { + $this->verifier = $verifier; + } + + /** + * @return ValidatorInterface + */ + public function getValidator(): ValidatorInterface + { + return $this->validator; + } + + /** + * @param ValidatorInterface $validator + */ + public function setValidator(ValidatorInterface $validator): void + { + $this->validator = $validator; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/DefaultValidator.php b/src/MiladRahimi/Jwt/Validator/DefaultValidator.php new file mode 100644 index 0000000..3752ba5 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/DefaultValidator.php @@ -0,0 +1,26 @@ + + * Date: 5/16/2018 AD + * Time: 01:46 + */ + +namespace MiladRahimi\Jwt\Validator; + +use MiladRahimi\Jwt\Enums\PublicClaimNames; +use MiladRahimi\Jwt\Validator\Rules\Optional\NewerThan; +use MiladRahimi\Jwt\Validator\Rules\Optional\OlderThanOrSameTimeWith; + +class DefaultValidator extends Validator +{ + /** + * DefaultVerifier constructor. + */ + public function __construct() + { + $this->addRule(PublicClaimNames::EXPIRATION_TIME, new NewerThan(time())); + $this->addRule(PublicClaimNames::NOT_BEFORE, new OlderThanOrSameTimeWith(time())); + $this->addRule(PublicClaimNames::ISSUED_AT, new OlderThanOrSameTimeWith(time())); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rule.php b/src/MiladRahimi/Jwt/Validator/Rule.php new file mode 100644 index 0000000..8013ba3 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rule.php @@ -0,0 +1,21 @@ + + * Date: 5/16/2018 AD + * Time: 00:40 + */ + +namespace MiladRahimi\Jwt\Validator; + +interface Rule +{ + /** + * Check value + * + * @param $value + * @param bool $exists + * @return bool + */ + public function check($value, bool $exists): bool; +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Optional/ConsistsOf.php b/src/MiladRahimi/Jwt/Validator/Rules/Optional/ConsistsOf.php new file mode 100644 index 0000000..1f4bded --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Optional/ConsistsOf.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 01:01 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Optional; + +use MiladRahimi\Jwt\Validator\Rule; + +class ConsistsOf implements Rule +{ + /** + * @var string + */ + private $string; + + /** + * @param string $string + */ + public function __construct(string $string) + { + $this->string = $string; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists == false || strpos($value ?: '', $this->string); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Optional/EqualsTo.php b/src/MiladRahimi/Jwt/Validator/Rules/Optional/EqualsTo.php new file mode 100644 index 0000000..105d756 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Optional/EqualsTo.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Optional; + +use MiladRahimi\Jwt\Validator\Rule; + +class EqualsTo implements Rule +{ + /** + * @var mixed + */ + private $expectedValue; + + /** + * @param mixed $expectedValue + */ + public function __construct($expectedValue) + { + $this->expectedValue = $expectedValue; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists == false || $this->expectedValue == $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Optional/GreaterThan.php b/src/MiladRahimi/Jwt/Validator/Rules/Optional/GreaterThan.php new file mode 100644 index 0000000..2d78814 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Optional/GreaterThan.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Optional; + +use MiladRahimi\Jwt\Validator\Rule; + +class GreaterThan implements Rule +{ + /** + * @var float + */ + protected $number; + + /** + * @param float $number + */ + public function __construct(float $number) + { + $this->number = $number; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists == false || $this->number > $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Optional/GreaterThanOrEqualTo.php b/src/MiladRahimi/Jwt/Validator/Rules/Optional/GreaterThanOrEqualTo.php new file mode 100644 index 0000000..7447c93 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Optional/GreaterThanOrEqualTo.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Optional; + +use MiladRahimi\Jwt\Validator\Rule; + +class GreaterThanOrEqualTo implements Rule +{ + /** + * @var float + */ + protected $number; + + /** + * @param float $number + */ + public function __construct(float $number) + { + $this->number = $number; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists == false || $this->number >= $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Optional/IdenticalTo.php b/src/MiladRahimi/Jwt/Validator/Rules/Optional/IdenticalTo.php new file mode 100644 index 0000000..b4b8291 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Optional/IdenticalTo.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Optional; + +use MiladRahimi\Jwt\Validator\Rule; + +class IdenticalTo implements Rule +{ + /** + * @var mixed + */ + private $expectedValueAndType; + + /** + * @param mixed $expectedValueAndType + */ + public function __construct($expectedValueAndType) + { + $this->expectedValueAndType = $expectedValueAndType; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists == false || $this->expectedValueAndType === $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Optional/LessThan.php b/src/MiladRahimi/Jwt/Validator/Rules/Optional/LessThan.php new file mode 100644 index 0000000..f799009 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Optional/LessThan.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Optional; + +use MiladRahimi\Jwt\Validator\Rule; + +class LessThan implements Rule +{ + /** + * @var float + */ + protected $number; + + /** + * @param float $number + */ + public function __construct(float $number) + { + $this->number = $number; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists == false || $this->number < $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Optional/LessThanOrEqualTo.php b/src/MiladRahimi/Jwt/Validator/Rules/Optional/LessThanOrEqualTo.php new file mode 100644 index 0000000..d178d2f --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Optional/LessThanOrEqualTo.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Optional; + +use MiladRahimi\Jwt\Validator\Rule; + +class LessThanOrEqualTo implements Rule +{ + /** + * @var float + */ + protected $number; + + /** + * @param float $number + */ + public function __construct(float $number) + { + $this->number = $number; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists == false || $this->number <= $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Optional/NewerThan.php b/src/MiladRahimi/Jwt/Validator/Rules/Optional/NewerThan.php new file mode 100644 index 0000000..f0ff69e --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Optional/NewerThan.php @@ -0,0 +1,20 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Optional; + +class NewerThan extends GreaterThan +{ + /** + * @param float $timestamp + */ + public function __construct(float $timestamp) + { + parent::__construct($timestamp); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Optional/NewerThanOrSameTimeWith.php b/src/MiladRahimi/Jwt/Validator/Rules/Optional/NewerThanOrSameTimeWith.php new file mode 100644 index 0000000..af939c2 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Optional/NewerThanOrSameTimeWith.php @@ -0,0 +1,20 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Optional; + +class NewerThanOrSameTimeWith extends GreaterThanOrEqualTo +{ + /** + * @param float $timestamp + */ + public function __construct(float $timestamp) + { + parent::__construct($timestamp); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Optional/OlderThan.php b/src/MiladRahimi/Jwt/Validator/Rules/Optional/OlderThan.php new file mode 100644 index 0000000..b963d32 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Optional/OlderThan.php @@ -0,0 +1,20 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Optional; + +class OlderThan extends LessThan +{ + /** + * @param float $timestamp + */ + public function __construct(float $timestamp) + { + parent::__construct($timestamp); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Optional/OlderThanOrSameTimeWith.php b/src/MiladRahimi/Jwt/Validator/Rules/Optional/OlderThanOrSameTimeWith.php new file mode 100644 index 0000000..33450dd --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Optional/OlderThanOrSameTimeWith.php @@ -0,0 +1,20 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Optional; + +class OlderThanOrSameTimeWith extends LessThanOrEqualTo +{ + /** + * @param float $timestamp + */ + public function __construct(float $timestamp) + { + parent::__construct($timestamp); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/ConsistsOf.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/ConsistsOf.php new file mode 100644 index 0000000..c16f77d --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/ConsistsOf.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 01:01 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +use MiladRahimi\Jwt\Validator\Rule; + +class ConsistsOf implements Rule +{ + /** + * @var string + */ + private $string; + + /** + * @param string $string + */ + public function __construct(string $string) + { + $this->string = $string; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists && strpos($value ?: '', $this->string); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/EqualsTo.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/EqualsTo.php new file mode 100644 index 0000000..bf121c3 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/EqualsTo.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +use MiladRahimi\Jwt\Validator\Rule; + +class EqualsTo implements Rule +{ + /** + * @var mixed + */ + private $expectedValue; + + /** + * @param mixed $expectedValue + */ + public function __construct($expectedValue) + { + $this->expectedValue = $expectedValue; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists && $this->expectedValue == $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/Exists.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/Exists.php new file mode 100644 index 0000000..4dd5ac3 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/Exists.php @@ -0,0 +1,22 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +use MiladRahimi\Jwt\Validator\Rule; + +class Exists implements Rule +{ + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/GreaterThan.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/GreaterThan.php new file mode 100644 index 0000000..4e2a33c --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/GreaterThan.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +use MiladRahimi\Jwt\Validator\Rule; + +class GreaterThan implements Rule +{ + /** + * @var float + */ + protected $number; + + /** + * @param float $number + */ + public function __construct(float $number) + { + $this->number = $number; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists && $this->number > $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/GreaterThanOrEqualTo.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/GreaterThanOrEqualTo.php new file mode 100644 index 0000000..d809d9d --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/GreaterThanOrEqualTo.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +use MiladRahimi\Jwt\Validator\Rule; + +class GreaterThanOrEqualTo implements Rule +{ + /** + * @var float + */ + protected $number; + + /** + * @param float $number + */ + public function __construct(float $number) + { + $this->number = $number; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists && $this->number >= $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/IdenticalTo.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/IdenticalTo.php new file mode 100644 index 0000000..0c59ae7 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/IdenticalTo.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +use MiladRahimi\Jwt\Validator\Rule; + +class IdenticalTo implements Rule +{ + /** + * @var mixed + */ + private $expectedValueAndType; + + /** + * @param mixed $expectedValueAndType + */ + public function __construct($expectedValueAndType) + { + $this->expectedValueAndType = $expectedValueAndType; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists && $this->expectedValueAndType === $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/LessThan.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/LessThan.php new file mode 100644 index 0000000..b8f53eb --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/LessThan.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +use MiladRahimi\Jwt\Validator\Rule; + +class LessThan implements Rule +{ + /** + * @var float + */ + protected $number; + + /** + * @param float $number + */ + public function __construct(float $number) + { + $this->number = $number; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists && $this->number < $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/LessThanOrEqualTo.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/LessThanOrEqualTo.php new file mode 100644 index 0000000..eb69c4d --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/LessThanOrEqualTo.php @@ -0,0 +1,35 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +use MiladRahimi\Jwt\Validator\Rule; + +class LessThanOrEqualTo implements Rule +{ + /** + * @var float + */ + protected $number; + + /** + * @param float $number + */ + public function __construct(float $number) + { + $this->number = $number; + } + + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists && $this->number <= $value; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/NewerThan.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/NewerThan.php new file mode 100644 index 0000000..e2567d1 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/NewerThan.php @@ -0,0 +1,20 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +class NewerThan extends GreaterThan +{ + /** + * @param float $timestamp + */ + public function __construct(float $timestamp) + { + parent::__construct($timestamp); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/NewerThanOrSameTimeWith.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/NewerThanOrSameTimeWith.php new file mode 100644 index 0000000..fe0f7ff --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/NewerThanOrSameTimeWith.php @@ -0,0 +1,20 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +class NewerThanOrSameTimeWith extends GreaterThanOrEqualTo +{ + /** + * @param float $timestamp + */ + public function __construct(float $timestamp) + { + parent::__construct($timestamp); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/NotNull.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/NotNull.php new file mode 100644 index 0000000..8af8517 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/NotNull.php @@ -0,0 +1,22 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +use MiladRahimi\Jwt\Validator\Rule; + +class NotNull implements Rule +{ + /** + * @inheritdoc + */ + public function check($value, bool $exists): bool + { + return $exists && $value !== null; + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/OlderThan.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/OlderThan.php new file mode 100644 index 0000000..531f0a3 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/OlderThan.php @@ -0,0 +1,20 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +class OlderThan extends LessThan +{ + /** + * @param float $timestamp + */ + public function __construct(float $timestamp) + { + parent::__construct($timestamp); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Rules/Required/OlderThanOrSameTimeWith.php b/src/MiladRahimi/Jwt/Validator/Rules/Required/OlderThanOrSameTimeWith.php new file mode 100644 index 0000000..c2e8560 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Rules/Required/OlderThanOrSameTimeWith.php @@ -0,0 +1,20 @@ + + * Date: 5/16/2018 AD + * Time: 00:42 + */ + +namespace MiladRahimi\Jwt\Validator\Rules\Required; + +class OlderThanOrSameTimeWith extends LessThanOrEqualTo +{ + /** + * @param float $timestamp + */ + public function __construct(float $timestamp) + { + parent::__construct($timestamp); + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/Validator.php b/src/MiladRahimi/Jwt/Validator/Validator.php new file mode 100644 index 0000000..814e416 --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/Validator.php @@ -0,0 +1,56 @@ + + * Date: 5/16/2018 AD + * Time: 01:27 + */ + +namespace MiladRahimi\Jwt\Validator; + +use MiladRahimi\Jwt\Exceptions\ValidationException; + +class Validator implements ValidatorInterface +{ + /** + * @var array + */ + protected $rules = []; + + /** + * @inheritdoc + */ + public function addRule(string $claimName, Rule $rule): void + { + $this->rules[$claimName][] = $rule; + } + + /** + * @inheritdoc + */ + public function cleanRules(string $claimName): void + { + unset($this->rules[$claimName]); + } + + /** + * @inheritdoc + */ + public function validate(array $claims = []): void + { + /** + * @var string $claimName + * @var Rule[] $rules + */ + foreach ($this->rules as $claimName => $rules) { + $exists = isset($claims[$claimName]); + $value = $exists ? $claims[$claimName] : null; + + foreach ($rules as $rule) { + if ($rule->check($value, $exists) == false) { + throw new ValidationException('Validation failed for the claim: ' . $claimName); + } + } + } + } +} \ No newline at end of file diff --git a/src/MiladRahimi/Jwt/Validator/ValidatorInterface.php b/src/MiladRahimi/Jwt/Validator/ValidatorInterface.php new file mode 100644 index 0000000..03f450a --- /dev/null +++ b/src/MiladRahimi/Jwt/Validator/ValidatorInterface.php @@ -0,0 +1,37 @@ + + * Date: 5/16/2018 AD + * Time: 00:27 + */ + +namespace MiladRahimi\Jwt\Validator; + +use MiladRahimi\Jwt\Exceptions\ValidationException; + +interface ValidatorInterface +{ + /** + * Add a new rule + * + * @param string $claimName + * @param Rule $rule + */ + public function addRule(string $claimName, Rule $rule): void; + + /** + * Clean added rules for given claim + * + * @param string $claimName + */ + public function cleanRules(string $claimName): void; + + /** + * Verify claims + * + * @param string[] $claims + * @throws ValidationException + */ + public function validate(array $claims = []): void; +} \ No newline at end of file diff --git a/tests/Base64ParserTest.php b/tests/Base64ParserTest.php new file mode 100644 index 0000000..32b8222 --- /dev/null +++ b/tests/Base64ParserTest.php @@ -0,0 +1,40 @@ + + * Date: 5/20/2018 AD + * Time: 00:34 + */ + +namespace MiladRahimi\Jwt\Tests; + +use MiladRahimi\Jwt\Base64\Base64Parser; +use MiladRahimi\Jwt\Base64\Base64ParserInterface; + +class Base64ParserTest extends TestCase +{ + /** + * @var Base64ParserInterface + */ + private $service; + + public function setUp() + { + parent::setUp(); + + $this->service = new Base64Parser(); + } + + public function test_encoding_and_decoding_it_should_get_done_successfully() + { + $plainText = md5(mt_rand(1, 100)); + + $encoded = $this->service->encode($plainText); + + $this->assertNotEmpty($encoded); + + $decoded = $this->service->decode($encoded); + + $this->assertEquals($plainText, $decoded); + } +} \ No newline at end of file diff --git a/tests/HSTest.php b/tests/HSTest.php new file mode 100644 index 0000000..02c6348 --- /dev/null +++ b/tests/HSTest.php @@ -0,0 +1,79 @@ + + * Date: 5/14/2018 AD + * Time: 01:05 + */ + +namespace MiladRahimi\Jwt\Tests; + +use MiladRahimi\Jwt\Cryptography\Algorithms\Hmac\HS256; +use MiladRahimi\Jwt\Cryptography\Algorithms\Hmac\HS384; +use MiladRahimi\Jwt\Cryptography\Algorithms\Hmac\HS512; +use MiladRahimi\Jwt\JwtGenerator; +use MiladRahimi\Jwt\JwtParser; + +class HSTest extends TestCase +{ + private $key; + + /** + * @throws \MiladRahimi\Jwt\Exceptions\InvalidKeyException + * @throws \MiladRahimi\Jwt\Exceptions\InvalidTokenException + */ + public function test_with_hs256_it_should_generate_jwt_and_parse_it() + { + $generator = new JwtGenerator(new HS256($this->key())); + $jwt = $generator->generate(['sub' => 1, 'jti' => 2]); + + $parser = new JwtParser(new HS256($this->key())); + $parser->verifySignature($jwt); + $parser->validate($jwt); + $claims = $parser->parse($jwt); + + $this->assertEquals($claims['sub'], 1); + $this->assertEquals($claims['jti'], 2); + } + + private function key(): string + { + return $this->key ?: $this->key = md5(mt_rand(1, 100)); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\InvalidKeyException + * @throws \MiladRahimi\Jwt\Exceptions\InvalidTokenException + */ + public function test_with_hs384_signer_it_should_generate_jwt_and_parse_it() + { + $service = new JwtGenerator(new HS384($this->key())); + $jwt = $service->generate(['sub' => 1, 'jti' => 2]); + + $parser = new JwtParser(new HS384($this->key())); + $parser->verifySignature($jwt); + $parser->validate($jwt); + $claims = $parser->parse($jwt); + + $this->assertEquals($claims['sub'], 1); + $this->assertEquals($claims['jti'], 2); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\InvalidKeyException + * @throws \MiladRahimi\Jwt\Exceptions\InvalidTokenException + */ + public function test_with_hs512_signer_it_should_generate_jwt_and_parse_it() + { + $service = new JwtGenerator(new HS512($this->key())); + $jwt = $service->generate(['sub' => 1, 'jti' => 2]); + + $parser = new JwtParser(new HS512($this->key())); + $parser->verifySignature($jwt); + $parser->validate($jwt); + $claims = $parser->parse($jwt); + + $this->assertEquals($claims['sub'], 1); + $this->assertEquals($claims['jti'], 2); + } +} \ No newline at end of file diff --git a/tests/JsonParserTest.php b/tests/JsonParserTest.php new file mode 100644 index 0000000..4147d40 --- /dev/null +++ b/tests/JsonParserTest.php @@ -0,0 +1,70 @@ + + * Date: 5/20/2018 AD + * Time: 00:34 + */ + +namespace MiladRahimi\Jwt\Tests; + +use MiladRahimi\Jwt\Exceptions\InvalidJsonException; +use MiladRahimi\Jwt\Json\JsonParser; +use MiladRahimi\Jwt\Json\JsonParserInterface; + +class JsonParserTest extends TestCase +{ + /** + * @var JsonParserInterface + */ + private $service; + + public function setUp() + { + parent::setUp(); + + $this->service = new JsonParser(); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\InvalidJsonException + */ + public function test_encoding_and_decoding_it_should_get_done_successfully() + { + $array = [ + 'string' => md5(mt_rand(1, 100)), + 'integer' => mt_rand(1, 100), + 'true' => true, + 'false' => false, + ]; + + $encoded = $this->service->encode($array); + + $decoded = $this->service->decode($encoded); + + $this->assertSame($array['string'], $decoded['string']); + $this->assertSame($array['integer'], $decoded['integer']); + $this->assertSame($array['true'], $decoded['true']); + $this->assertSame($array['false'], $decoded['false']); + } + + /** + * @throws InvalidJsonException + */ + public function test_decoding_it_should_throw_an_exception_when_json_is_invalid() + { + $this->expectException(InvalidJsonException::class); + + $this->service->decode('Invalid JSON'); + } + + /** + * @throws InvalidJsonException + */ + public function test_decoding_it_should_throw_an_exception_when_json_is_invalid_2() + { + $this->expectException(InvalidJsonException::class); + + $this->service->decode(json_encode('String...')); + } +} \ No newline at end of file diff --git a/tests/OptionalValidationRulesTest.php b/tests/OptionalValidationRulesTest.php new file mode 100644 index 0000000..f7709c2 --- /dev/null +++ b/tests/OptionalValidationRulesTest.php @@ -0,0 +1,426 @@ + + * Date: 5/20/2018 AD + * Time: 00:34 + */ + +namespace MiladRahimi\Jwt\Tests; + +use MiladRahimi\Jwt\Exceptions\ValidationException; +use MiladRahimi\Jwt\Validator\Rules\Optional\ConsistsOf; +use MiladRahimi\Jwt\Validator\Rules\Optional\EqualsTo; +use MiladRahimi\Jwt\Validator\Rules\Optional\GreaterThan; +use MiladRahimi\Jwt\Validator\Rules\Optional\GreaterThanOrEqualTo; +use MiladRahimi\Jwt\Validator\Rules\Optional\IdenticalTo; +use MiladRahimi\Jwt\Validator\Rules\Optional\LessThan; +use MiladRahimi\Jwt\Validator\Rules\Optional\LessThanOrEqualTo; +use MiladRahimi\Jwt\Validator\Validator; +use MiladRahimi\Jwt\Validator\ValidatorInterface; + +class OptionalValidationRulesTest extends TestCase +{ + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_consist_of_it_should_pass_when_the_claim_consists_of_the_value() + { + $service = $this->service(); + + $service->addRule('iss', new ConsistsOf('Company')); + + $service->validate(['iss' => 'My Company']); + + $this->assertTrue(true); + } + + public function service(): ValidatorInterface + { + return new Validator(); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_consist_of_it_should_pass_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('iss', new ConsistsOf('Company')); + + $service->validate(); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_consist_of_it_should_fail_when_the_claim_does_not_consists_of_the_value() + { + $service = $this->service(); + + $service->addRule('iss', new ConsistsOf('Company')); + + $this->expectException(ValidationException::class); + + $service->validate(['iss' => 'My Corporate']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_equals_to_it_should_pass_when_the_claim_equals_to_the_value() + { + $service = $this->service(); + + $service->addRule('aud', new EqualsTo('Customer')); + + $service->validate(['aud' => 'Customer']); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_equals_to_it_should_pass_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('aud', new EqualsTo('Customer')); + + $service->validate(); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_equals_to_it_should_fail_when_the_claim_does_not_equals_to_the_value() + { + $service = $this->service(); + + $service->addRule('aud', new EqualsTo('Customer')); + + $this->expectException(ValidationException::class); + + $service->validate(['aud' => 'Other Customer']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_greater_than_it_should_pass_when_the_claim_greater_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThan(100)); + + $service->validate(['sub' => 99]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_greater_than_it_should_pass_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThan(100)); + + $service->validate(); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_greater_than_it_should_fail_when_the_claim_is_not_greater_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThan(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => '1001']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_greater_than_it_should_fail_when_the_claim_equals_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThan(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => 1000]); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_gt_or_equal_to_it_should_pass_when_the_claim_greater_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThanOrEqualTo(100)); + + $service->validate(['sub' => 99]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_gt_or_equal_to_it_should_pass_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThanOrEqualTo(100)); + + $service->validate(); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_gt_or_equal_to_it_should_fail_when_the_claim_is_not_greater_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThanOrEqualTo(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => '1001']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_gt_or_equal_to_it_should_pass_when_the_claim_equals_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThanOrEqualTo(1000)); + + $service->validate(['sub' => 1000]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_identical_to_it_should_pass_when_the_claim_is_identical_to_int_value() + { + $service = $this->service(); + + $service->addRule('num', new IdenticalTo(666)); + + $service->validate(['num' => 666]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_identical_to_it_should_pass_when_the_claim_is_identical_to_bool_value() + { + $service = $this->service(); + + $service->addRule('is', new IdenticalTo(true)); + + $service->validate(['is' => true]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_identical_to_it_should_fail_when_the_claim_is_does_not_exist() + { + $service = $this->service(); + + $service->addRule('num', new IdenticalTo(666)); + + $service->validate(); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_identical_to_it_should_fail_when_the_claim_is_not_identical_to_int_value() + { + $service = $this->service(); + + $service->addRule('num', new IdenticalTo(666)); + + $this->expectException(ValidationException::class); + + $service->validate(['num' => '666']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_identical_to_it_should_fail_when_the_claim_is_not_identical_to_bool_value() + { + $service = $this->service(); + + $service->addRule('is', new IdenticalTo(true)); + + $this->expectException(ValidationException::class); + + $service->validate(['is' => 1]); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_less_than_it_should_pass_when_the_claim_less_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThan(100)); + + $service->validate(['sub' => 101]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_less_than_it_should_pass_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('sub', new LessThan(100)); + + $service->validate(); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_less_than_it_should_fail_when_the_claim_is_not_less_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThan(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => '999']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_less_than_it_should_fail_when_the_claim_equals_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThan(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => 1000]); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_lt_or_equal_to_it_should_pass_when_the_claim_less_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThanOrEqualTo(100)); + + $service->validate(['sub' => 101]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_lt_or_equal_to_it_should_pass_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('sub', new LessThanOrEqualTo(100)); + + $service->validate(); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_lt_or_equal_to_it_should_fail_when_the_claim_is_not_less_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThanOrEqualTo(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => '999']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_lt_or_equal_to_it_should_pass_when_the_claim_equals_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThanOrEqualTo(1000)); + + $service->validate(['sub' => 1000]); + + $this->assertTrue(true); + } +} \ No newline at end of file diff --git a/tests/RSTest.php b/tests/RSTest.php new file mode 100644 index 0000000..cd151db --- /dev/null +++ b/tests/RSTest.php @@ -0,0 +1,105 @@ + + * Date: 5/14/2018 AD + * Time: 01:05 + */ + +namespace MiladRahimi\Jwt\Tests; + +use MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Signers\RS256 as RS256Signer; +use MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Signers\RS384 as RS384Signer; +use MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Signers\RS512 as RS512Signer; +use MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Verifiers\RS256 as RS256Verifier; +use MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Verifiers\RS384 as RS384Verifier; +use MiladRahimi\Jwt\Cryptography\Algorithms\Rsa\Verifiers\RS512 as RS512Verifier; +use MiladRahimi\Jwt\Cryptography\Keys\PrivateKey; +use MiladRahimi\Jwt\Cryptography\Keys\PublicKey; +use MiladRahimi\Jwt\JwtGenerator; +use MiladRahimi\Jwt\JwtParser; + +class RSTest extends TestCase +{ + /** + * @var PublicKey + */ + private $publicKey; + + /** + * @var PrivateKey + */ + private $privateKey; + + /** + * @return PrivateKey + * @throws \MiladRahimi\Jwt\Exceptions\InvalidKeyException + */ + private function privateKey(): PrivateKey + { + return $this->privateKey ?: $this->privateKey = new PrivateKey(__DIR__ . '/keys/private.pem'); + } + + /** + * @return PublicKey + * @throws \MiladRahimi\Jwt\Exceptions\InvalidKeyException + */ + private function publicKey(): PublicKey + { + return $this->publicKey ?: $this->publicKey = new PublicKey(__DIR__ . '/keys/public.pem'); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\InvalidTokenException + * @throws \MiladRahimi\Jwt\Exceptions\InvalidKeyException + */ + public function test_with_hs256_signer_it_should_generate_jwt_and_parse_it() + { + $generator = new JwtGenerator(new RS256Signer($this->privateKey())); + $jwt = $generator->generate(['sub' => 1, 'jti' => 2]); + + $parser = new JwtParser(new RS256Verifier($this->publicKey())); + $parser->verifySignature($jwt); + $parser->validate($jwt); + $claims = $parser->parse($jwt); + + $this->assertEquals($claims['sub'], 1); + $this->assertEquals($claims['jti'], 2); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\InvalidKeyException + * @throws \MiladRahimi\Jwt\Exceptions\InvalidTokenException + */ + public function test_with_hs384_signer_it_should_generate_jwt_and_parse_it() + { + $generator = new JwtGenerator(new RS384Signer($this->privateKey())); + $jwt = $generator->generate(['sub' => 1, 'jti' => 2]); + + $parser = new JwtParser(new RS384Verifier($this->publicKey())); + $parser->verifySignature($jwt); + $parser->validate($jwt); + $claims = $parser->parse($jwt); + + $this->assertEquals($claims['sub'], 1); + $this->assertEquals($claims['jti'], 2); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\InvalidKeyException + * @throws \MiladRahimi\Jwt\Exceptions\InvalidTokenException + */ + public function test_with_hs512_signer_it_should_generate_jwt_and_parse_it() + { + $generator = new JwtGenerator(new RS512Signer($this->privateKey())); + $jwt = $generator->generate(['sub' => 1, 'jti' => 2]); + + $parser = new JwtParser(new RS512Verifier($this->publicKey())); + $parser->verifySignature($jwt); + $parser->validate($jwt); + $claims = $parser->parse($jwt); + + $this->assertEquals($claims['sub'], 1); + $this->assertEquals($claims['jti'], 2); + } +} \ No newline at end of file diff --git a/tests/RequiredValidationRulesTest.php b/tests/RequiredValidationRulesTest.php new file mode 100644 index 0000000..5a72683 --- /dev/null +++ b/tests/RequiredValidationRulesTest.php @@ -0,0 +1,518 @@ + + * Date: 5/20/2018 AD + * Time: 00:34 + */ + +namespace MiladRahimi\Jwt\Tests; + +use MiladRahimi\Jwt\Exceptions\ValidationException; +use MiladRahimi\Jwt\Validator\Rules\Required\ConsistsOf; +use MiladRahimi\Jwt\Validator\Rules\Required\EqualsTo; +use MiladRahimi\Jwt\Validator\Rules\Required\Exists; +use MiladRahimi\Jwt\Validator\Rules\Required\GreaterThan; +use MiladRahimi\Jwt\Validator\Rules\Required\GreaterThanOrEqualTo; +use MiladRahimi\Jwt\Validator\Rules\Required\IdenticalTo; +use MiladRahimi\Jwt\Validator\Rules\Required\LessThan; +use MiladRahimi\Jwt\Validator\Rules\Required\LessThanOrEqualTo; +use MiladRahimi\Jwt\Validator\Rules\Required\NotNull; +use MiladRahimi\Jwt\Validator\Validator; +use MiladRahimi\Jwt\Validator\ValidatorInterface; + +class RequiredValidationRulesTest extends TestCase +{ + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_exists_it_should_pass_the_claim_when_it_exists() + { + $service = $this->service(); + + $service->addRule('exp', new Exists()); + + $service->validate(['exp' => time()]); + + $this->assertTrue(true); + } + + public function service(): ValidatorInterface + { + return new Validator(); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_exists_it_should_fail_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('exp', new Exists()); + + $this->expectException(ValidationException::class); + + $service->validate(); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_not_null_it_should_pass_when_the_claim_is_not_null() + { + $service = $this->service(); + + $service->addRule('jti', new NotNull()); + + $service->validate(['jti' => uniqid('ID-')]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_not_null_it_should_fail_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('jti', new NotNull()); + + $this->expectException(ValidationException::class); + + $service->validate(); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_not_null_it_should_fail_when_the_claim_is_null() + { + $service = $this->service(); + + $service->addRule('jti', new NotNull()); + + $this->expectException(ValidationException::class); + + $service->validate(['jti' => null]); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_consist_of_it_should_pass_when_the_claim_consists_of_the_value() + { + $service = $this->service(); + + $service->addRule('iss', new ConsistsOf('Company')); + + $service->validate(['iss' => 'My Company']); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_consist_of_it_should_fail_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('iss', new ConsistsOf('Company')); + + $this->expectException(ValidationException::class); + + $service->validate(); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_consist_of_it_should_fail_when_the_claim_does_not_consists_of_the_value() + { + $service = $this->service(); + + $service->addRule('iss', new ConsistsOf('Company')); + + $this->expectException(ValidationException::class); + + $service->validate(['iss' => 'My Corporate']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_equals_to_it_should_pass_when_the_claim_equals_to_the_value() + { + $service = $this->service(); + + $service->addRule('aud', new EqualsTo('Customer')); + + $service->validate(['aud' => 'Customer']); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_equals_to_it_should_fail_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('aud', new EqualsTo('Customer')); + + $this->expectException(ValidationException::class); + + $service->validate(); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_equals_to_it_should_fail_when_the_claim_does_not_equals_to_the_value() + { + $service = $this->service(); + + $service->addRule('aud', new EqualsTo('Customer')); + + $this->expectException(ValidationException::class); + + $service->validate(['aud' => 'Other Customer']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_greater_than_it_should_pass_when_the_claim_greater_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThan(100)); + + $service->validate(['sub' => 99]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_greater_than_it_should_fail_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThan(100)); + + $this->expectException(ValidationException::class); + + $service->validate(); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_greater_than_it_should_fail_when_the_claim_is_not_greater_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThan(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => '1001']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_greater_than_it_should_fail_when_the_claim_equals_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThan(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => 1000]); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_gt_or_equal_to_it_should_pass_when_the_claim_greater_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThanOrEqualTo(100)); + + $service->validate(['sub' => 99]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_gt_or_equal_to_it_should_fail_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThanOrEqualTo(100)); + + $this->expectException(ValidationException::class); + + $service->validate(); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_gt_or_equal_to_it_should_fail_when_the_claim_is_not_greater_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThanOrEqualTo(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => '1001']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_gt_or_equal_to_it_should_pass_when_the_claim_equals_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new GreaterThanOrEqualTo(1000)); + + $service->validate(['sub' => 1000]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_identical_to_it_should_pass_when_the_claim_is_identical_to_int_value() + { + $service = $this->service(); + + $service->addRule('num', new IdenticalTo(666)); + + $service->validate(['num' => 666]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_identical_to_it_should_pass_when_the_claim_is_identical_to_bool_value() + { + $service = $this->service(); + + $service->addRule('is', new IdenticalTo(true)); + + $service->validate(['is' => true]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_identical_to_it_should_fail_when_the_claim_is_does_not_exist() + { + $service = $this->service(); + + $service->addRule('num', new IdenticalTo(666)); + + $this->expectException(ValidationException::class); + + $service->validate(); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_identical_to_it_should_fail_when_the_claim_is_not_identical_to_int_value() + { + $service = $this->service(); + + $service->addRule('num', new IdenticalTo(666)); + + $this->expectException(ValidationException::class); + + $service->validate(['num' => '666']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_identical_to_it_should_fail_when_the_claim_is_not_identical_to_bool_value() + { + $service = $this->service(); + + $service->addRule('is', new IdenticalTo(true)); + + $this->expectException(ValidationException::class); + + $service->validate(['is' => 1]); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_less_than_it_should_pass_when_the_claim_less_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThan(100)); + + $service->validate(['sub' => 101]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_less_than_it_should_fail_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('sub', new LessThan(100)); + + $this->expectException(ValidationException::class); + + $service->validate(); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_less_than_it_should_fail_when_the_claim_is_not_less_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThan(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => '999']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_less_than_it_should_fail_when_the_claim_equals_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThan(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => 1000]); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_lt_or_equal_to_it_should_pass_when_the_claim_less_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThanOrEqualTo(100)); + + $service->validate(['sub' => 101]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_lt_or_equal_to_it_should_fail_when_the_claim_does_not_exist() + { + $service = $this->service(); + + $service->addRule('sub', new LessThanOrEqualTo(100)); + + $this->expectException(ValidationException::class); + + $service->validate(); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_lt_or_equal_to_it_should_fail_when_the_claim_is_not_less_than_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThanOrEqualTo(1000)); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => '999']); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_lt_or_equal_to_it_should_pass_when_the_claim_equals_the_value() + { + $service = $this->service(); + + $service->addRule('sub', new LessThanOrEqualTo(1000)); + + $service->validate(['sub' => 1000]); + + $this->assertTrue(true); + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..164c9b8 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,14 @@ + + * Date: 5/14/2018 AD + * Time: 00:56 + */ + +namespace MiladRahimi\Jwt\Tests; + +abstract class TestCase extends \PHPUnit\Framework\TestCase +{ + // Nada! +} \ No newline at end of file diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php new file mode 100644 index 0000000..c1831b6 --- /dev/null +++ b/tests/ValidationTest.php @@ -0,0 +1,107 @@ + + * Date: 5/20/2018 AD + * Time: 00:34 + */ + +namespace MiladRahimi\Jwt\Tests; + +use MiladRahimi\Jwt\Exceptions\ValidationException; +use MiladRahimi\Jwt\Validator\Rules\Optional\NewerThanOrSameTimeWith; +use MiladRahimi\Jwt\Validator\Rules\Optional\OlderThan; +use MiladRahimi\Jwt\Validator\Rules\Required\Exists; +use MiladRahimi\Jwt\Validator\Rules\Required\NotNull; +use MiladRahimi\Jwt\Validator\Validator; +use MiladRahimi\Jwt\Validator\ValidatorInterface; + +class ValidationTest extends TestCase +{ + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_single_rule_validation_it_should_pass_when_the_rule_is_obeyed() + { + $service = $this->service(); + + $service->addRule('exp', new Exists()); + + $service->validate(['exp' => time()]); + + $this->assertTrue(true); + } + + public function service(): ValidatorInterface + { + return new Validator(); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_single_rule_validation_it_should_fail_when_the_rule_is_broken() + { + $service = $this->service(); + + $service->addRule('exp', new Exists()); + + $this->expectException(ValidationException::class); + + $service->validate(['iat' => time()]); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_validation_with_multiple_rules_it_should_pass_when_all_the_rule_are_obeyed() + { + $service = $this->service(); + + $service->addRule('nbf', new Exists()); + $service->addRule('nbf', new NewerThanOrSameTimeWith(time())); + $service->addRule('sub', new NotNull()); + + $service->validate(['sub' => 1, 'nbf' => time()]); + + $this->assertTrue(true); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_validation_with_multiple_rules_it_should_fail_when_at_least_one_of_them_is_broken() + { + $service = $this->service(); + + $service->addRule('nbf', new Exists()); + $service->addRule('nbf', new OlderThan(time())); + $service->addRule('sub', new NotNull()); + + $this->expectException(ValidationException::class); + + $service->validate(['sub' => 1, 'nbf' => time() - 1000]); + + $this->assertTrue(false); + } + + /** + * @throws \MiladRahimi\Jwt\Exceptions\ValidationException + */ + public function test_clean_rules_it_should_clean_rules() + { + $service = $this->service(); + + $service->addRule('nbf', new Exists()); + $service->addRule('nbf', new OlderThan(time())); + $service->addRule('sub', new NotNull()); + + $service->cleanRules('nbf'); + + $service->validate(['sub' => 1]); + + $this->assertTrue(true); + } +} \ No newline at end of file diff --git a/tests/keys/private.pem b/tests/keys/private.pem new file mode 100644 index 0000000..970d274 --- /dev/null +++ b/tests/keys/private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCyPKQtQcsiRIoe +ZstmN7VIoWhbi62f/UCWEkULLpxfRyleQn5KyOyhUxfrIz2TXr4Tlo0843SsSGZd +3A6ZaJc3XMu54zGbJ20k6sprYv7vtw9txddrL6aRgt4qGk/u9xS6EnunhMOohCsL +jALeAAWlvkX1DRbc8DBlXKp7Nslvsf8qooUaYDBJMzqwSQhgRZxWFhIF/7UiLHfW +qswDrXtcYkoH2l/AS+NUY80fNgJveK2CbCKnOvgBiHqAcalH/aPY3PWap19heT13 +A2zkcbL5vBnUAUxZTJdAONoVC2B8jXI4okmN5RUcNciiNKjC2sh2+rpNm1Z57aUD +JTNQbUg1AgMBAAECggEALwGXOuhTLmJtGr95fSfMA6+t2hboX31m/y0qUFymmOon +mthmfkqnPZSwhi7xGmCuOHRII3rJVnZxqOdUYlkXKF6szAWDG6w2OLpDUDb3jTj2 +NLksqvQSNeYRuYDe++Ll8HzXZ8K350WUJKN0TiArXR7lTE97bCFcYh8iRwSX84DJ +Hl0+JiuaQjjo1/DrNRlcT2X0jCDyk6kBONcu0ApSshvLKzz223oFhrF2yep8Y3Tn +3ogfwM2wboaqfldM3GT+aA9Y+TaSRRFKE5Pcqn6Ngh69vdQrkZIMQuH3pnVKetOD +KDD5kvs4skbcqyF33MVkrrtLR0RXxS9sJTXbNdE8sQKBgQDk369LLE2OEhcuNFKB +v1aByZF9jj6vUZeb/m1TA6lkNsT0qx+Vqyr+Gch9pSQk76J/HAeaUx7eFv6mA8Hc ++jX2y8LQ20OJtmlZ/Jf/dV+xHYwm8F+/cqn6kfIfamjbRgomco2d6Mhky93WJ3K6 +QhyMLssrb1ePfVmcpNx+REeIDwKBgQDHXJH/KsYF1tkOTukH0DQamM6yakh6Kf08 +gY7SacCg4u0tcfAFJNDH1SJPIWtpDZeRldk2YNOB1GTYgDcCGnIkpv7+aE5G5Ey/ +Fq5AulLhj7KWVtXX8HIuaTjjLyc3a/tqpSNlf7diNL3deiIpTPGjmrcVBX2sqLLS +FwqNasGHewKBgFbkOVfP1Q3X/mbzRFOwAba6gPrq8w96aZW2rWiz3i6GwJWnFhGr +b2ITBdP1y1gehlG9oYFMh06H6lu613H+qFgvGaJTbDuvPiYEyOwacp76ecgawC8d +6DySBhs5Od/tolLgcLV/t/zUjT8NsPfXu6DY7xdpaRO34jqfOHrTj1ivAoGAIkSO +L98pVJ2eh7AWrCokmqHDfbV5Kc6H4dufMhnNa5o2DIa2LBws6vx+vj1PWipk8Dhi +ss8n6/wXLYO0cN8c2aH+2LoTImphqsL6RtQAJRdvKhpY/Zot9j5N8fcL9aHRAH7M +O8kdO3s3W5d/wbuhGW8hozJjbMc+nZAqVy6Pf3sCgYAUgo8Mir46rejIHzllujw5 +Cozoo0Zn3XqBCIMbQNVXJmZrcG6xO9AkmxYAEyGqPJqA2cWG8k3yB3wOGr0Rx7tg +5A+HrwufcfXdhJxufby40JJLJ8wCXgy1U0NjZtlW8x9cdJ8KCCR+8pVxACdc3Ik9 ++f1N64VtbOI3+11XeR4DNA== +-----END PRIVATE KEY----- diff --git a/tests/keys/public.pem b/tests/keys/public.pem new file mode 100644 index 0000000..c69d660 --- /dev/null +++ b/tests/keys/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjykLUHLIkSKHmbLZje1 +SKFoW4utn/1AlhJFCy6cX0cpXkJ+SsjsoVMX6yM9k16+E5aNPON0rEhmXdwOmWiX +N1zLueMxmydtJOrKa2L+77cPbcXXay+mkYLeKhpP7vcUuhJ7p4TDqIQrC4wC3gAF +pb5F9Q0W3PAwZVyqezbJb7H/KqKFGmAwSTM6sEkIYEWcVhYSBf+1Iix31qrMA617 +XGJKB9pfwEvjVGPNHzYCb3itgmwipzr4AYh6gHGpR/2j2Nz1mqdfYXk9dwNs5HGy ++bwZ1AFMWUyXQDjaFQtgfI1yOKJJjeUVHDXIojSowtrIdvq6TZtWee2lAyUzUG1I +NQIDAQAB +-----END PUBLIC KEY-----