11 Commits
0.1.0 ... 0.2.2

Author SHA1 Message Date
f83998e6c7 Additional checks for validity and in validator
* Properly check nbf and exp claims in token to determine simple
  validity.
* Properly check nbf and exp claims in validator and throw exceptions
  if expired/not yet valid.
2024-03-11 23:34:19 +01:00
369514589f Added woodpecker config
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-21 01:46:52 +02:00
b0566e3148 Fixed case in test filenames 2023-04-10 00:55:15 +02:00
c5dd773026 Added docblocks 2023-04-10 00:53:51 +02:00
5c422226fd Fixed test @covers annotations 2023-04-10 00:43:42 +02:00
e30ded1e66 Added changelog 2023-04-09 16:04:34 +02:00
822b796d40 phpstan fixes 2023-04-09 14:12:48 +02:00
b9c690cb6e Fixed readme 2023-04-09 02:46:48 +02:00
fabf160346 Useless merge 2023-04-09 02:44:23 +02:00
953e831d84 Fixed capitalization, tests 2023-04-09 02:40:21 +02:00
5a4a2845e4 Update 'README.md'
Fixed code blocks
2022-12-29 01:18:13 +00:00
32 changed files with 552 additions and 378 deletions

7
.woodpecker.yml Normal file
View File

@ -0,0 +1,7 @@
pipeline:
phpunit:
image: walkero/phpunit-alpine:php8.1-phpunit9
commands:
- composer install
- vendor/bin/phpunit --testdox --no-progress
- vendor/bin/phpstan --no-progress

14
CHANGELOG.md Normal file
View File

@ -0,0 +1,14 @@
# SimpleJWT ChangeLog
## 0.2.1
- Mostly code cleanup
## 0.2.0
- Class- and filenames have had their case changed (`Jwt``JWT`)
- Added phpstan for static analysis
## 0.1.0
- Initial release

View File

@ -1,4 +1,4 @@
# SimpleJwt # SimpleJWT
This is a simple library for generating (signing) and verifying JWT tokens. It This is a simple library for generating (signing) and verifying JWT tokens. It
is by no means an advanced library. If you just need to sign and refresh tokens is by no means an advanced library. If you just need to sign and refresh tokens
@ -27,69 +27,77 @@ Install using composer:
## Usage ## Usage
You need a key for both generating and parsing tokens. Create a `JwtDerivedKey` You need a key for both generating and parsing tokens. Create a `JWTDerivedKey`
or a `JwtPlaintextKey` and pass it to the `JwtToken` constructor: or a `JWTPlaintextKey` and pass it to the `JWTToken` constructor:
use NoccyLabs\SimpleJwt\Key\{JwtDerivedKey,JwtPlaintextKey} ```php
use NoccyLabs\SimpleJWT\Key\{JWTDerivedKey,JWTPlaintextKey}
// Derive a key using secret and salt... // Derive a key using secret and salt...
$key = new JwtDerivedKey("secret", "salt"); $key = new JWTDerivedKey("secret", "salt");
// ...or use a prepared plaintext key // ...or use a prepared plaintext key
$key = new JwtPlaintextKey("This Should Be Binary Data.."); $key = new JWTPlaintextKey("This Should Be Binary Data..");
```
`JWTDerivedKey` uses hash_pbkdf2.
### Generating tokens ### Generating tokens
```php
use NoccyLabs\SimpleJWT\JWTToken;
use NoccyLabs\SimpleJwt\JwtToken; $tok = new JWTToken($key);
$tok->setExpiry("1h");
$tok = new JwtToken($key); $tok->claims->add("some/claim/MaxItems", 8);
$tok->setExpiry("1h");
$tok->claims->add("some/claim/MaxItems", 8);
$str = $tok->getSignedToken();
$str = $tok->getSignedToken();
```
### Parsing tokens ### Parsing tokens
Parsing is done by passing the raw token as the 2nd parameter Parsing is done by passing the raw token as the 2nd parameter
use NoccyLabs\SimpleJwt\JwtToken; ```php
use NoccyLabs\SimpleJWT\JWTToken;
$str = "...received token..."; $str = "...received token...";
$tok = new JwtToken($key, $str); $tok = new JWTToken($key, $str);
if (!$tok->isValid()) { if (!$tok->isValid()) {
// This check works, but using the validator might be better // This check works, but using the validator might be better
} }
// Using ->has() follwed by ->get() is one way // Using ->has() follwed by ->get() is one way
if ($tok->claims->has("some/claim/MaxItems")) { if ($tok->claims->has("some/claim/MaxItems")) {
// The claim exists, we can get the value (if any) // The claim exists, we can get the value (if any)
$val = $tok->claims->get("some/claim/MaxItems"); $val = $tok->claims->get("some/claim/MaxItems");
} }
// You can also use valueOf() to return a default value if needed // You can also use valueOf() to return a default value if needed
$val = $tok->claims->valueOf("some/claim/MaxItems", 64); $val = $tok->claims->valueOf("some/claim/MaxItems", 64);
```
### Validating tokens ### Validating tokens
use NoccyLabs\SimpleJwt\Validator\JwtValidator; ```php
use NoccyLabs\SimpleJWT\Validator\JWTValidator;
$validator = new JwtValidator(); $validator = new JWTValidator();
// Require that some claim exists // Require that some claim exists
$validator $validator
->requireIssuer("api.issuer.tld") ->requireIssuer("api.issuer.tld")
->requireAudience(["api.issuer.tld", "foo.issuer.tld"]) ->requireAudience(["api.issuer.tld", "foo.issuer.tld"])
->addRequiredClaim("some/required/Claim"); ->addRequiredClaim("some/required/Claim");
try { try {
// Pass a JwtToken to validateToken()... // Pass a JWTToken to validateToken()...
$valid = $validator->validateToken($tok); $valid = $validator->validateToken($tok);
// ...or pass a JwtKeyInterface and the raw string to validate() // ...or pass a JWTKeyInterface and the raw string to validate()
$valid = $validator->validate($key, $tokenstr); $valid = $validator->validate($key, $tokenstr);
} }
catch (JwtValidatorException $e) { catch (JWTValidatorException $e) {
// validation failed // validation failed
} }
```

View File

@ -11,7 +11,15 @@
], ],
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"NoccyLabs\\SimpleJwt\\": "src/" "NoccyLabs\\SimpleJWT\\": "src/"
} }
},
"require": {
"php": "^7.4|^8.0",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"phpstan/phpstan": "^1.10"
} }
} }

12
phpstan.neon Normal file
View File

@ -0,0 +1,12 @@
parameters:
level: 5
excludePaths:
- doc
- vendor
- tests
# Paths to include in the analysis
paths:
- src

View File

@ -1,22 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd" bootstrap="vendor/autoload.php" executionOrder="depends,defects" beStrictAboutOutputDuringTests="true" cacheDirectory=".phpunit.cache" requireCoverageMetadata="true" beStrictAboutCoverageMetadata="true">
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd" <coverage>
bootstrap="vendor/autoload.php" <include>
executionOrder="depends,defects" <directory suffix=".php">src</directory>
forceCoversAnnotation="true" </include>
beStrictAboutCoversAnnotation="true" </coverage>
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
verbose="true">
<testsuites> <testsuites>
<testsuite name="default"> <testsuite name="default">
<directory suffix="Test.php">tests</directory> <directory suffix="Test.php">tests</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit> </phpunit>

View File

@ -1,6 +1,6 @@
<?php <?php
namespace NoccyLabs\SimpleJwt\Collection; namespace NoccyLabs\SimpleJWT\Collection;
use ArrayAccess; use ArrayAccess;
use Countable; use Countable;
@ -21,8 +21,8 @@ class PropertyBag
/** /**
* Add a property value, fails if the property exists * Add a property value, fails if the property exists
* *
* @param string Property name * @param string $prop Property name
* @param mixed Value * @param mixed $value Value
* @throws PropertyException if the property already exists * @throws PropertyException if the property already exists
*/ */
public function add(string $prop, $value) public function add(string $prop, $value)
@ -37,14 +37,19 @@ class PropertyBag
* Set a property value, create the property if it doesn't * Set a property value, create the property if it doesn't
* exist. * exist.
* *
* @param string Property name * @param string $prop Property name
* @param mixed Value * @param mixed $value Value
*/ */
public function set(string $prop, $value) public function set(string $prop, $value)
{ {
$this->props[$prop] = $value; $this->props[$prop] = $value;
} }
/**
* Apply properties without removing anything.
*
* @param array $props The properties to apply
*/
public function setAll(array $props) public function setAll(array $props)
{ {
$this->props = array_merge( $this->props = array_merge(
@ -57,7 +62,7 @@ class PropertyBag
* Get the value of a property, fails if the property does not exist. * Get the value of a property, fails if the property does not exist.
* Use the value() method to get with a default value * Use the value() method to get with a default value
* *
* @param string Property name * @param string $prop Property name
* @return mixed * @return mixed
* @throws PropertyException if the property does not exist * @throws PropertyException if the property does not exist
*/ */
@ -92,8 +97,8 @@ class PropertyBag
/** /**
* Get the value of the property, or use the provided default value. * Get the value of the property, or use the provided default value.
* *
* @param string Property name * @param string $prop Property name
* @param mixed Default value * @param mixed|null $default Default value
* @return mixed * @return mixed
*/ */
public function valueOf(string $prop, $default=null) public function valueOf(string $prop, $default=null)
@ -105,6 +110,8 @@ class PropertyBag
/** /**
* Remove a property * Remove a property
*
* @param string $prop Property name
*/ */
public function delete(string $prop) public function delete(string $prop)
{ {
@ -113,6 +120,8 @@ class PropertyBag
/** /**
* Check if a property is present * Check if a property is present
*
* @param string $prop Property name
*/ */
public function has(string $prop): bool public function has(string $prop): bool
{ {
@ -121,6 +130,8 @@ class PropertyBag
/** /**
* Check if all the provided properties are present * Check if all the provided properties are present
*
* @param array $props Property names
*/ */
public function hasAll(array $props) public function hasAll(array $props)
{ {

View File

@ -1,6 +1,6 @@
<?php <?php
namespace NoccyLabs\SimpleJwt\Collection; namespace NoccyLabs\SimpleJWT\Collection;
class PropertyException extends \RuntimeException class PropertyException extends \RuntimeException

View File

@ -1,19 +1,19 @@
<?php <?php
namespace NoccyLabs\SimpleJwt; namespace NoccyLabs\SimpleJWT;
use NoccyLabs\SimpleJwt\Collection\PropertyBag; use NoccyLabs\SimpleJWT\Collection\PropertyBag;
use NoccyLabs\SimpleJwt\Key\KeyInterface; use NoccyLabs\SimpleJWT\Key\KeyInterface;
/** /**
* *
* *
* *
* *
* @property-read header PropertyBag * @property-read PropertyBag $header
* @property-read claim PropertyBag * @property-read PropertyBag $claims
*/ */
class JwtToken class JWTToken
{ {
/** @var PropertyBag */ /** @var PropertyBag */
private $header; private $header;
@ -30,7 +30,8 @@ class JwtToken
* Constructor * Constructor
* *
* *
* @param KeyInterface The key used to sign the token * @param KeyInterface $key The key used to sign the token
* @param string|null $token Token data
*/ */
public function __construct(KeyInterface $key, ?string $token=null) public function __construct(KeyInterface $key, ?string $token=null)
{ {
@ -54,13 +55,13 @@ class JwtToken
$this->generated = false; $this->generated = false;
[ $header, $payload, $signature ] = explode(".", trim($token), 3); [ $header, $payload, $signature ] = explode(".", trim($token), 3);
$hash = JwtUtil::encode(hash_hmac("sha256", $header.".".$payload, $this->key->getBinaryKey(), true)); $hash = JWTUtil::encode(hash_hmac("sha256", $header.".".$payload, $this->key->getBinaryKey(), true));
if ($signature == $hash) { if ($signature == $hash) {
$this->valid = true; $this->valid = true;
} }
$this->header = new PropertyBag(json_decode(JwtUtil::decode($header), true)); $this->header = new PropertyBag(json_decode(JWTUtil::decode($header), true));
$this->claims = new PropertyBag(json_decode(JwtUtil::decode($payload), true)); $this->claims = new PropertyBag(json_decode(JWTUtil::decode($payload), true));
if ($this->header->has('exp')) { if ($this->header->has('exp')) {
$exp = intval($this->header->get('exp')); $exp = intval($this->header->get('exp'));
@ -69,13 +70,35 @@ class JwtToken
$this->valid = false; $this->valid = false;
} }
} }
if ($this->header->has('nbf')) {
$nbf = intval($this->header->get('nbf'));
if ($nbf >= time()) {
// Invalid if before
$this->valid = false;
}
}
} }
/**
* Returns true if the expiry is not in the past.
*
* NOTE: This function will return true if the expiry header is missing, and
* it will not validate any claims. For actual verification of a token matching
* issuers, audience or other claims, see Validator\JWTValidator.
*
* @return bool True if the token expiry timestamp is missing or in the future
*/
public function isValid(): bool public function isValid(): bool
{ {
return $this->valid; return $this->valid;
} }
/**
* Returns true if the token was generated as opposed to parsed.
*
* @return bool
*/
public function isGenerated(): bool public function isGenerated(): bool
{ {
return $this->generated; return $this->generated;
@ -89,16 +112,44 @@ class JwtToken
} }
} }
/**
* Add a claim to a token. Throws an exception if the claim already exists.
*
* @param string $name The name of the claim
* @param mixed $value Claim value
* @throws \NoccyLabs\SimpleJWT\Collection\PropertyException if the claim already exists.
*/
public function addClaim(string $name, $value) public function addClaim(string $name, $value)
{ {
$this->claims->add($name, $value); $this->claims->add($name, $value);
} }
/**
* Add a claim to a token. If the claim already exists it will be updated with
* the provided value.
*
* @param string $name The name of the claim
* @param mixed $value Claim value
*/
public function setClaim(string $name, $value) public function setClaim(string $name, $value)
{ {
$this->claims->set($name, $value); $this->claims->set($name, $value);
} }
/**
* Set the time of expiry for the token.
*
* The expiry can be supplied as:
* - \DateTime instance
* - Unixtime as an integer
* - A string represening the expiry time
* - A period followed by a letter (m,h,d,w)
* - null, to unset the expiry
*
* @param string|int|\DateTime $expiry
* @return void
* @throws \InvalidArgumentException if the argument can not be interpreted
*/
public function setExpiry($expiry) public function setExpiry($expiry)
{ {
if ($expiry instanceof \DateTime) { if ($expiry instanceof \DateTime) {
@ -127,6 +178,8 @@ class JwtToken
case 'm': case 'm':
$fact = 60; $fact = 60;
break; break;
default:
throw new \InvalidArgumentException();
} }
$this->header->set('exp', time() + (intval($match[1]) * $fact)); $this->header->set('exp', time() + (intval($match[1]) * $fact));
} else { } else {
@ -136,9 +189,9 @@ class JwtToken
public function getSignedToken(): string public function getSignedToken(): string
{ {
$header = JwtUtil::encode($this->header->getJson()); $header = JWTUtil::encode($this->header->getJson());
$payload = JwtUtil::encode($this->claims->getJson()); $payload = JWTUtil::encode($this->claims->getJson());
$hash = JwtUtil::encode(hash_hmac("sha256", $header.".".$payload, $this->key->getBinaryKey(), true)); $hash = JWTUtil::encode(hash_hmac("sha256", $header.".".$payload, $this->key->getBinaryKey(), true));
return $header.".".$payload.".".$hash; return $header.".".$payload.".".$hash;
} }

View File

@ -1,9 +1,9 @@
<?php <?php
namespace NoccyLabs\SimpleJwt; namespace NoccyLabs\SimpleJWT;
class JwtUtil class JWTUtil
{ {
public static function encode($data) { public static function encode($data) {
return rtrim(str_replace(['+', '/'], ['-', '_'], base64_encode($data)), "="); return rtrim(str_replace(['+', '/'], ['-', '_'], base64_encode($data)), "=");

View File

@ -1,8 +1,8 @@
<?php <?php
namespace NoccyLabs\SimpleJwt\Key; namespace NoccyLabs\SimpleJWT\Key;
class JwtDerivedKey implements KeyInterface class JWTDerivedKey implements KeyInterface
{ {
private $key; private $key;

View File

@ -1,8 +1,8 @@
<?php <?php
namespace NoccyLabs\SimpleJwt\Key; namespace NoccyLabs\SimpleJWT\Key;
class JwtPlaintextKey implements KeyInterface class JWTPlaintextKey implements KeyInterface
{ {
private $key; private $key;

View File

@ -1,6 +1,6 @@
<?php <?php
namespace NoccyLabs\SimpleJwt\Key; namespace NoccyLabs\SimpleJWT\Key;
interface KeyInterface interface KeyInterface
{ {

View File

@ -0,0 +1,9 @@
<?php
namespace NoccyLabs\SimpleJWT\Validator;
class JWTClaimException extends JWTValidatorException
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\SimpleJWT\Validator;
class JWTHeaderException extends JWTValidatorException
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\SimpleJWT\Validator;
class JWTTokenException extends JWTValidatorException
{
}

View File

@ -1,11 +1,11 @@
<?php <?php
namespace NoccyLabs\SimpleJwt\Validator; namespace NoccyLabs\SimpleJWT\Validator;
use NoccyLabs\SimpleJwt\JwtToken; use NoccyLabs\SimpleJWT\JWTToken;
use NoccyLabs\SimpleJwt\Key\KeyInterface; use NoccyLabs\SimpleJWT\Key\KeyInterface;
class JwtValidator class JWTValidator
{ {
private $requireHeaders = []; private $requireHeaders = [];
@ -41,32 +41,44 @@ class JwtValidator
$this->requireAudience = (array)$audience; $this->requireAudience = (array)$audience;
} }
public function validateToken(JwtToken $token) public function validateToken(JWTToken $token)
{ {
if (!$token->isValid()) { if (!$token->isValid()) {
throw new JwtTokenException("The token is not valid"); throw new JWTTokenException("The token is not valid");
}
if ($token->claims->has("nbf")) {
$notBefore = intval($token->claims->get("nbf"));
if (time() < $notBefore)
throw new JWTTokenException("Token not yet valid");
}
if ($token->claims->has("exp")) {
$notAfter = intval($token->claims->get("exp"));
if (time() > $notAfter)
throw new JWTTokenException("Token no longer valid");
} }
if (!$token->header->hasAll($this->requireHeaders)) { if (!$token->header->hasAll($this->requireHeaders)) {
throw new JwtHeaderException("The token is missing one or more required headers"); throw new JWTHeaderException("The token is missing one or more required headers");
} }
if (!$token->claims->hasAll($this->requireClaims)) { if (!$token->claims->hasAll($this->requireClaims)) {
throw new JwtHeaderException("The token is missing one or more required claims"); throw new JWTHeaderException("The token is missing one or more required claims");
} }
if ($this->requireIssuer) { if ($this->requireIssuer) {
$hasIssuer = $token->header->has("iss"); $hasIssuer = $token->header->has("iss");
if ((!$hasIssuer) if ((!$hasIssuer)
|| (!in_array($token->header->get("iss"), $this->requireIssuer))) || (!in_array($token->header->get("iss"), $this->requireIssuer)))
throw new JwtTokenException("Invalid issuer"); throw new JWTTokenException("Invalid issuer");
} }
if ($this->requireAudience) { if ($this->requireAudience) {
$hasAudience = $token->header->has("aud"); $hasAudience = $token->header->has("aud");
if ((!$hasAudience) if ((!$hasAudience)
|| (!in_array($token->header->get("aud"), $this->requireAudience))) || (!in_array($token->header->get("aud"), $this->requireAudience)))
throw new JwtTokenException("Invalid audience"); throw new JWTTokenException("Invalid audience");
} }
return true; return true;
@ -74,7 +86,7 @@ class JwtValidator
public function validate(KeyInterface $key, string $raw) public function validate(KeyInterface $key, string $raw)
{ {
$token = new JwtToken($key, $raw); $token = new JWTToken($key, $raw);
if ($this->validateToken($token)) { if ($this->validateToken($token)) {
return $token; return $token;
} }

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\SimpleJWT\Validator;
class JWTValidatorException extends \RuntimeException
{
}

View File

@ -1,9 +0,0 @@
<?php
namespace NoccyLabs\SimpleJwt\Validator;
class JwtClaimException extends JwtValidatorException
{
}

View File

@ -1,8 +0,0 @@
<?php
namespace NoccyLabs\SimpleJwt\Validator;
class JwtHeaderException extends JwtValidatorException
{
}

View File

@ -1,8 +0,0 @@
<?php
namespace NoccyLabs\SimpleJwt\Validator;
class JwtTokenException extends JwtValidatorException
{
}

View File

@ -1,8 +0,0 @@
<?php
namespace NoccyLabs\SimpleJwt\Validator;
class JwtValidatorException extends \RuntimeException
{
}

50
tests/JWTTokenTest.php Normal file
View File

@ -0,0 +1,50 @@
<?php
namespace NoccyLabs\SimpleJWT;
use NoccyLabs\SimpleJWT\Key\JWTPlaintextKey;
class JWTTokenTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers NoccyLabs\SimpleJWT\JWTToken
* @covers NoccyLabs\SimpleJWT\Key\JWTPlaintextKey
* @covers NoccyLabs\SimpleJWT\Collection\PropertyBag
* @covers NoccyLabs\SimpleJWT\JWTUtil
*/
public function testGeneratingTokens()
{
$key = new JWTPlaintextKey("test");
$tok = new JWTToken($key);
$tok->addClaim("foo", true);
$token = $tok->getSignedToken();
$this->assertNotNull($token);
$this->assertTrue($tok->isGenerated());
}
/**
* @covers NoccyLabs\SimpleJWT\JWTToken
* @covers NoccyLabs\SimpleJWT\Key\JWTPlaintextKey
* @covers NoccyLabs\SimpleJWT\Collection\PropertyBag
* @covers NoccyLabs\SimpleJWT\JWTUtil
*/
public function testParsingTokens()
{
$key = new JWTPlaintextKey("test");
$tok = new JWTToken($key);
$tok->addClaim("foo", true);
$token = $tok->getSignedToken();
$parsed = new JWTToken($key, $token);
$this->assertTrue($parsed->isValid());
$this->assertFalse($parsed->isGenerated());
}
}

22
tests/JWTUtilTest.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace NoccyLabs\SimpleJWT;
class JWTUtilTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers \NoccyLabs\SimpleJWT\JWTUtil::encode
* @covers \NoccyLabs\SimpleJWT\JWTUtil::decode
*/
public function testTheEncodingShouldBeSymmetric()
{
$v1a = "HelloWorld";
$v1b = JWTUtil::encode($v1a);
$v1c = JWTUtil::decode($v1b);
$this->assertEquals($v1a, $v1c);
$this->assertNotEquals($v1a, $v1b);
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace NoccyLabs\SimpleJwt;
use NoccyLabs\SimpleJwt\Key\JwtPlaintextKey;
class JwtTokenTest extends \PhpUnit\Framework\TestCase
{
public function testGeneratingTokens()
{
$key = new JwtPlaintextKey("test");
$tok = new JwtToken($key);
$tok->addClaim("foo", true);
$token = $tok->getSignedToken();
$this->assertNotNull($token);
$this->assertTrue($tok->isGenerated());
}
public function testParsingTokens()
{
$key = new JwtPlaintextKey("test");
$tok = new JwtToken($key);
$tok->addClaim("foo", true);
$token = $tok->getSignedToken();
$parsed = new JwtToken($key, $token);
$this->assertTrue($parsed->isValid());
$this->assertFalse($parsed->isGenerated());
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace NoccyLabs\SimpleJwt;
class JwtUtilTest extends \PhpUnit\Framework\TestCase
{
public function testTheEncodingShouldBeSymmetric()
{
$v1a = "HelloWorld";
$v1b = JwtUtil::encode($v1a);
$v1c = JwtUtil::decode($v1b);
$this->assertEquals($v1a, $v1c);
$this->assertNotEquals($v1a, $v1b);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace NoccyLabs\SimpleJWT\Key;
class JWTDerivedKeyTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers \NoccyLabs\SimpleJWT\Key\JWTDerivedKey
*/
public function testTheDerivedKeysShouldBeConsistent()
{
$key1a = new JWTDerivedKey("foo", "foosalt");
$key1b = new JWTDerivedKey("foo", "foosalt");
$this->assertNotNull($key1a);
$this->assertEquals($key1a->getBinaryKey(), $key1b->getBinaryKey());
$key2a = new JWTDerivedKey("bar", "foosalt");
$key2b = new JWTDerivedKey("bar", "barsalt");
$key2c = new JWTDerivedKey("bar", "barsalt");
$this->assertNotNull($key2a);
$this->assertNotEquals($key2a->getBinaryKey(), $key2b->getBinaryKey());
$this->assertEquals($key2b->getBinaryKey(), $key2c->getBinaryKey());
}
/**
* @covers \NoccyLabs\SimpleJWT\Key\JWTDerivedKey
*/
public function testTheDerivedKeysShouldBeUnique()
{
$keys = [];
$keys[] = (new JWTDerivedKey("foo", "foosalt"))->getBinaryKey();
$keys[] = (new JWTDerivedKey("foo", "barsalt"))->getBinaryKey();
$keys[] = (new JWTDerivedKey("foo", "bazsalt"))->getBinaryKey();
$keys[] = (new JWTDerivedKey("bar", "foosalt"))->getBinaryKey();
$keys[] = (new JWTDerivedKey("bar", "barsalt"))->getBinaryKey();
$keys[] = (new JWTDerivedKey("bar", "bazsalt"))->getBinaryKey();
$unique = array_unique($keys);
$this->assertEquals(count($keys), count($unique));
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace NoccyLabs\SimpleJWT\Key;
class JWTPlaintextKeyTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers \NoccyLabs\SimpleJWT\Key\JWTPlaintextKey
*/
public function testThePlaintextKeyShouldBeReturned()
{
$key = new JWTPlaintextKey("foo");
$this->assertEquals("foo", $key->getBinaryKey());
$key = new JWTPlaintextKey("bar");
$this->assertEquals("bar", $key->getBinaryKey());
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace NoccyLabs\SimpleJwt\Key;
class JwtDerivedKeyTest extends \PhpUnit\Framework\TestCase
{
public function testTheDerivedKeysShouldBeConsistent()
{
$key1a = new JwtDerivedKey("foo", "foosalt");
$key1b = new JwtDerivedKey("foo", "foosalt");
$this->assertNotNull($key1a);
$this->assertEquals($key1a->getBinaryKey(), $key1b->getBinaryKey());
$key2a = new JwtDerivedKey("bar", "foosalt");
$key2b = new JwtDerivedKey("bar", "barsalt");
$key2c = new JwtDerivedKey("bar", "barsalt");
$this->assertNotNull($key2a);
$this->assertNotEquals($key2a->getBinaryKey(), $key2b->getBinaryKey());
$this->assertEquals($key2b->getBinaryKey(), $key2c->getBinaryKey());
}
public function testTheDerivedKeysShouldBeUnique()
{
$keys = [];
$keys[] = (new JwtDerivedKey("foo", "foosalt"))->getBinaryKey();
$keys[] = (new JwtDerivedKey("foo", "barsalt"))->getBinaryKey();
$keys[] = (new JwtDerivedKey("foo", "bazsalt"))->getBinaryKey();
$keys[] = (new JwtDerivedKey("bar", "foosalt"))->getBinaryKey();
$keys[] = (new JwtDerivedKey("bar", "barsalt"))->getBinaryKey();
$keys[] = (new JwtDerivedKey("bar", "bazsalt"))->getBinaryKey();
$unique = array_unique($keys);
$this->assertEquals(count($keys), count($unique));
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace NoccyLabs\SimpleJwt\Key;
class JwtPlaintextKeyTest extends \PhpUnit\Framework\TestCase
{
public function testThePlaintextKeyShouldBeReturned()
{
$key = new JwtPlaintextKey("foo");
$this->assertEquals("foo", $key->getBinaryKey());
$key = new JwtPlaintextKey("bar");
$this->assertEquals("bar", $key->getBinaryKey());
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace NoccyLabs\SimpleJWT\Validator;
use NoccyLabs\SimpleJWT\JWTToken;
use NoccyLabs\SimpleJWT\Key\JWTPlaintextKey;
class JWTValidatorTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers \NoccyLabs\SimpleJWT\Key\JWTPlaintextKey
* @covers \NoccyLabs\SimpleJWT\JWTToken
* @covers \NoccyLabs\SimpleJWT\Validator\JWTValidator
* @covers NoccyLabs\SimpleJWT\Collection\PropertyBag
* @covers NoccyLabs\SimpleJWT\JWTUtil
*/
public function testValidTokensShouldPassWithDefaultConfiguration()
{
$key = new JWTPlaintextKey("key");
$token = new JWTToken($key);
$validator = new JWTValidator();
$valid = $validator->validateToken($token);
$this->assertEquals(true, $valid);
}
/**
* @covers \NoccyLabs\SimpleJWT\Validator\JWTValidator
* @covers \NoccyLabs\SimpleJWT\Key\JWTPlaintextKey
* @covers \NoccyLabs\SimpleJWT\JWTToken
* @covers \NoccyLabs\SimpleJWT\Validator\JWTTokenException
* @covers NoccyLabs\SimpleJWT\Collection\PropertyBag
* @covers NoccyLabs\SimpleJWT\JWTUtil
*/
public function testExpiredTokensShouldFailWithException()
{
$key = new JWTPlaintextKey("key");
$token = new JWTToken($key);
$token->header->set("exp", 0);
$token = new JWTToken($key, $token->getSignedToken());
$validator = new JWTValidator();
$this->expectException(JWTTokenException::class);
$valid = $validator->validateToken($token);
}
/**
* @covers \NoccyLabs\SimpleJWT\Validator\JWTValidator
* @covers \NoccyLabs\SimpleJWT\Key\JWTPlaintextKey
* @covers \NoccyLabs\SimpleJWT\JWTToken
* @covers \NoccyLabs\SimpleJWT\Validator\JWTTokenException
* @covers NoccyLabs\SimpleJWT\Collection\PropertyBag
* @covers NoccyLabs\SimpleJWT\JWTUtil
* @dataProvider tokenGenerator
*/
public function testPinningIssuer($issuer,$audience,$key,$token)
{
$goodIssuer = "a-dom.tld";
$jwtKey = new JWTPlaintextKey($key);
$jwtToken = new JWTToken($jwtKey, $token);
$validator = new JWTValidator();
$validator->requireIssuer($goodIssuer);
if ($goodIssuer != $issuer) {
$this->expectException(JWTTokenException::class);
}
$valid = $validator->validateToken($jwtToken);
if ($goodIssuer == $issuer) {
$this->assertTrue($valid);
}
}
/**
* @covers \NoccyLabs\SimpleJWT\Validator\JWTValidator
* @covers \NoccyLabs\SimpleJWT\Key\JWTPlaintextKey
* @covers \NoccyLabs\SimpleJWT\JWTToken
* @covers \NoccyLabs\SimpleJWT\Validator\JWTTokenException
* @covers NoccyLabs\SimpleJWT\Collection\PropertyBag
* @covers NoccyLabs\SimpleJWT\JWTUtil
* @dataProvider tokenGenerator
*/
public function testPinningAudience($issuer,$audience,$key,$token)
{
$goodAudience = [ "a-dom.tld", "app.a-dom.tld" ];
$jwtKey = new JWTPlaintextKey($key);
$jwtToken = new JWTToken($jwtKey, $token);
$validator = new JWTValidator();
$validator->requireAudience($goodAudience);
if (!in_array($audience, $goodAudience)) {
$this->expectException(JWTTokenException::class);
}
$valid = $validator->validateToken($jwtToken);
if (in_array($audience, $goodAudience)) {
$this->assertTrue($valid);
}
}
/**
* @covers \NoccyLabs\SimpleJWT\Validator\JWTValidator
* @covers \NoccyLabs\SimpleJWT\Key\JWTPlaintextKey
* @covers \NoccyLabs\SimpleJWT\JWTToken
* @covers \NoccyLabs\SimpleJWT\Validator\JWTTokenException
* @covers NoccyLabs\SimpleJWT\Collection\PropertyBag
* @covers NoccyLabs\SimpleJWT\JWTUtil
* @dataProvider tokenGenerator
*/
public function testPinningBoth($issuer,$audience,$key,$token)
{
$goodIssuer = "a-dom.tld";
$goodAudience = [ "a-dom.tld", "app.a-dom.tld" ];
$jwtKey = new JWTPlaintextKey($key);
$jwtToken = new JWTToken($jwtKey, $token);
$validator = new JWTValidator();
$validator->requireIssuer($goodIssuer);
$validator->requireAudience($goodAudience);
if (($goodIssuer != $issuer) || (!in_array($audience, $goodAudience))) {
$this->expectException(JWTTokenException::class);
}
$valid = $validator->validateToken($jwtToken);
if (($goodIssuer == $issuer) && (in_array($audience, $goodAudience))) {
$this->assertTrue($valid);
}
}
public static function tokenGenerator()
{
$keyrand = function () {
return substr(sha1(microtime(true).rand(0,65535)), 5, 10);
};
$token = function ($head,$claims,$key) {
$jwtKey = new JWTPlaintextKey($key);
$tok = new JWTToken($jwtKey);
$tok->header->setAll($head);
$tok->claims->setAll($claims);
return $tok->getSignedToken();
};
$row = function ($iss, $aud, array $claims) use ($keyrand, $token) {
$key = $keyrand();
$jwtKey = new JWTPlaintextKey($key);
return [
$iss,
$aud,
$key,
$token(['iss'=>$iss, 'aud'=>$aud], $claims, $key),
];
};
return [
$row("a-dom.tld", "a-dom.tld", []),
$row("b-dom.tld", "a-dom.tld", []),
$row("b-dom.tld", "b-dom.tld", []),
$row("a-dom.tld", "app.a-dom.tld", []),
$row("a-dom.tld", "app.b-dom.tld", []),
$row("", "app.b-dom.tld", []),
];
}
}

View File

@ -1,130 +0,0 @@
<?php
namespace NoccyLabs\SimpleJwt\Validator;
use NoccyLabs\SimpleJwt\JwtToken;
use NoccyLabs\SimpleJwt\Key\JwtPlaintextKey;
class JwtValidatorTest extends \PhpUnit\Framework\TestCase
{
public function testValidTokensShouldPassWithDefaultConfiguration()
{
$key = new JwtPlaintextKey("key");
$token = new JwtToken($key);
$validator = new JwtValidator();
$valid = $validator->validateToken($token);
$this->assertEquals(true, $valid);
}
public function testExpiredTokensShouldFailWithException()
{
$key = new JwtPlaintextKey("key");
$token = new JwtToken($key);
$token->header->set("exp", 0);
$token = new JwtToken($key, $token->getSignedToken());
$validator = new JwtValidator();
$this->expectException(JwtTokenException::class);
$valid = $validator->validateToken($token);
}
/**
* @dataProvider tokenGenerator
*/
public function testPinningIssuer($issuer,$audience,$key,$token)
{
$goodIssuer = "a-dom.tld";
$jwtKey = new JwtPlaintextKey($key);
$jwtToken = new JwtToken($jwtKey, $token);
$validator = new JwtValidator();
$validator->requireIssuer($goodIssuer);
if ($goodIssuer != $issuer) {
$this->expectException(JwtTokenException::class);
}
$valid = $validator->validateToken($jwtToken);
if ($goodIssuer == $issuer) {
$this->assertTrue($valid);
}
}
/**
* @dataProvider tokenGenerator
*/
public function testPinningAudience($issuer,$audience,$key,$token)
{
$goodAudience = [ "a-dom.tld", "app.a-dom.tld" ];
$jwtKey = new JwtPlaintextKey($key);
$jwtToken = new JwtToken($jwtKey, $token);
$validator = new JwtValidator();
$validator->requireAudience($goodAudience);
if (!in_array($audience, $goodAudience)) {
$this->expectException(JwtTokenException::class);
}
$valid = $validator->validateToken($jwtToken);
if (in_array($audience, $goodAudience)) {
$this->assertTrue($valid);
}
}
/**
* @dataProvider tokenGenerator
*/
public function testPinningBoth($issuer,$audience,$key,$token)
{
$goodIssuer = "a-dom.tld";
$goodAudience = [ "a-dom.tld", "app.a-dom.tld" ];
$jwtKey = new JwtPlaintextKey($key);
$jwtToken = new JwtToken($jwtKey, $token);
$validator = new JwtValidator();
$validator->requireIssuer($goodIssuer);
$validator->requireAudience($goodAudience);
if (($goodIssuer != $issuer) || (!in_array($audience, $goodAudience))) {
$this->expectException(JwtTokenException::class);
}
$valid = $validator->validateToken($jwtToken);
if (($goodIssuer == $issuer) && (in_array($audience, $goodAudience))) {
$this->assertTrue($valid);
}
}
public function tokenGenerator()
{
$keyrand = function () {
return substr(sha1(microtime(true).rand(0,65535)), 5, 10);
};
$token = function ($head,$claims,$key) {
$jwtKey = new JwtPlaintextKey($key);
$tok = new JwtToken($jwtKey);
$tok->header->setAll($head);
$tok->claims->setAll($claims);
return $tok->getSignedToken();
};
$row = function ($iss, $aud, array $claims) use ($keyrand, $token) {
$key = $keyrand();
$jwtKey = new JwtPlaintextKey($key);
return [
$iss,
$aud,
$key,
$token(['iss'=>$iss, 'aud'=>$aud], $claims, $key),
];
};
return [
$row("a-dom.tld", "a-dom.tld", []),
$row("b-dom.tld", "a-dom.tld", []),
$row("b-dom.tld", "b-dom.tld", []),
$row("a-dom.tld", "app.a-dom.tld", []),
$row("a-dom.tld", "app.b-dom.tld", []),
$row("", "app.b-dom.tld", []),
];
}
}