diff --git a/Adapter.php b/Adapter.php index de3969c..67191cd 100644 --- a/Adapter.php +++ b/Adapter.php @@ -121,6 +121,9 @@ public function upgradeStorage($storageId) if ($oldStorage->hasAuthorizationState($service)) { $newStorage->storeAuthorizationState($service, $oldStorage->retrieveAuthorizationState($service)); } + if ($oldStorage->hasCodeVerifier($service)) { + $newStorage->storeCodeVerifier($service, $oldStorage->retrieveCodeVerifier($service)); + } // fixme invalidate current oauth object? reinitialize it? } diff --git a/Storage.php b/Storage.php index f2bf76b..5f14142 100644 --- a/Storage.php +++ b/Storage.php @@ -149,4 +149,51 @@ public function clearAllAuthorizationStates() return $this; } + + /** @inheritDoc */ + public function storeCodeVerifier($service, $verifier) + { + $data = $this->loadServiceFile($service); + $data['verifier'] = $verifier; + $this->saveServiceFile($service, $data); + return $this; + } + + /** @inheritDoc */ + public function hasCodeVerifier($service) + { + $data = $this->loadServiceFile($service); + return isset($data['verifier']); + } + + /** + * @inheritDoc + * @throws TokenNotFoundException + */ + public function retrieveCodeVerifier($service) + { + $data = $this->loadServiceFile($service); + if (!isset($data['verifier'])) { + throw new TokenNotFoundException('No code verifier found in storage'); + } + return $data['verifier']; + } + + /** @inheritDoc */ + public function clearCodeVerifier($service) + { + $data = $this->loadServiceFile($service); + if (isset($data['verifier'])) unset($data['verifier']); + $this->saveServiceFile($service, $data); + + return $this; + } + + /** @inheritDoc */ + public function clearAllCodeVerifiers() + { + // TODO: Implement clearAllCodeVerifiers() method. + + return $this; + } } diff --git a/vendor/lusitanian/oauth/src/OAuth/Common/Storage/Exception/CodeVerifierNotFoundException.php b/vendor/lusitanian/oauth/src/OAuth/Common/Storage/Exception/CodeVerifierNotFoundException.php new file mode 100644 index 0000000..d3e6b3b --- /dev/null +++ b/vendor/lusitanian/oauth/src/OAuth/Common/Storage/Exception/CodeVerifierNotFoundException.php @@ -0,0 +1,10 @@ +tokens = array(); $this->states = array(); + $this->verifiers = array(); } /** @@ -136,4 +143,59 @@ public function clearAllAuthorizationStates() // allow chaining return $this; } + + /** + * {@inheritDoc} + */ + public function retrieveCodeVerifier($service) + { + if ($this->hasCodeVerifier($service)) { + return $this->verifiers[$service]; + } + + throw new CodeVerifierNotFoundException('code verifier not stored'); + } + + /** + * {@inheritDoc} + */ + public function storeCodeVerifier($service, $verifier) + { + $this->verifiers[$service] = $verifier; + + // allow chaining + return $this; + } + + /** + * {@inheritDoc} + */ + public function hasCodeVerifier($service) + { + return isset($this->verifiers[$service]) && null !== $this->verifiers[$service]; + } + + /** + * {@inheritDoc} + */ + public function clearCodeVerifier($service) + { + if (array_key_exists($service, $this->verifiers)) { + unset($this->verifiers[$service]); + } + + // allow chaining + return $this; + } + + /** + * {@inheritDoc} + */ + public function clearAllCodeVerifiers() + { + $this->verifiers = array(); + + // allow chaining + return $this; + } } diff --git a/vendor/lusitanian/oauth/src/OAuth/Common/Storage/Redis.php b/vendor/lusitanian/oauth/src/OAuth/Common/Storage/Redis.php index 5d3d9ae..254af36 100644 --- a/vendor/lusitanian/oauth/src/OAuth/Common/Storage/Redis.php +++ b/vendor/lusitanian/oauth/src/OAuth/Common/Storage/Redis.php @@ -5,6 +5,7 @@ use OAuth\Common\Token\TokenInterface; use OAuth\Common\Storage\Exception\TokenNotFoundException; use OAuth\Common\Storage\Exception\AuthorizationStateNotFoundException; +use OAuth\Common\Storage\Exception\CodeVerifierNotFoundException; use Predis\Client as Predis; /* @@ -19,6 +20,11 @@ class Redis implements TokenStorageInterface protected $stateKey; + /** + * @var string + */ + protected $verifierKey; + /** * @var object|\Redis */ @@ -34,18 +40,26 @@ class Redis implements TokenStorageInterface */ protected $cachedStates; + /** + * @var object + */ + protected $cachedVerifiers; + /** * @param Predis $redis An instantiated and connected redis client * @param string $key The key to store the token under in redis * @param string $stateKey The key to store the state under in redis. + * @param string $verifierKey The key to store the verifier under in redis. */ - public function __construct(Predis $redis, $key, $stateKey) + public function __construct(Predis $redis, $key, $stateKey, $verifierKey) { $this->redis = $redis; $this->key = $key; $this->stateKey = $stateKey; + $this->verifierKey = $verifierKey; $this->cachedTokens = array(); $this->cachedStates = array(); + $this->cachedVerifiers = array(); } /** @@ -212,6 +226,88 @@ function ($pipe) use ($keys, $me) { return $this; } + /** + * {@inheritDoc} + */ + public function retrieveCodeVerifier($service) + { + if (!$this->hasCodeVerifier($service)) { + throw new CodeVerifierNotFoundException('CodeVerifier not found in redis'); + } + + if (isset($this->cachedVerifiers[$service])) { + return $this->cachedVerifiers[$service]; + } + + $val = $this->redis->hget($this->verifierKey, $service); + + return $this->cachedVerifiers[$service] = $val; + } + + /** + * {@inheritDoc} + */ + public function storeCodeVerifier($service, $verifier) + { + // (over)write the token + $this->redis->hset($this->verifierKey, $service, $verifier); + $this->cachedVerifiers[$service] = $verifier; + + // allow chaining + return $this; + } + + /** + * {@inheritDoc} + */ + public function hasCodeVerifier($service) + { + if (isset($this->cachedVerifiers[$service]) + && null !== $this->cachedVerifiers[$service] + ) { + return true; + } + + return $this->redis->hexists($this->verifierKey, $service); + } + + /** + * {@inheritDoc} + */ + public function clearCodeVerifier($service) + { + $this->redis->hdel($this->verifierKey, $service); + unset($this->cachedVerifiers[$service]); + + // allow chaining + return $this; + } + + /** + * {@inheritDoc} + */ + public function clearAllCodeVerifiers() + { + // memory + $this->cachedVerifiers = array(); + + // redis + $keys = $this->redis->hkeys($this->verifierKey); + $me = $this; // 5.3 compat + + // pipeline for performance + $this->redis->pipeline( + function ($pipe) use ($keys, $me) { + foreach ($keys as $k) { + $pipe->hdel($me->getKey(), $k); + } + } + ); + + // allow chaining + return $this; + } + /** * @return Predis $redis */ diff --git a/vendor/lusitanian/oauth/src/OAuth/Common/Storage/Session.php b/vendor/lusitanian/oauth/src/OAuth/Common/Storage/Session.php index dd9aba7..4ac7522 100644 --- a/vendor/lusitanian/oauth/src/OAuth/Common/Storage/Session.php +++ b/vendor/lusitanian/oauth/src/OAuth/Common/Storage/Session.php @@ -5,6 +5,7 @@ use OAuth\Common\Token\TokenInterface; use OAuth\Common\Storage\Exception\TokenNotFoundException; use OAuth\Common\Storage\Exception\AuthorizationStateNotFoundException; +use OAuth\Common\Storage\Exception\CodeVerifierNotFoundException; /** * Stores a token in a PHP session. @@ -26,6 +27,11 @@ class Session implements TokenStorageInterface */ protected $stateVariableName; + /** + * @var string + */ + protected $verifierVariableName; + /** * @param bool $startSession Whether or not to start the session upon construction. * @param string $sessionVariableName the variable name to use within the _SESSION superglobal @@ -35,6 +41,7 @@ public function __construct( $startSession = true, $sessionVariableName = 'lusitanian-oauth-token', $stateVariableName = 'lusitanian-oauth-state' + $verifierVariableName = 'lusitanian-oauth-verifier' ) { if ($startSession && !$this->sessionHasStarted()) { session_start(); @@ -43,12 +50,16 @@ public function __construct( $this->startSession = $startSession; $this->sessionVariableName = $sessionVariableName; $this->stateVariableName = $stateVariableName; + $this->verifierVariableName = $verifierVariableName; if (!isset($_SESSION[$sessionVariableName])) { $_SESSION[$sessionVariableName] = array(); } if (!isset($_SESSION[$stateVariableName])) { $_SESSION[$stateVariableName] = array(); } + if (!isset($_SESSION[$verifierVariableName])) { + $_SESSION[$verifierVariableName] = array(); + } } /** @@ -179,6 +190,69 @@ public function clearAllAuthorizationStates() return $this; } + /** + * {@inheritDoc} + */ + public function storeCodeVerifier($service, $verifier) + { + if (isset($_SESSION[$this->verifierVariableName]) + && is_array($_SESSION[$this->verifierVariableName]) + ) { + $_SESSION[$this->verifierVariableName][$service] = $verifier; + } else { + $_SESSION[$this->verifierVariableName] = array( + $service => $verifier, + ); + } + + // allow chaining + return $this; + } + + /** + * {@inheritDoc} + */ + public function hasCodeVerifier($service) + { + return isset($_SESSION[$this->verifierVariableName], $_SESSION[$this->verifierVariableName][$service]); + } + + /** + * {@inheritDoc} + */ + public function retrieveCodeVerifier($service) + { + if ($this->hasCodeVerifier($service)) { + return $_SESSION[$this->verifierVariableName][$service]; + } + + throw new CodeVerifierNotFoundException('verifier not found in session, are you sure you stored it?'); + } + + /** + * {@inheritDoc} + */ + public function clearCodeVerifier($service) + { + if (array_key_exists($service, $_SESSION[$this->verifierVariableName])) { + unset($_SESSION[$this->verifierVariableName][$service]); + } + + // allow chaining + return $this; + } + + /** + * {@inheritDoc} + */ + public function clearAllCodeVerifiers() + { + unset($_SESSION[$this->verifierVariableName]); + + // allow chaining + return $this; + } + public function __destruct() { if ($this->startSession) { diff --git a/vendor/lusitanian/oauth/src/OAuth/Common/Storage/SymfonySession.php b/vendor/lusitanian/oauth/src/OAuth/Common/Storage/SymfonySession.php index 6c5fbf6..607a349 100644 --- a/vendor/lusitanian/oauth/src/OAuth/Common/Storage/SymfonySession.php +++ b/vendor/lusitanian/oauth/src/OAuth/Common/Storage/SymfonySession.php @@ -5,6 +5,7 @@ use OAuth\Common\Token\TokenInterface; use OAuth\Common\Storage\Exception\TokenNotFoundException; use OAuth\Common\Storage\Exception\AuthorizationStateNotFoundException; +use OAuth\Common\Storage\Exception\CodeVerifierNotFoundException; use Symfony\Component\HttpFoundation\Session\SessionInterface; class SymfonySession implements TokenStorageInterface @@ -12,22 +13,26 @@ class SymfonySession implements TokenStorageInterface private $session; private $sessionVariableName; private $stateVariableName; + private $verifierVariableName; /** * @param SessionInterface $session * @param bool $startSession * @param string $sessionVariableName * @param string $stateVariableName + * @param string $verifierVariableName */ public function __construct( SessionInterface $session, $startSession = true, $sessionVariableName = 'lusitanian_oauth_token', $stateVariableName = 'lusitanian_oauth_state' + $verifierVariableName = 'lusitanian_oauth_verifier' ) { $this->session = $session; $this->sessionVariableName = $sessionVariableName; $this->stateVariableName = $stateVariableName; + $this->verifierVariableName = $verifierVariableName; } /** @@ -190,6 +195,86 @@ public function clearAllAuthorizationStates() return $this; } + /** + * {@inheritDoc} + */ + public function retrieveCodeVerifier($service) + { + if ($this->hasCodeVerifier($service)) { + // get from session + $verifiers = $this->session->get($this->verifierVariableName); + + // one item + return $verifiers[$service]; + } + + throw new CodeVerifierNotFoundException('verifier not found in session, are you sure you stored it?'); + } + + /** + * {@inheritDoc} + */ + public function storeCodeVerifier($service, $verifier) + { + // get previously saved tokens + $verifiers = $this->session->get($this->verifierVariableName); + + if (!is_array($verifiers)) { + $verifiers = array(); + } + + $verifiers[$service] = $verifier; + + // save + $this->session->set($this->verifierVariableName, $verifiers); + + // allow chaining + return $this; + } + + /** + * {@inheritDoc} + */ + public function hasCodeVerifier($service) + { + // get from session + $verifiers = $this->session->get($this->verifierVariableName); + + return is_array($verifiers) + && isset($verifiers[$service]) + && null !== $verifiers[$service]; + } + + /** + * {@inheritDoc} + */ + public function clearCodeVerifier($service) + { + // get previously saved tokens + $verifiers = $this->session->get($this->verifierVariableName); + + if (is_array($verifiers) && array_key_exists($service, $verifiers)) { + unset($verifiers[$service]); + + // Replace the stored tokens array + $this->session->set($this->verifierVariableName, $verifiers); + } + + // allow chaining + return $this; + } + + /** + * {@inheritDoc} + */ + public function clearAllCodeVerifiers() + { + $this->session->remove($this->verifierVariableName); + + // allow chaining + return $this; + } + /** * @return Session */ diff --git a/vendor/lusitanian/oauth/src/OAuth/Common/Storage/TokenStorageInterface.php b/vendor/lusitanian/oauth/src/OAuth/Common/Storage/TokenStorageInterface.php index 46552ce..67ded00 100644 --- a/vendor/lusitanian/oauth/src/OAuth/Common/Storage/TokenStorageInterface.php +++ b/vendor/lusitanian/oauth/src/OAuth/Common/Storage/TokenStorageInterface.php @@ -95,4 +95,49 @@ public function clearAuthorizationState($service); * @return TokenStorageInterface */ public function clearAllAuthorizationStates(); + + /** + * Store the authorization verifier related to a given service + * + * @param string $service + * @param string $verifier + * + * @return TokenStorageInterface + */ + public function storeCodeVerifier($service, $verifier); + + /** + * Check if an authorization verifier for a given service exists + * + * @param string $service + * + * @return bool + */ + public function hasCodeVerifier($service); + + /** + * Retrieve the authorization verifier for a given service + * + * @param string $service + * + * @return string + */ + public function retrieveCodeVerifier($service); + + /** + * Clear the authorization verifier of a given service + * + * @param string $service + * + * @return TokenStorageInterface + */ + public function clearCodeVerifier($service); + + /** + * Delete *ALL* user authorization verifiers. Use with care. Most of the time you will likely + * want to use clearAuthorization() instead. + * + * @return TokenStorageInterface + */ + public function clearAllCodeVerifiers(); } diff --git a/vendor/lusitanian/oauth/src/OAuth/OAuth2/Service/AbstractService.php b/vendor/lusitanian/oauth/src/OAuth/OAuth2/Service/AbstractService.php index e5c5878..919b602 100644 --- a/vendor/lusitanian/oauth/src/OAuth/OAuth2/Service/AbstractService.php +++ b/vendor/lusitanian/oauth/src/OAuth/OAuth2/Service/AbstractService.php @@ -85,6 +85,16 @@ public function getAuthorizationUri(array $additionalParameters = []) $this->storeAuthorizationState($parameters['state']); } + $code_challenge_method = $this->getCodeChallengeMethod(); + if ($code_challenge_method) { + $challenge = $this->generateCodeChallenge($code_challenge_method); + if (!isset($parameters['code_challenge'])) { + $parameters['code_challenge'] = $challenge['challenge']; + $parameters['code_challenge_method'] = $code_challenge_method; + } + $this->storeCodeVerifier($challenge['verifier']); + } + // Build the url $url = clone $this->getAuthorizationEndpoint(); foreach ($parameters as $key => $val) { @@ -111,6 +121,10 @@ public function requestAccessToken($code, $state = null) 'grant_type' => 'authorization_code', ]; + if ($this->getCodeChallengeMethod()) { + $bodyParams['code_verifier'] = $this->retrieveCodeVerifier(); + } + $responseBody = $this->httpClient->retrieveResponse( $this->getAccessTokenEndpoint(), $bodyParams, @@ -283,6 +297,59 @@ protected function storeAuthorizationState($state): void $this->storage->storeAuthorizationState($this->service(), $state); } + /** + * Get the method used for the code challenge + * + * @return string null|'plain'|'S256' + */ + public function getCodeChallengeMethod() + { + return null; + } + + /** + * Generates a random code challenge. + * + * @param string $method 'plain'|'S256' + * + * @return array + */ + protected function generateCodeChallenge($method) + { + $verifier = $this->base64url(random_bytes(64)); + if ($method === 'plain') { + return array( + 'verifier' => $verifier, + 'challenge' => $verifier, + ); + } else { + return array( + 'verifier' => $verifier, + 'challenge' => $this->base64url(hash('sha256', $verifier, true)), + ); + } + } + + /** + * Retrieves the code verifier for the current service. + * + * @return string + */ + protected function retrieveCodeVerifier() + { + return $this->storage->retrieveCodeVerifier($this->service()); + } + + /** + * Stores a given code verifier into the storage. + * + * @param string $verifier + */ + protected function storeCodeVerifier($verifier): void + { + $this->storage->storeCodeVerifier($this->service(), $verifier); + } + /** * Return any additional headers always needed for this service implementation's OAuth calls. * @@ -346,4 +413,16 @@ protected function getScopesDelimiter() { return ' '; } + + /** + * URL compatible base64 encoding. + * + * @param string $bytes + * + * @return string + */ + protected function base64url($bytes) + { + return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='); + } }