From dbd7c834a6713d329f09538a55de62cd96d1523f Mon Sep 17 00:00:00 2001
From: dpi <pro@danielph.in>
Date: Sun, 8 Nov 2020 20:02:06 +0800
Subject: [PATCH 1/3] Switch to fully using simulated time where possible

---
 src/Token/AccessToken.php                  | 16 +++++++++--
 test/src/Provider/AbstractProviderTest.php | 12 ++++++--
 test/src/Token/AccessTokenTest.php         | 33 ++++++++++++++--------
 3 files changed, 45 insertions(+), 16 deletions(-)

diff --git a/src/Token/AccessToken.php b/src/Token/AccessToken.php
index 81533c30..7fbb3a07 100644
--- a/src/Token/AccessToken.php
+++ b/src/Token/AccessToken.php
@@ -15,6 +15,7 @@
 namespace League\OAuth2\Client\Token;
 
 use InvalidArgumentException;
+use League\OAuth2\Client\Provider\ProviderClock;
 use RuntimeException;
 
 /**
@@ -50,12 +51,17 @@ class AccessToken implements AccessTokenInterface, ResourceOwnerAccessTokenInter
     protected $values = [];
 
     /**
-     * @var int
+     * The current time, or NULL to get the true current time via PHP.
+     *
+     * @var int|null
      */
     private static $timeNow;
 
     /**
      * Set the time now. This should only be used for testing purposes.
+
+    /**
+     * Sets the current time.
      *
      * @param int $timeNow the time in seconds since epoch
      * @return void
@@ -66,7 +72,7 @@ public static function setTimeNow($timeNow)
     }
 
     /**
-     * Reset the time now if it was set for test purposes.
+     * Reset the current time so the true current time via PHP is used.
      *
      * @return void
      */
@@ -76,6 +82,10 @@ public static function resetTimeNow()
     }
 
     /**
+
+    /**
+     * Get the current time, whether true or simulated.
+     *
      * @return int
      */
     public function getTimeNow()
@@ -196,7 +206,7 @@ public function hasExpired()
             throw new RuntimeException('"expires" is not set on the token');
         }
 
-        return $expires < time();
+        return $expires < $this->getTimeNow();
     }
 
     /**
diff --git a/test/src/Provider/AbstractProviderTest.php b/test/src/Provider/AbstractProviderTest.php
index bf24ba38..463af186 100644
--- a/test/src/Provider/AbstractProviderTest.php
+++ b/test/src/Provider/AbstractProviderTest.php
@@ -24,6 +24,14 @@
 
 class AbstractProviderTest extends TestCase
 {
+
+    /**
+     * The current simulated time.
+     *
+     * @var int
+     */
+    const NOW = 1359504000;
+
     protected function getMockProvider()
     {
         return new MockProvider([
@@ -504,7 +512,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
@@ -786,7 +794,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/Token/AccessTokenTest.php b/test/src/Token/AccessTokenTest.php
index 23ad105f..fbe369ac 100644
--- a/test/src/Token/AccessTokenTest.php
+++ b/test/src/Token/AccessTokenTest.php
@@ -10,6 +10,14 @@
 
 class AccessTokenTest extends TestCase
 {
+
+    /**
+     * The current simulated time.
+     *
+     * @var int
+     */
+    const NOW = 1359504000;
+
     /**
      * BC teardown.
      *
@@ -40,14 +48,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();
     }
@@ -79,13 +88,13 @@ public function testSetTimeNow()
 
     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 +104,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 +139,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 +159,7 @@ public function testHasExpiredWhenPropertySetInPast()
             'access_token' => 'access_token'
         ];
 
-        $expectedExpires = strtotime('-1 day');
+        $expectedExpires = static::NOW - 1;
 
         $token = Mockery::mock(AccessToken::class, [$options])->makePartial();
         $token
@@ -195,7 +206,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 +223,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!',
         ];

From 04ccbe45ae4f024e881e2e43f9622793206314c2 Mon Sep 17 00:00:00 2001
From: dpi <pro@danielph.in>
Date: Sun, 8 Nov 2020 21:01:38 +0800
Subject: [PATCH 2/3] Allow time to be fetched from a universal clock

---
 src/Provider/AbstractProvider.php          | 36 ++++++++++++++++++++++
 src/Provider/Clock.php                     | 20 ++++++++++++
 src/Token/AccessToken.php                  | 30 ++++++++++++++----
 src/Token/AccessTokenInterface.php         | 17 ++++++++++
 src/Tool/MacAuthorizationTrait.php         |  9 +++++-
 test/src/Provider/AbstractProviderTest.php |  9 ++++++
 test/src/Provider/FrozenClock.php          | 29 +++++++++++++++++
 test/src/Token/AccessTokenTest.php         | 27 +++++++++++++++-
 8 files changed, 169 insertions(+), 8 deletions(-)
 create mode 100644 src/Provider/Clock.php
 create mode 100644 test/src/Provider/FrozenClock.php

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 @@
+<?php
+
+namespace League\OAuth2\Client\Provider;
+
+/**
+ * Represents an implementation of a Clock.
+ */
+class Clock
+{
+
+  /**
+   * Get the current time.
+   *
+   * @return \DateTimeImmutable
+   */
+    public function now()
+    {
+        return new \DateTimeImmutable();
+    }
+}
diff --git a/src/Token/AccessToken.php b/src/Token/AccessToken.php
index 7fbb3a07..a52a8930 100644
--- a/src/Token/AccessToken.php
+++ b/src/Token/AccessToken.php
@@ -15,7 +15,7 @@
 namespace League\OAuth2\Client\Token;
 
 use InvalidArgumentException;
-use League\OAuth2\Client\Provider\ProviderClock;
+use League\OAuth2\Client\Provider\Clock;
 use RuntimeException;
 
 /**
@@ -58,7 +58,11 @@ class AccessToken implements AccessTokenInterface, ResourceOwnerAccessTokenInter
     private static $timeNow;
 
     /**
-     * Set the time now. This should only be used for testing purposes.
+     * The clock.
+     *
+     * @var Clock
+     */
+    protected $clock;
 
     /**
      * Sets the current time.
@@ -82,15 +86,25 @@ public static function resetTimeNow()
     }
 
     /**
+     * @inheritdoc
+     */
+    public function setClock(Clock $clock)
+    {
+        $this->clock = $clock;
+    }
 
     /**
-     * Get the current time, whether true or simulated.
-     *
-     * @return int
+     * @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();
+        }
     }
 
     /**
@@ -116,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.
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 463af186..cf7c83f8 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;
@@ -49,6 +50,14 @@ public function testGetOptionProvider()
         );
     }
 
+    public function testGetClock()
+    {
+        $this->assertInstanceOf(
+            Clock::class,
+            $this->getMockProvider()->getClock()
+        );
+    }
+
     public function testInvalidGrantString()
     {
         $this->expectException(InvalidGrantException::class);
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 @@
+<?php
+
+namespace League\OAuth2\Client\Test\Provider;
+
+use League\OAuth2\Client\Provider\Clock;
+
+/**
+ * A clock with a frozen time for testing.
+ */
+class FrozenClock extends Clock
+{
+
+    /**
+     * The simulated time.
+     *
+     * Evaluates to 1st January 2015 @ 12pm.
+     *
+     * @var int
+     */
+    const NOW = 1420113600;
+
+    /**
+     * @inheritdoc
+     */
+    public function now()
+    {
+        return (new \DateTimeImmutable('@' . static::NOW));
+    }
+}
diff --git a/test/src/Token/AccessTokenTest.php b/test/src/Token/AccessTokenTest.php
index fbe369ac..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;
@@ -76,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);
@@ -86,6 +102,15 @@ 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(static::NOW);
@@ -197,7 +222,7 @@ public function testInvalidExpiresIn()
 
          $token = $this->getAccessToken($options);
 
-        self::tearDownForBackwardsCompatibility();
+         self::tearDownForBackwardsCompatibility();
     }
 
 

From d286c6a2b7b3427f09cad70d4915cdc66f2bc26f Mon Sep 17 00:00:00 2001
From: dpi <pro@danielph.in>
Date: Sun, 8 Nov 2020 22:31:07 +0800
Subject: [PATCH 3/3] Tests an access token has the same clock as a provider.

---
 test/src/Provider/AbstractProviderTest.php | 10 ++++++
 test/src/Provider/ProgrammableClock.php    | 41 ++++++++++++++++++++++
 2 files changed, 51 insertions(+)
 create mode 100644 test/src/Provider/ProgrammableClock.php

diff --git a/test/src/Provider/AbstractProviderTest.php b/test/src/Provider/AbstractProviderTest.php
index cf7c83f8..39b081c1 100644
--- a/test/src/Provider/AbstractProviderTest.php
+++ b/test/src/Provider/AbstractProviderTest.php
@@ -555,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);
@@ -563,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()
diff --git a/test/src/Provider/ProgrammableClock.php b/test/src/Provider/ProgrammableClock.php
new file mode 100644
index 00000000..7231b8bd
--- /dev/null
+++ b/test/src/Provider/ProgrammableClock.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace League\OAuth2\Client\Test\Provider;
+
+use League\OAuth2\Client\Provider\Clock;
+
+/**
+ * A clock which must be initialised, and may be changed at any time.
+ */
+class ProgrammableClock extends Clock
+{
+
+    /**
+     * @var \DateTimeImmutable|null
+     */
+    protected $time = null;
+
+    /**
+     * @inheritdoc
+     */
+    public function now()
+    {
+        if (!isset($this->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;
+    }
+}