diff --git a/src/Provider/AbstractProvider.php b/src/Provider/AbstractProvider.php index d1679998..a3fd9e94 100644 --- a/src/Provider/AbstractProvider.php +++ b/src/Provider/AbstractProvider.php @@ -98,6 +98,11 @@ abstract class AbstractProvider */ protected $optionProvider; + /** + * @var Clock + */ + protected $clock; + /** * Constructs an OAuth 2.0 service provider. * @@ -138,6 +143,11 @@ public function __construct(array $options = [], array $collaborators = []) $collaborators['optionProvider'] = new PostAuthOptionProvider(); } $this->setOptionProvider($collaborators['optionProvider']); + + if (empty($collaborators['clock'])) { + $collaborators['clock'] = new Clock(); + } + $this->setClock($collaborators['clock']); } /** @@ -252,6 +262,31 @@ public function getOptionProvider() return $this->optionProvider; } + /** + * Sets the clock. + * + * @param Clock $clock + * + * @return self + */ + public function setClock(Clock $clock) + { + $this->clock = $clock; + + return $this; + } + + + /** + * Returns the clock. + * + * @return Clock + */ + public function getClock() + { + return $this->clock; + } + /** * Returns the current value of the state parameter. * @@ -541,6 +576,7 @@ public function getAccessToken($grant, array $options = []) ); } $prepared = $this->prepareAccessTokenResponse($response); + $prepared['clock'] = $this->clock; $token = $this->createAccessToken($prepared, $grant); return $token; diff --git a/src/Provider/Clock.php b/src/Provider/Clock.php new file mode 100644 index 00000000..bb5a2d50 --- /dev/null +++ b/src/Provider/Clock.php @@ -0,0 +1,20 @@ +clock = $clock; + } + + /** + * @inheritdoc */ public function getTimeNow() { - return self::$timeNow ? self::$timeNow : time(); + if (self::$timeNow) { + return self::$timeNow; + } elseif (isset($this->clock)) { + return $this->clock->now()->getTimestamp(); + } else { + return time(); + } } /** @@ -106,6 +130,10 @@ public function __construct(array $options = []) $this->refreshToken = $options['refresh_token']; } + if (!empty($options['clock'])) { + $this->clock = $options['clock']; + } + // We need to know when the token expires. Show preference to // 'expires_in' since it is defined in RFC6749 Section 5.1. // Defer to 'expires' if it is provided instead. @@ -196,7 +224,7 @@ public function hasExpired() throw new RuntimeException('"expires" is not set on the token'); } - return $expires < time(); + return $expires < $this->getTimeNow(); } /** diff --git a/src/Token/AccessTokenInterface.php b/src/Token/AccessTokenInterface.php index c5f13350..132397db 100644 --- a/src/Token/AccessTokenInterface.php +++ b/src/Token/AccessTokenInterface.php @@ -15,6 +15,7 @@ namespace League\OAuth2\Client\Token; use JsonSerializable; +use League\OAuth2\Client\Provider\Clock; use RuntimeException; interface AccessTokenInterface extends JsonSerializable @@ -69,4 +70,20 @@ public function __toString(); * @return array */ public function jsonSerialize(); + + /** + * Sets the clock. + * + * @param Clock $clock a clock. + * + * @return void + */ + public function setClock(Clock $clock); + + /** + * Get the current time, whether real or simulated. + * + * @return int + */ + public function getTimeNow(); } diff --git a/src/Tool/MacAuthorizationTrait.php b/src/Tool/MacAuthorizationTrait.php index f8dcd77c..7de6f3d5 100644 --- a/src/Tool/MacAuthorizationTrait.php +++ b/src/Tool/MacAuthorizationTrait.php @@ -14,6 +14,7 @@ namespace League\OAuth2\Client\Tool; +use League\OAuth2\Client\Provider\Clock; use League\OAuth2\Client\Token\AccessToken; use League\OAuth2\Client\Token\AccessTokenInterface; @@ -24,6 +25,12 @@ */ trait MacAuthorizationTrait { + + /** + * @var Clock + */ + protected $clock; + /** * Returns the id of this token for MAC generation. * @@ -68,7 +75,7 @@ protected function getAuthorizationHeaders($token = null) return []; } - $ts = time(); + $ts = $this->clock->now()->getTimestamp(); $id = $this->getTokenId($token); $nonce = $this->getRandomState(16); $mac = $this->getMacSignature($id, $ts, $nonce); diff --git a/test/src/Provider/AbstractProviderTest.php b/test/src/Provider/AbstractProviderTest.php index bf24ba38..39b081c1 100644 --- a/test/src/Provider/AbstractProviderTest.php +++ b/test/src/Provider/AbstractProviderTest.php @@ -3,6 +3,7 @@ namespace League\OAuth2\Client\Test\Provider; use League\OAuth2\Client\OptionProvider\PostAuthOptionProvider; +use League\OAuth2\Client\Provider\Clock; use Mockery; use ReflectionClass; use UnexpectedValueException; @@ -24,6 +25,14 @@ class AbstractProviderTest extends TestCase { + + /** + * The current simulated time. + * + * @var int + */ + const NOW = 1359504000; + protected function getMockProvider() { return new MockProvider([ @@ -41,6 +50,14 @@ public function testGetOptionProvider() ); } + public function testGetClock() + { + $this->assertInstanceOf( + Clock::class, + $this->getMockProvider()->getClock() + ); + } + public function testInvalidGrantString() { $this->expectException(InvalidGrantException::class); @@ -504,7 +521,7 @@ public function testGetAccessToken($method) $provider->setAccessTokenMethod($method); - $raw_response = ['access_token' => 'okay', 'expires' => time() + 3600, 'resource_owner_id' => 3]; + $raw_response = ['access_token' => 'okay', 'expires' => static::NOW + 3600, 'resource_owner_id' => 3]; $grant = Mockery::mock(AbstractGrant::class); $grant @@ -538,6 +555,9 @@ public function testGetAccessToken($method) ]); $provider->setHttpClient($client); + $clock = (new ProgrammableClock()) + ->setTime(new \DateTimeImmutable('1st February 2013 1pm')); + $provider->setClock($clock); $token = $provider->getAccessToken($grant, ['code' => 'mock_authorization_code']); $this->assertInstanceOf(AccessTokenInterface::class, $token); @@ -546,6 +566,13 @@ public function testGetAccessToken($method) $this->assertSame($raw_response['access_token'], $token->getToken()); $this->assertSame($raw_response['expires'], $token->getExpires()); + // Set the time to a different value so we know references to the + // original clock object in provider was not lost. + $newTime = new \DateTimeImmutable('2nd February 2013 1pm'); + $clock->setTime($newTime); + + $this->assertEquals($newTime->getTimestamp(), $token->getTimeNow()); + $client ->shouldHaveReceived('send') ->once() @@ -786,7 +813,7 @@ public function testExtendedProviderDoesNotErrorWhenUsingAccessTokenAsTheTypeHin $token = new AccessToken([ 'access_token' => 'mock_access_token', 'refresh_token' => 'mock_refresh_token', - 'expires' => time(), + 'expires' => 123, 'resource_owner_id' => 'mock_resource_owner_id', ]); diff --git a/test/src/Provider/FrozenClock.php b/test/src/Provider/FrozenClock.php new file mode 100644 index 00000000..ff2b0983 --- /dev/null +++ b/test/src/Provider/FrozenClock.php @@ -0,0 +1,29 @@ +time)) { + throw new \LogicException('Time must be set explicitly'); + } + return $this->time; + } + + /** + * Sets the current time. + * + * @param \DateTimeImmutable|null the current time. + * @return self + */ + public function setTime($time) + { + $this->time = $time; + + return $this; + } +} diff --git a/test/src/Token/AccessTokenTest.php b/test/src/Token/AccessTokenTest.php index 23ad105f..989ad85d 100644 --- a/test/src/Token/AccessTokenTest.php +++ b/test/src/Token/AccessTokenTest.php @@ -3,6 +3,7 @@ namespace League\OAuth2\Client\Test\Token; use InvalidArgumentException; +use League\OAuth2\Client\Test\Provider\FrozenClock; use League\OAuth2\Client\Token\AccessToken; use Mockery; use PHPUnit\Framework\TestCase; @@ -10,6 +11,14 @@ class AccessTokenTest extends TestCase { + + /** + * The current simulated time. + * + * @var int + */ + const NOW = 1359504000; + /** * BC teardown. * @@ -40,14 +49,15 @@ protected function getAccessToken($options = []) public function testExpiresInCorrection() { + // Correction happens in constructor so time needs to be set before + // object is created. + AccessToken::setTimeNow(static::NOW); $options = ['access_token' => 'access_token', 'expires_in' => 100]; $token = $this->getAccessToken($options); $expires = $token->getExpires(); - $this->assertNotNull($expires); - $this->assertGreaterThan(time(), $expires); - $this->assertLessThan(time() + 200, $expires); + $this->assertEquals(static::NOW + 100, $expires); self::tearDownForBackwardsCompatibility(); } @@ -67,6 +77,21 @@ public function testExpiresInCorrectionUsingSetTimeNow() self::tearDownForBackwardsCompatibility(); } + public function testSetClockConstructor() + { + $clock = new FrozenClock(); + $token = $this->getAccessToken(['access_token' => 'asdf', 'clock' => $clock]); + $this->assertEquals(FrozenClock::NOW, $token->getTimeNow()); + } + + public function testSetClockMethod() + { + $clock = new FrozenClock(); + $token = $this->getAccessToken(['access_token' => 'asdf']); + $token->setClock($clock); + $this->assertEquals(FrozenClock::NOW, $token->getTimeNow()); + } + public function testSetTimeNow() { AccessToken::setTimeNow(1577836800); @@ -77,15 +102,24 @@ public function testSetTimeNow() self::tearDownForBackwardsCompatibility(); } + public function testSetClockAndSetTime() + { + // When both a clock and time set, time wins over clock. + $clock = new FrozenClock(); + AccessToken::setTimeNow(static::NOW); + $token = $this->getAccessToken(['access_token' => 'asdf', 'clock' => $clock]); + $this->assertEquals(static::NOW, $token->getTimeNow()); + } + public function testResetTimeNow() { - AccessToken::setTimeNow(1577836800); + AccessToken::setTimeNow(static::NOW); $token = $this->getAccessToken(['access_token' => 'asdf']); - $this->assertEquals(1577836800, $token->getTimeNow()); + $this->assertEquals(static::NOW, $token->getTimeNow()); AccessToken::resetTimeNow(); - $this->assertNotEquals(1577836800, $token->getTimeNow()); + $this->assertNotEquals(static::NOW, $token->getTimeNow()); $timeBeforeAssertion = time(); $this->assertGreaterThanOrEqual($timeBeforeAssertion, $token->getTimeNow()); @@ -95,8 +129,9 @@ public function testResetTimeNow() public function testExpiresPastTimestamp() { - $options = ['access_token' => 'access_token', 'expires' => strtotime('5 days ago')]; + $options = ['access_token' => 'access_token', 'expires' => static::NOW - 1]; $token = $this->getAccessToken($options); + AccessToken::setTimeNow(static::NOW); $this->assertTrue($token->hasExpired()); @@ -129,8 +164,9 @@ public function testHasNotExpiredWhenPropertySetInFuture() 'access_token' => 'access_token' ]; - $expectedExpires = strtotime('+1 day'); + $expectedExpires = static::NOW + 1; + AccessToken::setTimeNow(static::NOW); $token = Mockery::mock(AccessToken::class, [$options])->makePartial(); $token ->shouldReceive('getExpires') @@ -148,7 +184,7 @@ public function testHasExpiredWhenPropertySetInPast() 'access_token' => 'access_token' ]; - $expectedExpires = strtotime('-1 day'); + $expectedExpires = static::NOW - 1; $token = Mockery::mock(AccessToken::class, [$options])->makePartial(); $token @@ -186,7 +222,7 @@ public function testInvalidExpiresIn() $token = $this->getAccessToken($options); - self::tearDownForBackwardsCompatibility(); + self::tearDownForBackwardsCompatibility(); } @@ -195,7 +231,7 @@ public function testJsonSerializable() $options = [ 'access_token' => 'mock_access_token', 'refresh_token' => 'mock_refresh_token', - 'expires' => time(), + 'expires' => static::NOW + 3600, 'resource_owner_id' => 'mock_resource_owner_id', ]; @@ -212,7 +248,7 @@ public function testValues() $options = [ 'access_token' => 'mock_access_token', 'refresh_token' => 'mock_refresh_token', - 'expires' => time(), + 'expires' => static::NOW + 3600, 'resource_owner_id' => 'mock_resource_owner_id', 'custom_thing' => 'i am a test!', ];