diff --git a/README.md b/README.md index e35b1722..11ac68b2 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,13 @@ Just use the builder to create a new JWT/JWS tokens: ```php use Lcobucci\JWT\Builder; +$time = time(); $token = (new Builder())->issuedBy('http://example.com') // Configures the issuer (iss claim) ->canOnlyBeUsedBy('http://example.org') // Configures the audience (aud claim) ->identifiedBy('4f1g23a12aa', true) // Configures the id (jti claim), replicating as a header item - ->issuedAt(time()) // Configures the time that the token was issue (iat claim) - ->canOnlyBeUsedAfter(time() + 60) // Configures the time that the token can be used (nbf claim) - ->expiresAt(time() + 3600) // Configures the expiration time of the token (exp claim) + ->issuedAt($time) // Configures the time that the token was issue (iat claim) + ->canOnlyBeUsedAfter($time + 60) // Configures the time that the token can be used (nbf claim) + ->expiresAt($time + 3600) // Configures the expiration time of the token (exp claim) ->with('uid', 1) // Configures a new claim, called "uid" ->getToken(); // Retrieves the generated token @@ -69,7 +70,7 @@ echo $token->getClaim('uid'); // will print "1" ### Validating -We can easily validate if the token is valid (using the previous token as example): +We can easily validate if the token is valid (using the previous token and time as example): ```php use Lcobucci\JWT\ValidationData; @@ -79,15 +80,37 @@ $data->setIssuer('http://example.com'); $data->setAudience('http://example.org'); $data->setId('4f1g23a12aa'); -var_dump($token->validate($data)); // false, because token cannot be used before of now() + 60 +var_dump($token->validate($data)); // false, because token cannot be used before now() + 60 -$data->setCurrentTime(time() + 61); // changing the validation time to future +$data->setCurrentTime($time + 61); // changing the validation time to future var_dump($token->validate($data)); // true, because current time is between "nbf" and "exp" claims -$data->setCurrentTime(time() + 4000); // changing the validation time to future +$data->setCurrentTime($time + 4000); // changing the validation time to future var_dump($token->validate($data)); // false, because token is expired since current time is greater than exp + +// We can also use the $leeway parameter to deal with clock skew (see notes below) +// If token's claimed time is invalid but the difference between that and the validation time is less than $leeway, +// then token is still considered valid +$dataWithLeeway = new ValidationData($time, 20); +$dataWithLeeway->setIssuer('http://example.com'); +$dataWithLeeway->setAudience('http://example.org'); +$dataWithLeeway->setId('4f1g23a12aa'); + +var_dump($token->validate($dataWithLeeway)); // false, because token can't be used before now() + 60, not within leeway + +$dataWithLeeway->setCurrentTime($time + 51); // changing the validation time to future + +var_dump($token->validate($dataWithLeeway)); // true, because current time plus leeway is between "nbf" and "exp" claims + +$dataWithLeeway->setCurrentTime($time + 3610); // changing the validation time to future but within leeway + +var_dump($token->validate($dataWithLeeway)); // true, because current time - 20 seconds leeway is less than exp + +$dataWithLeeway->setCurrentTime($time + 4000); // changing the validation time to future outside of leeway + +var_dump($token->validate($dataWithLeeway)); // false, because token is expired since current time is greater than exp ``` #### Important @@ -97,6 +120,11 @@ var_dump($token->validate($data)); // false, because token is expired since curr configured in ```ValidationData``` they will be ignored by ```Token::validate()```. - ```exp```, ```nbf``` and ```iat``` claims are configured by default in ```ValidationData::__construct()``` with the current UNIX time (```time()```). +- The optional ```$leeway``` parameter of ```ValidationData``` will cause us to use that number of seconds of leeway +when validating the time-based claims, pretending we are further in the future for the "Issued At" (```iat```) and "Not +Before" (```nbf```) claims and pretending we are further in the past for the "Expiration Time" (```exp```) claim. This +allows for situations where the clock of the issuing server has a different time than the clock of the verifying server, +as mentioned in [section 4.1 of RFC 7519](https://tools.ietf.org/html/rfc7519#section-4.1). ## Token signature @@ -119,13 +147,14 @@ use Lcobucci\JWT\Builder; use Lcobucci\JWT\Signer\Hmac\Sha256; $signer = new Sha256(); +$time = time(); $token = (new Builder())->issuedBy('http://example.com') // Configures the issuer (iss claim) ->canOnlyBeUsedBy('http://example.org') // Configures the audience (aud claim) ->identifiedBy('4f1g23a12aa', true) // Configures the id (jti claim), replicating as a header item - ->issuedAt(time()) // Configures the time that the token was issue (iat claim) - ->canOnlyBeUsedAfter(time() + 60) // Configures the time that the token can be used (nbf claim) - ->expiresAt(time() + 3600) // Configures the expiration time of the token (exp claim) + ->issuedAt($time) // Configures the time that the token was issue (iat claim) + ->canOnlyBeUsedAfter($time + 60) // Configures the time that the token can be used (nbf claim) + ->expiresAt($time + 3600) // Configures the expiration time of the token (exp claim) ->with('uid', 1) // Configures a new claim, called "uid" ->sign($signer, 'testing') // creates a signature using "testing" as key ->getToken(); // Retrieves the generated token @@ -146,13 +175,14 @@ use Lcobucci\JWT\Signer\Rsa\Sha256; // you can use Lcobucci\JWT\Signer\Ecdsa\Sha $signer = new Sha256(); $privateKey = new Key('file://{path to your private key}'); +$time = time(); $token = (new Builder())->issuedBy('http://example.com') // Configures the issuer (iss claim) ->canOnlyBeUsedBy('http://example.org') // Configures the audience (aud claim) ->identifiedBy('4f1g23a12aa', true) // Configures the id (jti claim), replicating as a header item - ->issuedAt(time()) // Configures the time that the token was issue (iat claim) - ->canOnlyBeUsedAfter(time() + 60) // Configures the time that the token can be used (nbf claim) - ->expiresAt(time() + 3600) // Configures the expiration time of the token (exp claim) + ->issuedAt($time) // Configures the time that the token was issue (iat claim) + ->canOnlyBeUsedAfter($time + 60) // Configures the time that the token can be used (nbf claim) + ->expiresAt($time + 3600) // Configures the expiration time of the token (exp claim) ->with('uid', 1) // Configures a new claim, called "uid" ->sign($signer, $privateKey) // creates a signature using your private key ->getToken(); // Retrieves the generated token diff --git a/src/Builder.php b/src/Builder.php index 069813d3..a30e1bed 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -268,7 +268,7 @@ public function setSubject($subject, $replicateAsHeader = false) } /** - * Configures a registed claim + * Configures a registered claim * * @param string $name * @param mixed $value diff --git a/src/ValidationData.php b/src/ValidationData.php index 3aca00b6..3d06c251 100644 --- a/src/ValidationData.php +++ b/src/ValidationData.php @@ -24,24 +24,31 @@ class ValidationData */ private $items; + /** + * The leeway (in seconds) to use when validating time claims + * @var int + */ + private $leeway; + /** * Initializes the object * * @param int $currentTime + * @param int $leeway */ - public function __construct($currentTime = null) + public function __construct($currentTime = null, $leeway = 0) { - $currentTime = $currentTime ?: time(); + $currentTime = $currentTime ?: time(); + $this->leeway = (int) $leeway; $this->items = [ 'jti' => null, 'iss' => null, 'aud' => null, - 'sub' => null, - 'iat' => $currentTime, - 'nbf' => $currentTime, - 'exp' => $currentTime + 'sub' => null ]; + + $this->setCurrentTime($currentTime); } /** @@ -91,9 +98,11 @@ public function setSubject($subject) */ public function setCurrentTime($currentTime) { - $this->items['iat'] = (int) $currentTime; - $this->items['nbf'] = (int) $currentTime; - $this->items['exp'] = (int) $currentTime; + $currentTime = (int) $currentTime; + + $this->items['iat'] = $currentTime + $this->leeway; + $this->items['nbf'] = $currentTime + $this->leeway; + $this->items['exp'] = $currentTime - $this->leeway; } /** diff --git a/test/functional/UnsignedTokenTest.php b/test/functional/UnsignedTokenTest.php index 089cb5da..83366ece 100644 --- a/test/functional/UnsignedTokenTest.php +++ b/test/functional/UnsignedTokenTest.php @@ -118,6 +118,30 @@ public function tokenValidationShouldReturnFalseWhenExpectedDataDontMatch(Valida $this->assertFalse($generated->validate($data)); } + /** + * @test + * + * @depends builderCanGenerateAToken + * + * @covers Lcobucci\JWT\Builder + * @covers Lcobucci\JWT\Parser + * @covers Lcobucci\JWT\Token + * @covers Lcobucci\JWT\ValidationData + * @covers Lcobucci\JWT\Claim\Factory + * @covers Lcobucci\JWT\Claim\Basic + * @covers Lcobucci\JWT\Claim\EqualsTo + * @covers Lcobucci\JWT\Claim\GreaterOrEqualsTo + * @covers Lcobucci\JWT\Parsing\Encoder + * @covers Lcobucci\JWT\Parsing\Decoder + */ + public function tokenValidationShouldReturnTrueWhenExpectedDataMatchBecauseOfLeeway(Token $generated) + { + $notExpiredDueToLeeway = new ValidationData(self::CURRENT_TIME + 3020, 50); + $notExpiredDueToLeeway->setAudience('http://client.abc.com'); + $notExpiredDueToLeeway->setIssuer('http://api.abc.com'); + $this->assertTrue($generated->validate($notExpiredDueToLeeway)); + } + public function invalidValidationData() { $expired = new ValidationData(self::CURRENT_TIME + 3020); diff --git a/test/unit/TokenTest.php b/test/unit/TokenTest.php index d3fa01cd..52371d63 100644 --- a/test/unit/TokenTest.php +++ b/test/unit/TokenTest.php @@ -364,6 +364,40 @@ public function validateShouldReturnFalseWhenThereIsAtLeastOneFailedValidatableC $this->assertFalse($token->validate($data)); } + /** + * @test + * + * @uses Lcobucci\JWT\Token::__construct + * @uses Lcobucci\JWT\ValidationData + * @uses Lcobucci\JWT\Claim\Basic + * @uses Lcobucci\JWT\Claim\EqualsTo + * @uses Lcobucci\JWT\Claim\LesserOrEqualsTo + * @uses Lcobucci\JWT\Claim\GreaterOrEqualsTo + * + * @covers Lcobucci\JWT\Token::validate + * @covers Lcobucci\JWT\Token::getValidatableClaims + */ + public function validateShouldReturnFalseWhenATimeBasedClaimFails() + { + $now = time(); + + $token = new Token( + [], + [ + 'iss' => new EqualsTo('iss', 'test'), + 'iat' => new LesserOrEqualsTo('iat', $now), + 'nbf' => new LesserOrEqualsTo('nbf', $now + 20), + 'exp' => new GreaterOrEqualsTo('exp', $now + 500), + 'testing' => new Basic('testing', 'test') + ] + ); + + $data = new ValidationData($now + 10); + $data->setIssuer('test'); + + $this->assertFalse($token->validate($data)); + } + /** * @test * @@ -380,6 +414,7 @@ public function validateShouldReturnFalseWhenThereIsAtLeastOneFailedValidatableC public function validateShouldReturnTrueWhenThereAreNoFailedValidatableClaims() { $now = time(); + $token = new Token( [], [ @@ -396,6 +431,40 @@ public function validateShouldReturnTrueWhenThereAreNoFailedValidatableClaims() $this->assertTrue($token->validate($data)); } + /** + * @test + * + * @uses Lcobucci\JWT\Token::__construct + * @uses Lcobucci\JWT\ValidationData + * @uses Lcobucci\JWT\Claim\Basic + * @uses Lcobucci\JWT\Claim\EqualsTo + * @uses Lcobucci\JWT\Claim\LesserOrEqualsTo + * @uses Lcobucci\JWT\Claim\GreaterOrEqualsTo + * + * @covers Lcobucci\JWT\Token::validate + * @covers Lcobucci\JWT\Token::getValidatableClaims + */ + public function validateShouldReturnTrueWhenLeewayMakesAllTimeBasedClaimsTrueAndOtherClaimsAreTrue() + { + $now = time(); + + $token = new Token( + [], + [ + 'iss' => new EqualsTo('iss', 'test'), + 'iat' => new LesserOrEqualsTo('iat', $now), + 'nbf' => new LesserOrEqualsTo('nbf', $now + 20), + 'exp' => new GreaterOrEqualsTo('exp', $now + 500), + 'testing' => new Basic('testing', 'test') + ] + ); + + $data = new ValidationData($now + 10, 20); + $data->setIssuer('test'); + + $this->assertTrue($token->validate($data)); + } + /** * @test * diff --git a/test/unit/ValidationDataTest.php b/test/unit/ValidationDataTest.php index ac612658..52512412 100644 --- a/test/unit/ValidationDataTest.php +++ b/test/unit/ValidationDataTest.php @@ -26,6 +26,19 @@ public function constructorShouldConfigureTheItems() $this->assertAttributeSame($expected, 'items', $data); } + /** + * @test + * + * @covers Lcobucci\JWT\ValidationData::__construct + */ + public function constructorWithLeewayShouldConfigureTheItems() + { + $expected = $this->createExpectedData(null, null, null, null, 111, 111, 89); + $data = new ValidationData(100, 11); + + $this->assertAttributeSame($expected, 'items', $data); + } + /** * @test * @@ -114,6 +127,22 @@ public function setCurrentTimeShouldChangeTheTimeBasedValues() $this->assertAttributeSame($expected, 'items', $data); } + /** + * @test + * + * @uses Lcobucci\JWT\ValidationData::__construct + * + * @covers Lcobucci\JWT\ValidationData::setCurrentTime + */ + public function setCurrentTimeShouldChangeTheTimeBasedValuesUsingLeeway() + { + $expected = $this->createExpectedData(null, null, null, null, 30, 30, 10); + $data = new ValidationData(15, 10); + $data->setCurrentTime(20); + + $this->assertAttributeSame($expected, 'items', $data); + } + /** * @test * @@ -196,11 +225,13 @@ public function claimValues() } /** - * @param string $id - * @param string $sub - * @param string $iss - * @param string $aud - * @param int $time + * @param string|null $id + * @param string|null $sub + * @param string|null $iss + * @param string|null $aud + * @param int $iat + * @param int|null $nbf + * @param int|null $exp * * @return array */ @@ -209,16 +240,18 @@ private function createExpectedData( $sub = null, $iss = null, $aud = null, - $time = 1 + $iat = 1, + $nbf = null, + $exp = null ) { return [ 'jti' => $id !== null ? (string) $id : null, 'iss' => $iss !== null ? (string) $iss : null, 'aud' => $aud !== null ? (string) $aud : null, 'sub' => $sub !== null ? (string) $sub : null, - 'iat' => $time, - 'nbf' => $time, - 'exp' => $time + 'iat' => $iat, + 'nbf' => $nbf !== null ? $nbf: $iat, + 'exp' => $exp !== null ? $exp: $iat ]; } }