From 7753853e58eea0e137085dee3ef759d5625ebca0 Mon Sep 17 00:00:00 2001 From: Christopher Vagnetoft Date: Tue, 16 Feb 2021 18:25:29 +0100 Subject: [PATCH] Added support for validating token issuer and audience * Use requireIssuer() and requireAudience() on the JwtValidator to make sure that the token is for what you expect it to be for. * A setAll() method has been added to property bag, applying but not overriding values. * Added tests for JwtValidator. --- README.md | 5 ++- src/Collection/PropertyBag.php | 10 ++++- src/Validator/JwtValidator.php | 28 ++++++++++++++ tests/Validator/JwtValidatorTest.php | 58 ++++++++++++++++++++++++++-- 4 files changed, 96 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7af598d..34df3d2 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,10 @@ Parsing is done by passing the raw token as the 2nd parameter $validator = new JwtValidator(); // Require that some claim exists - $validator->addRequiredClaim("some/required/Claim"); + $validator + ->requireIssuer("api.issuer.tld") + ->requireAudience(["api.issuer.tld", "foo.issuer.tld"]) + ->addRequiredClaim("some/required/Claim"); try { // Pass a JwtToken to validateToken()... diff --git a/src/Collection/PropertyBag.php b/src/Collection/PropertyBag.php index 19fab0c..a6116b0 100644 --- a/src/Collection/PropertyBag.php +++ b/src/Collection/PropertyBag.php @@ -45,6 +45,14 @@ class PropertyBag $this->props[$prop] = $value; } + public function setAll(array $props) + { + $this->props = array_merge( + $this->props, + $props + ); + } + /** * Get the value of a property, fails if the property does not exist. * Use the value() method to get with a default value @@ -122,4 +130,4 @@ class PropertyBag return true; } -} \ No newline at end of file +} diff --git a/src/Validator/JwtValidator.php b/src/Validator/JwtValidator.php index 407f8ba..d98a125 100644 --- a/src/Validator/JwtValidator.php +++ b/src/Validator/JwtValidator.php @@ -11,6 +11,10 @@ class JwtValidator private $requireClaims = []; + private $requireIssuer = []; + + private $requireAudience = []; + public function __construct() { $this->requireHeaders = [ @@ -27,6 +31,16 @@ class JwtValidator $this->requireClaims[] = $name; } + public function requireIssuer($issuer) + { + $this->requireIssuer = (array)$issuer; + } + + public function requireAudience($audience) + { + $this->requireAudience = (array)$audience; + } + public function validateToken(JwtToken $token) { if (!$token->isValid()) { @@ -41,6 +55,20 @@ class JwtValidator throw new JwtHeaderException("The token is missing one or more required claims"); } + if ($this->requireIssuer) { + $hasIssuer = $token->header->has("iss"); + if ((!$hasIssuer) + || (!in_array($token->header->get("iss"), $this->requireIssuer))) + throw new JwtTokenException("Invalid issuer"); + } + + if ($this->requireAudience) { + $hasAudience = $token->header->has("aud"); + if ((!$hasAudience) + || (!in_array($token->header->get("aud"), $this->requireAudience))) + throw new JwtTokenException("Invalid audience"); + } + return true; } diff --git a/tests/Validator/JwtValidatorTest.php b/tests/Validator/JwtValidatorTest.php index 1dffb8a..50a5601 100644 --- a/tests/Validator/JwtValidatorTest.php +++ b/tests/Validator/JwtValidatorTest.php @@ -8,7 +8,7 @@ use NoccyLabs\SimpleJwt\Key\JwtPlaintextKey; class JwtValidatorTest extends \PhpUnit\Framework\TestCase { - public function testValidKeysShouldPassWithDefaultConfiguration() + public function testValidTokensShouldPassWithDefaultConfiguration() { $key = new JwtPlaintextKey("key"); $token = new JwtToken($key); @@ -18,7 +18,7 @@ class JwtValidatorTest extends \PhpUnit\Framework\TestCase $this->assertEquals(true, $valid); } - public function testExpiredKeysShouldFailWithException() + public function testExpiredTokensShouldFailWithException() { $key = new JwtPlaintextKey("key"); $token = new JwtToken($key); @@ -31,4 +31,56 @@ class JwtValidatorTest extends \PhpUnit\Framework\TestCase $valid = $validator->validateToken($token); } -} \ No newline at end of file + /** + * @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); + } + } + // public function testPinningAudience() + + 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", []), + ]; + + } + +}