├── .vscode └── settings.json ├── src ├── Exception │ ├── OAuth2Exception.php │ ├── ReauthorizationException.php │ ├── AccessTokenRequestException.php │ └── ReauthorizationRequestException.php ├── Signer │ ├── AccessToken │ │ ├── BasicAuth.php │ │ ├── SignerInterface.php │ │ ├── BearerAuth.php │ │ └── QueryString.php │ └── ClientCredentials │ │ ├── SignerInterface.php │ │ ├── BasicAuth.php │ │ ├── Json.php │ │ └── PostFormData.php ├── Token │ ├── Serializable.php │ ├── TokenInterface.php │ ├── RawToken.php │ ├── TokenSerializer.php │ └── RawTokenFactory.php ├── GrantType │ ├── GrantTypeInterface.php │ ├── NullGrantType.php │ ├── Specific │ │ ├── HWIOAuthBundleRefreshToken.php │ │ └── GithubApplication.php │ ├── PasswordCredentials.php │ ├── ClientCredentials.php │ ├── RefreshToken.php │ └── AuthorizationCode.php ├── Persistence │ ├── NullTokenPersistence.php │ ├── TokenPersistenceInterface.php │ ├── FileTokenPersistence.php │ ├── DoctrineCacheTokenPersistence.php │ ├── Laravel5CacheTokenPersistence.php │ ├── ClosureTokenPersistence.php │ └── SimpleCacheTokenPersistence.php ├── Utils │ ├── Helper.php │ └── Collection.php ├── OAuth2Subscriber.php ├── OAuth2Middleware.php └── OAuth2Handler.php ├── composer.json ├── LICENSE ├── .github └── workflows │ └── phpunit-runner.yml └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "intelephense.environment.phpVersion": "7.2.0" 3 | } 4 | -------------------------------------------------------------------------------- /src/Exception/OAuth2Exception.php: -------------------------------------------------------------------------------- 1 | =', 6)) { 12 | return $request->withHeader('Authorization', 'Bearer ' . $accessToken); 13 | } 14 | 15 | $request->setHeader('Authorization', 'Bearer ' . $accessToken); 16 | 17 | return $request; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/GrantType/GrantTypeInterface.php: -------------------------------------------------------------------------------- 1 | =', 6)) { 12 | return $request->withHeader('Authorization', 'Basic ' . base64_encode($clientId . ':' . $clientSecret)); 13 | } 14 | 15 | $request->getConfig()->set('auth', 'basic'); 16 | $request->setHeader('Authorization', 'Basic ' . base64_encode($clientId . ':' . $clientSecret)); 17 | return $request; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Exception/ReauthorizationRequestException.php: -------------------------------------------------------------------------------- 1 | getPrevious(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kamermans/guzzle-oauth2-subscriber", 3 | "description": "OAuth 2.0 client for Guzzle 4, 5, 6 and 7+", 4 | "keywords": ["oauth", "guzzle"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Steve Kamerman", 9 | "email": "stevekamerman@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.1.0" 14 | }, 15 | "suggest": { 16 | "guzzlehttp/guzzle": "Guzzle ~4.0|~5.0|~6.0|~7.0" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "kamermans\\OAuth2\\": "src" 21 | } 22 | }, 23 | "autoload-dev": { 24 | "psr-4": { 25 | "kamermans\\OAuth2\\Tests\\": "tests" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Signer/AccessToken/QueryString.php: -------------------------------------------------------------------------------- 1 | fieldName = $fieldName; 14 | } 15 | 16 | public function sign($request, $accessToken) 17 | { 18 | if (Helper::guzzleIs('>=', 6)) { 19 | $uri = \GuzzleHttp\Psr7\Uri::withQueryValue( 20 | $request->getUri(), 21 | $this->fieldName, 22 | $accessToken 23 | ); 24 | 25 | return $request->withUri($uri); 26 | } 27 | 28 | $request->getQuery()->set($this->fieldName, $accessToken); 29 | return $request; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Persistence/TokenPersistenceInterface.php: -------------------------------------------------------------------------------- 1 | filepath = $filepath; 17 | } 18 | 19 | public function saveToken(TokenInterface $token) 20 | { 21 | file_put_contents($this->filepath, json_encode($token->serialize()), LOCK_EX); 22 | } 23 | 24 | public function restoreToken(TokenInterface $token) 25 | { 26 | if (!file_exists($this->filepath)) { 27 | return null; 28 | } 29 | 30 | $data = @json_decode(@file_get_contents($this->filepath), true); 31 | 32 | if (!is_array($data)) { 33 | return null; 34 | } 35 | 36 | return $token->unserialize($data); 37 | } 38 | 39 | public function deleteToken() 40 | { 41 | if (file_exists($this->filepath)) { 42 | @unlink($this->filepath); 43 | } 44 | } 45 | 46 | public function hasToken() 47 | { 48 | return file_exists($this->filepath); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Persistence/DoctrineCacheTokenPersistence.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 23 | $this->cacheKey = $cacheKey; 24 | } 25 | 26 | public function saveToken(TokenInterface $token) 27 | { 28 | $this->cache->save($this->cacheKey, $token->serialize()); 29 | } 30 | 31 | public function restoreToken(TokenInterface $token) 32 | { 33 | $data = $this->cache->fetch($this->cacheKey); 34 | 35 | if (!is_array($data)) { 36 | return null; 37 | } 38 | 39 | return $token->unserialize($data); 40 | } 41 | 42 | public function deleteToken() 43 | { 44 | $this->cache->delete($this->cacheKey); 45 | } 46 | 47 | public function hasToken() 48 | { 49 | return $this->cache->contains($this->cacheKey); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Persistence/Laravel5CacheTokenPersistence.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 23 | $this->cacheKey = $cacheKey; 24 | } 25 | 26 | public function saveToken(TokenInterface $token) 27 | { 28 | $this->cache->forever($this->cacheKey, $token->serialize()); 29 | } 30 | 31 | public function restoreToken(TokenInterface $token) 32 | { 33 | $data = $this->cache->get($this->cacheKey); 34 | 35 | if (!is_array($data)) { 36 | return null; 37 | } 38 | 39 | return $token->unserialize($data); 40 | } 41 | 42 | public function deleteToken() 43 | { 44 | $this->cache->forget($this->cacheKey); 45 | } 46 | 47 | public function hasToken() 48 | { 49 | return $this->cache->has($this->cacheKey); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Token/RawToken.php: -------------------------------------------------------------------------------- 1 | accessToken = (string) $accessToken; 18 | $this->refreshToken = (string) $refreshToken; 19 | $this->expiresAt = (int) $expiresAt; 20 | } 21 | 22 | /** 23 | * @return string The access token 24 | */ 25 | public function getAccessToken() 26 | { 27 | return $this->accessToken; 28 | } 29 | 30 | /** 31 | * @return string|null The refresh token 32 | */ 33 | public function getRefreshToken() 34 | { 35 | return $this->refreshToken; 36 | } 37 | 38 | /** 39 | * @return int The expiration timestamp 40 | */ 41 | public function getExpiresAt() 42 | { 43 | return $this->expiresAt; 44 | } 45 | 46 | /** 47 | * @return boolean 48 | */ 49 | public function isExpired() 50 | { 51 | return $this->expiresAt && $this->expiresAt < time(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/phpunit-runner.yml: -------------------------------------------------------------------------------- 1 | name: 'PHPUnit Runner' 2 | run-name: Run PHPUnit tests 3 | on: [push] 4 | jobs: 5 | matrix-testing: 6 | strategy: 7 | matrix: 8 | php: ["7.3", "7.4", "8.0", "8.1"] 9 | guzzle: ["5", "6", "7", "7-php8"] 10 | exclude: 11 | - {php: "7.3", guzzle: "5"} 12 | - {php: "7.3", guzzle: "7-php8"} 13 | - {php: "7.4", guzzle: "5"} 14 | - {php: "7.4", guzzle: "7-php8"} 15 | - {php: "8.0", guzzle: "5"} 16 | - {php: "8.1", guzzle: "5"} 17 | runs-on: ubuntu-latest 18 | container: 19 | image: kamermans/composer:php${{ matrix.php }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Cache composer dependencies 23 | id: cache-composer 24 | uses: actions/cache@v3 25 | with: 26 | path: guzzle_environments/${{ matrix.guzzle }} 27 | key: ${{ runner.os }}-build-php${{ matrix.php }}-guzzle${{ matrix.guzzle }}-composer-${{ hashFiles('guzzle_environments/**/composer.json') }} 28 | - if: ${{ steps.cache-composer.outputs.cache-hit != 'true' }} 29 | name: Composer install 30 | working-directory: guzzle_environments/${{ matrix.guzzle }} 31 | run: composer install 32 | - name: PHPUnit 33 | working-directory: guzzle_environments/${{ matrix.guzzle }} 34 | run: vendor/bin/phpunit -vvvv 35 | -------------------------------------------------------------------------------- /src/Persistence/ClosureTokenPersistence.php: -------------------------------------------------------------------------------- 1 | doSaveToken = $saveToken; 19 | $this->doRestoreToken = $restoreToken; 20 | $this->doDeleteToken = $deleteToken; 21 | $this->doHasToken = $hasToken; 22 | } 23 | 24 | public function saveToken(TokenInterface $token) 25 | { 26 | call_user_func($this->doSaveToken, $token->serialize()); 27 | } 28 | 29 | public function restoreToken(TokenInterface $token) 30 | { 31 | $data = call_user_func($this->doRestoreToken); 32 | 33 | if (!is_array($data)) { 34 | return null; 35 | } 36 | 37 | return $token->unserialize($data); 38 | } 39 | 40 | public function deleteToken() 41 | { 42 | call_user_func($this->doDeleteToken); 43 | } 44 | 45 | public function hasToken() 46 | { 47 | return call_user_func($this->doHasToken); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Persistence/SimpleCacheTokenPersistence.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 27 | $this->cacheKey = $cacheKey; 28 | } 29 | 30 | public function saveToken(TokenInterface $token) 31 | { 32 | $this->cache->set($this->cacheKey, $token->serialize()); 33 | } 34 | 35 | public function restoreToken(TokenInterface $token) 36 | { 37 | $data = $this->cache->get($this->cacheKey); 38 | 39 | if (!is_array($data)) { 40 | return null; 41 | } 42 | 43 | return $token->unserialize($data); 44 | } 45 | 46 | public function deleteToken() 47 | { 48 | $this->cache->delete($this->cacheKey); 49 | } 50 | 51 | public function hasToken() 52 | { 53 | return $this->cache->has($this->cacheKey); 54 | ; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Token/TokenSerializer.php: -------------------------------------------------------------------------------- 1 | $this->accessToken, 36 | 'refresh_token' => $this->refreshToken, 37 | 'expires_at' => $this->expiresAt, 38 | ]; 39 | } 40 | 41 | /** 42 | * Unserialize token data 43 | * @return self 44 | */ 45 | public function unserialize(array $data) 46 | { 47 | if (!isset($data['access_token'])) { 48 | throw new \InvalidArgumentException('Unable to create a RawToken without an "access_token"'); 49 | } 50 | 51 | $this->accessToken = $data['access_token']; 52 | $this->refreshToken = isset($data['refresh_token']) ? $data['refresh_token'] : null; 53 | $this->expiresAt = isset($data['expires_at']) ? $data['expires_at'] : null; 54 | 55 | return $this; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Signer/ClientCredentials/Json.php: -------------------------------------------------------------------------------- 1 | clientIdField = $clientIdField; 16 | $this->clientSecretField = $clientSecretField; 17 | } 18 | 19 | public function sign($request, $clientId, $clientSecret) 20 | { 21 | if (Helper::guzzleIs('>=', 6)) { 22 | if ($request->getHeaderLine('Content-Type') != 'application/x-www-form-urlencoded') { 23 | throw new \RuntimeException('Unable to set fields in request body'); 24 | } 25 | } 26 | 27 | parse_str((string) $request->getBody(), $data); 28 | 29 | unset($data['client_id'], $data['client_secret']); 30 | 31 | $data[$this->clientIdField] = $clientId; 32 | $data[$this->clientSecretField] = $clientSecret; 33 | 34 | $body_stream = json_encode($data); 35 | 36 | if (Helper::guzzleIs('>=', 6)) { 37 | return $request 38 | ->withHeader('Content-Type', 'application/json') 39 | ->withBody(Helper::streamFor($body_stream)); 40 | } 41 | 42 | $request->setHeader('Content-Type', 'application/json'); 43 | $request->setBody(Stream::factory($body_stream)); 44 | return $request; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/GrantType/Specific/HWIOAuthBundleRefreshToken.php: -------------------------------------------------------------------------------- 1 | securityContext = $securityContext; 34 | $this->resourceOwnerMap = $resourceOwnerMap; 35 | } 36 | 37 | public function getRawData(SignerInterface $clientCredentialsSigner, $refreshToken = null) 38 | { 39 | $token = $this->securityContext->getToken(); 40 | $resourceName = $token->getResourceOwnerName(); 41 | $resourceOwner = $this->resourceOwnerMap->getResourceOwnerByName($resourceName); 42 | 43 | $data = $resourceOwner->refreshAccessToken($refreshToken); 44 | $token->setRawToken($data); 45 | 46 | return $data; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Signer/ClientCredentials/PostFormData.php: -------------------------------------------------------------------------------- 1 | clientIdField = $clientIdField; 16 | $this->clientSecretField = $clientSecretField; 17 | } 18 | 19 | public function sign($request, $clientId, $clientSecret) 20 | { 21 | if (Helper::guzzleIs('>=', 6)) { 22 | if ($request->getHeaderLine('Content-Type') != 'application/x-www-form-urlencoded') { 23 | throw new \RuntimeException('Unable to set fields in request body'); 24 | } 25 | 26 | parse_str($request->getBody(), $data); 27 | $data[$this->clientIdField] = $clientId; 28 | $data[$this->clientSecretField] = $clientSecret; 29 | 30 | $body_stream = Helper::streamFor(http_build_query($data, '', '&')); 31 | return $request->withBody($body_stream); 32 | } 33 | 34 | $body = $request->getBody(); 35 | 36 | if (!($body instanceof PostBodyInterface)) { 37 | throw new \RuntimeException('Unable to set fields in request body'); 38 | } 39 | 40 | $body->setField($this->clientIdField, $clientId); 41 | $body->setField($this->clientSecretField, $clientSecret); 42 | 43 | return $request; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Token/RawTokenFactory.php: -------------------------------------------------------------------------------- 1 | getRefreshToken(); 28 | } 29 | 30 | // Read the "expires_in" attribute 31 | $expiresIn = isset($data['expires_in']) ? (int) $data['expires_in'] : null; 32 | 33 | // Facebook unfortunately breaks the spec by using 'expires' instead of 'expires_in' 34 | if (!$expiresIn && isset($data['expires'])) { 35 | $expiresIn = (int) $data['expires']; 36 | } 37 | 38 | // Set the absolute expiration if a relative expiration was provided 39 | if ($expiresIn) { 40 | $expiresAt = time() + $expiresIn; 41 | } 42 | 43 | return new RawToken($accessToken, $refreshToken, $expiresAt); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Utils/Helper.php: -------------------------------------------------------------------------------- 1 | 5.1, but I don't 39 | $guzzle_version = preg_replace('/(\.0+)+$/', '', $guzzle_version); 40 | $version = preg_replace('/(\.0+)+$/', '', $version); 41 | 42 | if ($operator === '~') { 43 | return self::fuzzyVersionCompare($version, $guzzle_version); 44 | } 45 | 46 | return version_compare($guzzle_version, $version, $operator); 47 | } 48 | 49 | private static function fuzzyVersionCompare($version, $guzzle_version) 50 | { 51 | $num_version_segments = substr_count($version, '.') + 1; 52 | $num_guzzle_segments = substr_count($guzzle_version, '.') + 1; 53 | 54 | if ($num_version_segments < $num_guzzle_segments) { 55 | // Shorten Guzzle version 56 | $guzzle_version = implode('.', array_slice(explode('.', $guzzle_version), 0, $num_version_segments)); 57 | } elseif ($num_version_segments > $num_guzzle_segments) { 58 | // Shorten Test version 59 | $version = implode('.', array_slice(explode('.', $version), 0, $num_guzzle_segments)); 60 | } 61 | 62 | return version_compare($guzzle_version, $version, '=='); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/OAuth2Subscriber.php: -------------------------------------------------------------------------------- 1 | ['onBefore', RequestEvents::VERIFY_RESPONSE + 100], 21 | 'error' => ['onError', RequestEvents::EARLY - 100], 22 | ]; 23 | } 24 | 25 | /** 26 | * Request before-send event handler. 27 | * 28 | * Adds the Authorization header if an access token was found. 29 | * 30 | * @param BeforeEvent $event Event received 31 | */ 32 | public function onBefore(BeforeEvent $event) 33 | { 34 | $request = $event->getRequest(); 35 | 36 | // Only sign requests using "auth"="oauth" 37 | if ('oauth' !== $request->getConfig()['auth']) { 38 | return; 39 | } 40 | 41 | $this->signRequest($request); 42 | } 43 | 44 | /** 45 | * Request error event handler. 46 | * 47 | * Handles unauthorized errors by acquiring a new access token and 48 | * retrying the request. 49 | * 50 | * @param ErrorEvent $event Event received 51 | */ 52 | public function onError(ErrorEvent $event) 53 | { 54 | $request = $event->getRequest(); 55 | $response = $event->getResponse(); 56 | 57 | // Only sign requests using "auth"="oauth" 58 | if ('oauth' !== $request->getConfig()['auth']) { 59 | return; 60 | } 61 | 62 | // Only deal with Unauthorized response. 63 | if ($response && $response->getStatusCode() != 401) { 64 | return; 65 | } 66 | 67 | // If we already retried once, give up. 68 | if ($request->getHeader('X-Guzzle-Retry')) { 69 | return; 70 | } 71 | 72 | // Delete the previous access token, if any 73 | $this->deleteAccessToken(); 74 | 75 | // Acquire a new access token, and retry the request. 76 | $accessToken = $this->getAccessToken(); 77 | if ($accessToken != null) { 78 | $newRequest = clone $request; 79 | $newRequest->setHeader('X-Guzzle-Retry', '1'); 80 | 81 | $this->accessTokenSigner->sign($newRequest, $accessToken); 82 | 83 | $event->intercept( 84 | $event->getClient()->send($newRequest) 85 | ); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/GrantType/PasswordCredentials.php: -------------------------------------------------------------------------------- 1 | client = $client; 35 | $this->config = Collection::fromConfig( 36 | $config, 37 | // Default 38 | [ 39 | 'client_secret' => '', 40 | 'scope' => '', 41 | ], 42 | // Required 43 | [ 44 | 'client_id', 45 | 'username', 46 | 'password', 47 | ] 48 | ); 49 | } 50 | 51 | public function getRawData(SignerInterface $clientCredentialsSigner, $refreshToken = null) 52 | { 53 | if (Helper::guzzleIs('>=', 6)) { 54 | $request = (new \GuzzleHttp\Psr7\Request('POST', '')) 55 | ->withBody($this->getPostBody()) 56 | ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 57 | } else { 58 | $request = $this->client->createRequest('POST', null); 59 | $request->setBody($this->getPostBody()); 60 | } 61 | 62 | $request = $clientCredentialsSigner->sign( 63 | $request, 64 | $this->config['client_id'], 65 | $this->config['client_secret'] 66 | ); 67 | 68 | $response = $this->client->send($request); 69 | $rawData = json_decode($response->getBody(), true); 70 | 71 | return is_array($rawData) ? $rawData : []; 72 | } 73 | 74 | /** 75 | * @return PostBody|\Psr\Http\Message\StreamInterface 76 | */ 77 | protected function getPostBody() 78 | { 79 | if (Helper::guzzleIs('>=', '6')) { 80 | $data = [ 81 | 'grant_type' => 'password', 82 | 'username' => $this->config['username'], 83 | 'password' => $this->config['password'], 84 | ]; 85 | 86 | if ($this->config['scope']) { 87 | $data['scope'] = $this->config['scope']; 88 | } 89 | 90 | return Helper::streamFor(http_build_query($data, '', '&')); 91 | } 92 | 93 | $postBody = new PostBody(); 94 | $postBody->replaceFields([ 95 | 'grant_type' => 'password', 96 | 'username' => $this->config['username'], 97 | 'password' => $this->config['password'], 98 | ]); 99 | 100 | if ($this->config['scope']) { 101 | $postBody->setField('scope', $this->config['scope']); 102 | } 103 | 104 | return $postBody; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/OAuth2Middleware.php: -------------------------------------------------------------------------------- 1 | signRequest($request); 31 | 32 | return $handler($request, $options)->then( 33 | $this->onFulfilled($request, $options, $handler), 34 | $this->onRejected($request, $options, $handler) 35 | ); 36 | }; 37 | } 38 | 39 | /** 40 | * Request error event handler. 41 | * 42 | * Handles unauthorized errors by acquiring a new access token and 43 | * retrying the request. 44 | * 45 | * @param \Psr\Http\Message\RequestInterface $request 46 | * @param array $options 47 | * @param callable $handler 48 | * 49 | * @return callable 50 | */ 51 | private function onFulfilled(RequestInterface $request, array $options, $handler) 52 | { 53 | return function ($response) use ($request, $options, $handler) { 54 | // Only deal with Unauthorized response. 55 | if ($response && $response->getStatusCode() != 401) { 56 | return $response; 57 | } 58 | 59 | // If we already retried once, give up. 60 | // This is extremely unlikely in Guzzle 6+ since we're using promises 61 | // to check the response - looping should be impossible, but I'm leaving 62 | // the code here in case something interferes with the Middleware 63 | if ($request->hasHeader('X-Guzzle-Retry')) { 64 | return $response; 65 | } 66 | 67 | // Delete the previous access token, if any 68 | $this->deleteAccessToken(); 69 | 70 | // Acquire a new access token, and retry the request. 71 | $accessToken = $this->getAccessToken(); 72 | if ($accessToken === null) { 73 | return $response; 74 | } 75 | 76 | $request = $request->withHeader('X-Guzzle-Retry', '1'); 77 | $request = $this->signRequest($request); 78 | 79 | return $handler($request, $options); 80 | }; 81 | } 82 | 83 | private function onRejected(RequestInterface $request, array $options, $handler) 84 | { 85 | return function ($reason) use ($request, $options) { 86 | if (class_exists('\GuzzleHttp\Promise\Create')) { 87 | return \GuzzleHttp\Promise\Create::rejectionFor($reason); 88 | } 89 | 90 | // As of Guzzle Promises 2.0.0, the rejection_for function is deprecated and replaced with Create::rejectionFor 91 | return \GuzzleHttp\Promise\rejection_for($reason); 92 | }; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/GrantType/ClientCredentials.php: -------------------------------------------------------------------------------- 1 | client = $client; 39 | $this->config = Collection::fromConfig( 40 | $config, 41 | // Defaults 42 | [ 43 | 'client_secret' => '', 44 | 'scope' => '', 45 | ], 46 | // Required 47 | [ 48 | 'client_id', 49 | ] 50 | ); 51 | } 52 | 53 | public function getRawData(SignerInterface $clientCredentialsSigner, $refreshToken = null) 54 | { 55 | if (Helper::guzzleIs('>=', 6)) { 56 | $request = (new \GuzzleHttp\Psr7\Request('POST', '')) 57 | ->withBody($this->getPostBody()) 58 | ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 59 | } else { 60 | $request = $this->client->createRequest('POST', null); 61 | $request->setBody($this->getPostBody()); 62 | } 63 | 64 | $request = $clientCredentialsSigner->sign( 65 | $request, 66 | $this->config['client_id'], 67 | $this->config['client_secret'] 68 | ); 69 | 70 | $response = $this->client->send($request); 71 | $rawData = json_decode($response->getBody(), true); 72 | 73 | return is_array($rawData) ? $rawData : []; 74 | } 75 | 76 | /** 77 | * @return PostBody|\Psr\Http\Message\StreamInterface 78 | */ 79 | protected function getPostBody() 80 | { 81 | if (Helper::guzzleIs('>=', '6')) { 82 | $data = [ 83 | 'grant_type' => 'client_credentials' 84 | ]; 85 | 86 | if ($this->config['scope']) { 87 | $data['scope'] = $this->config['scope']; 88 | } 89 | 90 | if (!empty($this->config['audience'])) { 91 | $data['audience'] = $this->config['audience']; 92 | } 93 | 94 | return Helper::streamFor(http_build_query($data, '', '&')); 95 | } 96 | 97 | $postBody = new PostBody(); 98 | $postBody->replaceFields([ 99 | 'grant_type' => 'client_credentials' 100 | ]); 101 | 102 | if ($this->config['scope']) { 103 | $postBody->setField('scope', $this->config['scope']); 104 | } 105 | 106 | if (!empty($this->config['audience'])) { 107 | $postBody->setField('audience', $this->config['audience']); 108 | } 109 | 110 | return $postBody; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/GrantType/RefreshToken.php: -------------------------------------------------------------------------------- 1 | client = $client; 35 | $this->config = Collection::fromConfig( 36 | $config, 37 | // Defaults 38 | [ 39 | 'client_secret' => '', 40 | 'refresh_token' => '', 41 | 'scope' => '', 42 | ], 43 | // Required 44 | [ 45 | 'client_id', 46 | ] 47 | ); 48 | } 49 | 50 | public function getRawData(SignerInterface $clientCredentialsSigner, $refreshToken = null) 51 | { 52 | if (Helper::guzzleIs('>=', 6)) { 53 | $request = (new \GuzzleHttp\Psr7\Request('POST', '')) 54 | ->withBody($this->getPostBody($refreshToken)) 55 | ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 56 | } else { 57 | $request = $this->client->createRequest('POST', null); 58 | $request->setBody($this->getPostBody($refreshToken)); 59 | } 60 | 61 | $request = $clientCredentialsSigner->sign( 62 | $request, 63 | $this->config['client_id'], 64 | $this->config['client_secret'] 65 | ); 66 | 67 | $response = $this->client->send($request); 68 | $rawData = json_decode($response->getBody(), true); 69 | 70 | return is_array($rawData) ? $rawData : []; 71 | } 72 | 73 | /** 74 | * @return PostBody|\Psr\Http\Message\StreamInterface 75 | */ 76 | protected function getPostBody($refreshToken) 77 | { 78 | if (Helper::guzzleIs('>=', '6')) { 79 | $data = [ 80 | 'grant_type' => 'refresh_token', 81 | // If no refresh token was provided to the method, use the one 82 | // provided to the constructor. 83 | 'refresh_token' => $refreshToken ?: $this->config['refresh_token'], 84 | ]; 85 | 86 | if ($this->config['scope']) { 87 | $data['scope'] = $this->config['scope']; 88 | } 89 | 90 | return Helper::streamFor(http_build_query($data, '', '&')); 91 | } 92 | 93 | $postBody = new PostBody(); 94 | $postBody->replaceFields([ 95 | 'grant_type' => 'refresh_token', 96 | // If no refresh token was provided to the method, use the one 97 | // provided to the constructor. 98 | 'refresh_token' => $refreshToken ?: $this->config['refresh_token'], 99 | ]); 100 | 101 | if ($this->config['scope']) { 102 | $postBody->setField('scope', $this->config['scope']); 103 | } 104 | 105 | return $postBody; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/GrantType/AuthorizationCode.php: -------------------------------------------------------------------------------- 1 | client = $client; 35 | $this->config = Collection::fromConfig( 36 | $config, 37 | // Defaults 38 | [ 39 | 'client_secret' => '', 40 | 'scope' => '', 41 | 'redirect_uri' => '', 42 | ], 43 | // Required 44 | [ 45 | 'client_id', 46 | 'code', 47 | ] 48 | ); 49 | } 50 | 51 | public function getRawData(SignerInterface $clientCredentialsSigner, $refreshToken = null) 52 | { 53 | if (Helper::guzzleIs('>=', 6)) { 54 | $request = (new \GuzzleHttp\Psr7\Request('POST', '')) 55 | ->withBody($this->getPostBody()) 56 | ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 57 | } else { 58 | $request = $this->client->createRequest('POST', null); 59 | $request->setBody($this->getPostBody()); 60 | } 61 | 62 | $request = $clientCredentialsSigner->sign( 63 | $request, 64 | $this->config['client_id'], 65 | $this->config['client_secret'] 66 | ); 67 | 68 | $response = $this->client->send($request); 69 | $rawData = json_decode($response->getBody(), true); 70 | 71 | return is_array($rawData) ? $rawData : []; 72 | } 73 | 74 | /** 75 | * @return PostBody|\Psr\Http\Message\StreamInterface 76 | */ 77 | protected function getPostBody() 78 | { 79 | if (Helper::guzzleIs('>=', '6')) { 80 | $data = [ 81 | 'grant_type' => 'authorization_code', 82 | 'code' => $this->config['code'], 83 | ]; 84 | 85 | if ($this->config['scope']) { 86 | $data['scope'] = $this->config['scope']; 87 | } 88 | 89 | if ($this->config['redirect_uri']) { 90 | $data['redirect_uri'] = $this->config['redirect_uri']; 91 | } 92 | 93 | return Helper::streamFor(http_build_query($data, '', '&')); 94 | } 95 | 96 | $postBody = new PostBody(); 97 | $postBody->replaceFields([ 98 | 'grant_type' => 'authorization_code', 99 | 'code' => $this->config['code'], 100 | ]); 101 | 102 | if ($this->config['scope']) { 103 | $postBody->setField('scope', $this->config['scope']); 104 | } 105 | 106 | if ($this->config['redirect_uri']) { 107 | $postBody->setField('redirect_uri', $this->config['redirect_uri']); 108 | } 109 | 110 | return $postBody; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/GrantType/Specific/GithubApplication.php: -------------------------------------------------------------------------------- 1 | client = $client; 42 | $this->config = Collection::fromConfig( 43 | $config, 44 | // Defaults 45 | [ 46 | 'scope' => '', 47 | ], 48 | // Required 49 | [ 50 | 'client_id', 51 | 'client_secret', 52 | 'note', 53 | 'username', 54 | 'password', 55 | ] 56 | ); 57 | } 58 | 59 | public function getRawData(SignerInterface $clientCredentialsSigner, $refreshToken = null) 60 | { 61 | if (Helper::guzzleIs('>=', 6)) { 62 | $request = (new \GuzzleHttp\Psr7\Request('POST', '')) 63 | ->withBody($this->getPostBody()) 64 | ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 65 | } else { 66 | $request = $this->client->createRequest('POST', null); 67 | $request->setBody($this->getPostBody()); 68 | } 69 | 70 | $clientCredentialsSigner->sign( 71 | $request, 72 | $this->config['username'], 73 | $this->config['password'] 74 | ); 75 | 76 | $response = $this->client->send($request); 77 | 78 | // Restructure some fields from the GitHub response 79 | /* Example Response: 80 | { 81 | "id": 00913101, 82 | "url": "https://api.github.com/authorizations/00913101", 83 | "app": 84 | { 85 | "name": "OAuthTestApplication", 86 | "url": "http://localhost/test", 87 | "client_id": "042c2d7a8a216e2bbf82", 88 | }, 89 | "token": "ab3758bd55c324cfee74c87fcc704656af6d98f6", 90 | "note": "OAuth Test Token", 91 | "note_url": NULL, 92 | "created_at": "2014-10-10T19:05:00Z", 93 | "updated_at": "2014-10-10T19:05:00Z", 94 | "scopes": 95 | [ 96 | "public_repo", 97 | "repo", 98 | "user", 99 | ], 100 | } 101 | */ 102 | 103 | $data = json_decode($response->getBody(), true); 104 | $data['access_token'] = $data['token']; 105 | unset($data['token']); 106 | 107 | return $data; 108 | } 109 | 110 | protected function getPostBody() 111 | { 112 | $postBody = [ 113 | 'client_id' => $this->config['client_id'], 114 | 'client_secret' => $this->config['client_secret'], 115 | 'note' => $this->config['note'], 116 | 'scopes' => [], 117 | ]; 118 | 119 | if ($this->config['scope']) { 120 | // In github's API, "scope" is called "scopes" and is passed as a JSON array 121 | $postBody['scopes'] = explode(',', $this->config['scope']); 122 | } 123 | 124 | if ($this->config['note_url']) { 125 | $postBody['note_url'] = $this->config['note_url']; 126 | } 127 | 128 | $postBody = json_encode($postBody); 129 | 130 | return Helper::guzzleIs('<', 6)? Stream::factory($postBody): Helper::streamFor($postBody); 131 | } 132 | 133 | /** 134 | * Helper function to parse the GitHub HTTP Response header "Link", which 135 | * contains links to next and/or previous "pages" of data. 136 | * 137 | * @param GuzzleHttpMessageResponse $response 138 | * 139 | * @return array Array containing keys: next, prev, first, last 140 | */ 141 | public static function parseLinkHeader(Response $response) 142 | { 143 | $linkHeader = $response->getHeader('Link'); 144 | 145 | if (!strpos($linkHeader, 'rel')) { 146 | return null; 147 | } 148 | 149 | $out = [ 150 | "next" => null, 151 | "last" => null, 152 | "prev" => null, 153 | "first" => null, 154 | ]; 155 | 156 | $links = explode(',', $linkHeader); 157 | foreach ($links as $link) { 158 | $parts = explode(';', $link); 159 | if (count($parts) < 2) { 160 | continue; 161 | } 162 | 163 | // Get the URL 164 | $url = trim(array_shift($parts), '<> '); 165 | $relParts = explode('=', trim(array_shift($parts))); 166 | 167 | if (count($relParts) !== 2 || $relParts[0] != 'rel') { 168 | continue; 169 | } 170 | 171 | // Get the rel="" value (next, prev, first, last) 172 | $rel = trim($relParts[1], ' "\''); 173 | $out[$rel] = $url; 174 | } 175 | 176 | return $out; 177 | } 178 | 179 | /** 180 | * Helper function to retrieve all the "pages" of results from a GitHub API call 181 | * and returns them as a single array 182 | * 183 | * @param ClientInterface $client 184 | * @param string $url 185 | * @return array 186 | */ 187 | public static function getAllResults(ClientInterface $client, $url) 188 | { 189 | $data = []; 190 | do { 191 | $response = $client->get($url); 192 | $data = array_merge($data, $response->json()); 193 | 194 | $url = GithubApplication::parseLinkHeader($response)['next']; 195 | } while ($url); 196 | 197 | return $data; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Utils/Collection.php: -------------------------------------------------------------------------------- 1 | data = $data; 29 | } 30 | 31 | public function getIterator(): Traversable 32 | { 33 | return new ArrayIterator($this->data); 34 | } 35 | 36 | public function offsetGet($offset): ?string 37 | { 38 | return isset($this->data[$offset]) ? $this->data[$offset] : null; 39 | } 40 | 41 | public function offsetSet($offset, $value): void 42 | { 43 | $this->data[$offset] = $value; 44 | } 45 | 46 | public function offsetExists($offset): bool 47 | { 48 | return isset($this->data[$offset]); 49 | } 50 | 51 | public function offsetUnset($offset): void 52 | { 53 | unset($this->data[$offset]); 54 | } 55 | 56 | public function toArray(): array 57 | { 58 | return $this->data; 59 | } 60 | 61 | public function count(): int 62 | { 63 | return count($this->data); 64 | } 65 | 66 | /** 67 | * Create a new collection from an array, validate the keys, and add default 68 | * values where missing 69 | * 70 | * @param array $config Configuration values to apply. 71 | * @param array $defaults Default parameters 72 | * @param array $required Required parameter names 73 | * 74 | * @return self 75 | * @throws \InvalidArgumentException if a parameter is missing 76 | */ 77 | public static function fromConfig( 78 | array $config = [], 79 | array $defaults = [], 80 | array $required = [] 81 | ): Collection 82 | { 83 | $data = $config + $defaults; 84 | 85 | if ($missing = array_diff($required, array_keys($data))) { 86 | throw new \InvalidArgumentException( 87 | 'Config is missing the following keys: ' . 88 | implode(', ', $missing) 89 | ); 90 | } 91 | 92 | return new self($data); 93 | } 94 | 95 | /** 96 | * Removes all key value pairs 97 | * 98 | * @return Collection 99 | */ 100 | public function clear(): Collection 101 | { 102 | $this->data = []; 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Get a specific key value. 109 | * 110 | * @param string $key Key to retrieve. 111 | * 112 | * @return mixed|null Value of the key or NULL 113 | */ 114 | public function get(string $key): string 115 | { 116 | return isset($this->data[$key]) ? $this->data[$key] : null; 117 | } 118 | 119 | /** 120 | * Set a key value pair 121 | * 122 | * @param string $key Key to set 123 | * @param string $value Value to set 124 | * 125 | * @return Collection Returns a reference to the object 126 | */ 127 | public function set(string $key, string $value): Collection 128 | { 129 | $this->data[$key] = $value; 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * Add a value to a key. If a key of the same name has already been added, 136 | * the key value will be converted into an array and the new value will be 137 | * pushed to the end of the array. 138 | * 139 | * @param string $key Key to add 140 | * @param mixed $value Value to add to the key 141 | * 142 | * @return Collection Returns a reference to the object. 143 | */ 144 | public function add($key, $value): Collection 145 | { 146 | if (!array_key_exists($key, $this->data)) { 147 | $this->data[$key] = $value; 148 | } elseif (is_array($this->data[$key])) { 149 | $this->data[$key][] = $value; 150 | } else { 151 | $this->data[$key] = [$this->data[$key], $value]; 152 | } 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * Remove a specific key value pair 159 | * 160 | * @param string $key A key to remove 161 | * 162 | * @return Collection 163 | */ 164 | public function remove(string $key): Collection 165 | { 166 | unset($this->data[$key]); 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * Get all keys in the collection 173 | * 174 | * @return array 175 | */ 176 | public function getKeys(): array 177 | { 178 | return array_keys($this->data); 179 | } 180 | 181 | /** 182 | * Returns whether or not the specified key is present. 183 | * 184 | * @param string $key The key for which to check the existence. 185 | * 186 | * @return bool 187 | */ 188 | public function hasKey(string $key): bool 189 | { 190 | return array_key_exists($key, $this->data); 191 | } 192 | 193 | /** 194 | * Checks if any keys contains a certain value 195 | * 196 | * @param string $value Value to search for 197 | * 198 | * @return mixed Returns the key if the value was found or NULL if the value 199 | * was not found. 200 | */ 201 | public function hasValue(string $value): ?string 202 | { 203 | $val = array_search($value, $this->data, true); 204 | return $val === false ? null : $val; 205 | } 206 | 207 | /** 208 | * Replace the data of the object with the value of an array 209 | * 210 | * @param array $data Associative array of data 211 | * 212 | * @return Collection Returns a reference to the object 213 | */ 214 | public function replace(array $data): Collection 215 | { 216 | $this->data = $data; 217 | 218 | return $this; 219 | } 220 | 221 | /** 222 | * Add and merge in a Collection or array of key value pair data. 223 | * 224 | * @param Collection|array $data Associative array of key value pair data 225 | * 226 | * @return Collection Returns a reference to the object. 227 | */ 228 | public function merge(Traversable $data): Collection 229 | { 230 | foreach ($data as $key => $value) { 231 | $this->add($key, $value); 232 | } 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * Over write key value pairs in this collection with all of the data from 239 | * an array or collection. 240 | * 241 | * @param array|\Traversable $data Values to override over this config 242 | * 243 | * @return self 244 | */ 245 | public function overwriteWith(Traversable $data): Collection 246 | { 247 | if (is_array($data)) { 248 | $this->data = $data + $this->data; 249 | } elseif ($data instanceof Collection) { 250 | $this->data = $data->toArray() + $this->data; 251 | } else { 252 | foreach ($data as $key => $value) { 253 | $this->data[$key] = $value; 254 | } 255 | } 256 | 257 | return $this; 258 | } 259 | 260 | /** 261 | * Returns a Collection containing all the elements of the collection after 262 | * applying the callback function to each one. 263 | * 264 | * The callable should accept three arguments: 265 | * - (string) $key 266 | * - (string) $value 267 | * - (array) $context 268 | * 269 | * The callable must return a the altered or unaltered value. 270 | * 271 | * @param callable $closure Map function to apply 272 | * @param array $context Context to pass to the callable 273 | * 274 | * @return Collection 275 | */ 276 | public function map(callable $closure, array $context = []): Collection 277 | { 278 | $collection = new static(); 279 | foreach ($this as $key => $value) { 280 | $collection[$key] = $closure($key, $value, $context); 281 | } 282 | 283 | return $collection; 284 | } 285 | 286 | /** 287 | * Iterates over each key value pair in the collection passing them to the 288 | * callable. If the callable returns true, the current value from input is 289 | * returned into the result Collection. 290 | * 291 | * The callable must accept two arguments: 292 | * - (string) $key 293 | * - (string) $value 294 | * 295 | * @param callable $closure Evaluation function 296 | * 297 | * @return Collection 298 | */ 299 | public function filter(callable $closure): Collection 300 | { 301 | $collection = new static(); 302 | foreach ($this->data as $key => $value) { 303 | if ($closure($key, $value)) { 304 | $collection[$key] = $value; 305 | } 306 | } 307 | 308 | return $collection; 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/OAuth2Handler.php: -------------------------------------------------------------------------------- 1 | grantType = $grantType; 85 | $this->refreshTokenGrantType = $refreshTokenGrantType; 86 | $this->clientCredentialsSigner = $clientCredentialsSigner; 87 | $this->accessTokenSigner = $accessTokenSigner; 88 | 89 | if ($this->clientCredentialsSigner === null) { 90 | $this->clientCredentialsSigner = new Signer\ClientCredentials\BasicAuth(); 91 | } 92 | 93 | if ($this->accessTokenSigner === null) { 94 | $this->accessTokenSigner = new Signer\AccessToken\BearerAuth(); 95 | } 96 | 97 | $this->tokenPersistence = new Persistence\NullTokenPersistence(); 98 | $this->tokenFactory = new Token\RawTokenFactory(); 99 | $this->newTokenSupplier = function(){ return new Token\RawToken(); }; 100 | } 101 | 102 | /** 103 | * @param Signer\ClientCredentials\SignerInterface $signer 104 | * 105 | * @return self 106 | */ 107 | public function setClientCredentialsSigner(Signer\ClientCredentials\SignerInterface $signer) 108 | { 109 | $this->clientCredentialsSigner = $signer; 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * @param Signer\AccessToken\SignerInterface $signer 116 | * 117 | * @return self 118 | */ 119 | public function setAccessTokenSigner(Signer\AccessToken\SignerInterface $signer) 120 | { 121 | $this->accessTokenSigner = $signer; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * @param Persistence\TokenPersistenceInterface $tokenPersistence 128 | * 129 | * @return self 130 | */ 131 | public function setTokenPersistence(Persistence\TokenPersistenceInterface $tokenPersistence) 132 | { 133 | $this->tokenPersistence = $tokenPersistence; 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * @param callable $tokenFactory 140 | * 141 | * @return self 142 | */ 143 | public function setTokenFactory(callable $tokenFactory) 144 | { 145 | $this->tokenFactory = $tokenFactory; 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * @param callable $tokenSupplier the new token supplier 152 | * 153 | * @return self 154 | */ 155 | public function setNewTokenSupplier(callable $tokenSupplier) { 156 | $this->newTokenSupplier = $tokenSupplier; 157 | 158 | return $this; 159 | } 160 | 161 | /** 162 | * Manually set the access token. 163 | * 164 | * @param string|array|Token\TokenInterface $token An array of token data, an access token string, or a TokenInterface object 165 | * 166 | * @return self 167 | */ 168 | public function setAccessToken($token) 169 | { 170 | if ($token instanceof Token\TokenInterface) { 171 | $this->rawToken = $token; 172 | } else { 173 | $this->rawToken = is_array($token) ? 174 | $this->tokenFactory($token) : 175 | $this->tokenFactory(['access_token' => $token]); 176 | } 177 | 178 | if ($this->rawToken === null) { 179 | throw new Exception\OAuth2Exception("setAccessToken() takes a string, array or TokenInterface object"); 180 | } 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Forcefully delete an access token, even if it's valid 187 | */ 188 | public function deleteAccessToken() 189 | { 190 | $this->rawToken = null; 191 | $this->tokenPersistence->deleteToken(); 192 | } 193 | 194 | /** 195 | * Get a valid access token. 196 | * 197 | * @return string|null A valid access token or null if unable to get one 198 | * 199 | * @throws Exception\AccessTokenRequestException while trying to run `requestNewAccessToken` method 200 | */ 201 | public function getAccessToken() 202 | { 203 | // If token is not set try to get it from the persistent storage. 204 | if ($this->rawToken === null) { 205 | $this->rawToken = $this->tokenPersistence->restoreToken(call_user_func($this->newTokenSupplier)); 206 | } 207 | 208 | // If token is not set or expired then try to acquire a new one... 209 | if ($this->rawToken === null || $this->rawToken->isExpired()) { 210 | 211 | // Hydrate `rawToken` with a new access token 212 | $this->requestNewAccessToken(); 213 | 214 | // ...and save it. 215 | if ($this->rawToken) { 216 | $this->tokenPersistence->saveToken($this->rawToken); 217 | } 218 | } 219 | 220 | return $this->rawToken? $this->rawToken->getAccessToken(): null; 221 | } 222 | 223 | /** 224 | * Gets the current Token object 225 | * 226 | * @return Token\TokenInterface|null 227 | */ 228 | public function getRawToken() 229 | { 230 | return $this->rawToken; 231 | } 232 | 233 | protected function signRequest($request) 234 | { 235 | $accessToken = $this->getAccessToken(); 236 | 237 | if ($accessToken === null) { 238 | return $request; 239 | } 240 | 241 | return $this->accessTokenSigner->sign($request, $accessToken); 242 | } 243 | 244 | /** 245 | * Helper method for (callable)tokenFactory 246 | * 247 | * @return Token\TokenInterface 248 | */ 249 | protected function tokenFactory() 250 | { 251 | return call_user_func_array($this->tokenFactory, func_get_args()); 252 | } 253 | 254 | /** 255 | * Acquire a new access token from the server. 256 | * 257 | * @throws Exception\AccessTokenRequestException 258 | */ 259 | protected function requestNewAccessToken() 260 | { 261 | if ($this->refreshTokenGrantType && $this->rawToken && $this->rawToken->getRefreshToken()) { 262 | try { 263 | // Get an access token using the stored refresh token. 264 | $rawData = $this->refreshTokenGrantType->getRawData( 265 | $this->clientCredentialsSigner, 266 | $this->rawToken->getRefreshToken() 267 | ); 268 | 269 | $this->rawToken = $this->tokenFactory($rawData, $this->rawToken); 270 | 271 | return; 272 | } catch (BadResponseException $e) { 273 | // If the refresh token is invalid, then clear the entire token information. 274 | $this->rawToken = null; 275 | } 276 | } 277 | 278 | if ($this->grantType === null) { 279 | throw new Exception\ReauthorizationException('You must specify a grantType class to request an access token'); 280 | } 281 | 282 | try { 283 | // Request an access token using the main grant type. 284 | $rawData = $this->grantType->getRawData($this->clientCredentialsSigner); 285 | 286 | $this->rawToken = $this->tokenFactory($rawData); 287 | } catch (BadResponseException $e) { 288 | throw new Exception\AccessTokenRequestException('Unable to request a new access token: ' . $e->getMessage(), $e); 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guzzle OAuth 2.0 Subscriber 2 | 3 | > Tested with Guzzle 4, 5, 6, 7 and PHP 7.1, 7.2, 7.3, 7.4, 8.0 and 8.1. 4 | 5 | This is an OAuth 2.0 client for Guzzle which aims to be 100% compatible with Guzzle 4, 5, 6, 7 and all future versions within a single package. 6 | Although I love Guzzle, its interfaces keep changing, causing massive breaking changes every 12 months or so, so I have created this package 7 | to help reduce the dependency hell that most third-party Guzzle dependencies bring with them. I wrote the official Guzzle OAuth 2.0 plugin 8 | which is still on the `oauth2` branch, [over at the official Guzzle repo](https://github.com/guzzle/oauth-subscriber/tree/oauth2), but I 9 | see that they have dropped support for Guzzle < v6 on `master`, which prompted me to split this back off to a separate package. 10 | 11 | ## Features 12 | 13 | - Acquires access tokens via one of the supported grant types (code, client credentials, 14 | user credentials, refresh token). Or you can set an access token yourself. 15 | - Supports refresh tokens (stores them and uses them to get new access tokens). 16 | - Handles token expiration (acquires new tokens and retries failed requests). 17 | - Allows storage and lookup of access tokens via callbacks 18 | 19 | 20 | ## Installation 21 | 22 | This project can be installed using Composer. Run `composer require kamermans/guzzle-oauth2-subscriber` or add the following to your `composer.json`: 23 | 24 | ```javascript 25 | { 26 | "require": { 27 | "kamermans/guzzle-oauth2-subscriber": "~1.1" 28 | } 29 | } 30 | ``` 31 | 32 | ## Usage 33 | 34 | This plugin integrates seamlessly with Guzzle, transparently adding authentication to outgoing requests and optionally attempting re-authorization if the access token is no longer valid. 35 | 36 | There are multiple grant types available like `PasswordCredentials`, `ClientCredentials` and `AuthorizationCode`. 37 | 38 | ### Guzzle 4 & 5 vs Guzzle 6+ 39 | With the Guzzle 6 release, most of the library was refactored or completely rewritten, and as such, the integration of this library is different. 40 | 41 | #### Emitters (Guzzle 4 & 5) 42 | Guzzle 4 & 5 use **Event Subscribers**, and this library includes `OAuth2Subscriber` for that purpose: 43 | 44 | ```php 45 | $oauth = new OAuth2Subscriber($grant_type); 46 | 47 | $client = new Client([ 48 | 'auth' => 'oauth', 49 | ]); 50 | 51 | $client->getEmitter()->attach($oauth); 52 | ``` 53 | 54 | #### Middleware (Guzzle 6+) 55 | Starting with Guzzle 6, **Middleware** is used to integrate OAuth, and this library includes `OAuth2Middleware` for that purpose: 56 | 57 | ```php 58 | $oauth = new OAuth2Middleware($grant_type); 59 | 60 | $stack = HandlerStack::create(); 61 | $stack->push($oauth); 62 | 63 | $client = new Client([ 64 | 'auth' => 'oauth', 65 | 'handler' => $stack, 66 | ]); 67 | ``` 68 | 69 | Alternatively, you can add the middleware to an existing Guzzle Client: 70 | 71 | ```php 72 | $oauth = new OAuth2Middleware($grant_type); 73 | $client->getConfig('handler')->push($oauth); 74 | ``` 75 | 76 | 77 | ### Client Credentials Example 78 | Client credentials are normally used in server-to-server authentication. With this grant type, a client is requesting authorization in its own behalf, so there are only two parties involved. At a minimum, a `client_id` and `client_secret` are required, although many services require a `scope` and other parameters. 79 | 80 | Here's an example of the client credentials method in Guzzle 4 and Guzzle 5: 81 | 82 | ```php 83 | use kamermans\OAuth2\GrantType\ClientCredentials; 84 | use kamermans\OAuth2\OAuth2Subscriber; 85 | 86 | // Authorization client - this is used to request OAuth access tokens 87 | $reauth_client = new GuzzleHttp\Client([ 88 | // URL for access_token request 89 | 'base_url' => 'http://some_host/access_token_request_url', 90 | ]); 91 | $reauth_config = [ 92 | "client_id" => "your client id", 93 | "client_secret" => "your client secret", 94 | "scope" => "your scope(s)", // optional 95 | "state" => time(), // optional 96 | ]; 97 | $grant_type = new ClientCredentials($reauth_client, $reauth_config); 98 | $oauth = new OAuth2Subscriber($grant_type); 99 | 100 | // This is the normal Guzzle client that you use in your application 101 | $client = new GuzzleHttp\Client([ 102 | 'auth' => 'oauth', 103 | ]); 104 | $client->getEmitter()->attach($oauth); 105 | $response = $client->get('http://somehost/some_secure_url'); 106 | 107 | echo "Status: ".$response->getStatusCode()."\n"; 108 | ``` 109 | 110 | Here's the same example for Guzzle 6+: 111 | 112 | ```php 113 | use kamermans\OAuth2\GrantType\ClientCredentials; 114 | use kamermans\OAuth2\OAuth2Middleware; 115 | use GuzzleHttp\HandlerStack; 116 | 117 | // Authorization client - this is used to request OAuth access tokens 118 | $reauth_client = new GuzzleHttp\Client([ 119 | // URL for access_token request 120 | 'base_uri' => 'http://some_host/access_token_request_url', 121 | ]); 122 | $reauth_config = [ 123 | "client_id" => "your client id", 124 | "client_secret" => "your client secret", 125 | "scope" => "your scope(s)", // optional 126 | "state" => time(), // optional 127 | ]; 128 | $grant_type = new ClientCredentials($reauth_client, $reauth_config); 129 | $oauth = new OAuth2Middleware($grant_type); 130 | 131 | $stack = HandlerStack::create(); 132 | $stack->push($oauth); 133 | 134 | // This is the normal Guzzle client that you use in your application 135 | $client = new GuzzleHttp\Client([ 136 | 'handler' => $stack, 137 | 'auth' => 'oauth', 138 | ]); 139 | 140 | $response = $client->get('http://somehost/some_secure_url'); 141 | 142 | echo "Status: ".$response->getStatusCode()."\n"; 143 | ``` 144 | 145 | ### Authorization Code Example 146 | There is a full example of using the `AuthorizationCode` grant type with a `RefreshToken` in the `examples/` directory. 147 | 148 | ### Grant Types 149 | The following OAuth grant types are supported directly, and you can always create your own by implementing `kamermans\OAuth2\GrantType\GrantTypeInterface`: 150 | - `AuthorizationCode` 151 | - `ClientCredentials` 152 | - `PasswordCredentials` 153 | - `RefreshToken` 154 | 155 | Each of these takes a Guzzle client as the first argument. This client is used to obtain or refresh your OAuth access token, out of band from the other requests you are making. 156 | 157 | ### Request Signers 158 | There are two cases where we need to *sign* an HTTP request: when adding client credentials to a request for a new access token, and when adding an access token to a request. 159 | 160 | #### Client Credentials Signers 161 | When requesting a new access token, we need to send the required credentials to the OAuth 2 server. Adding information to a request is called *signing* in this library. 162 | 163 | There are two client credentials signers included in `kamermans\OAuth2\Signer\ClientCredentials`: 164 | - `BasicAuth`: (default) Sends the credentials to the OAuth 2 server using HTTP Basic Auth in the `Authorization` header. 165 | - `PostFormData`: Sends the credentials to the OAuth 2 server using an HTTP Form Body (`Content-Type: application/x-www-form-urlencoded`). The Client ID is stored in the field `client_id` and the Client Secret is stored in `client_secret`. The field names can be changed by passing arguments to the constructor like this: `new PostFormData('MyClientId', 'MySecret');` (which would place the ID and secret into the fields `MyClientId` and `MySecret`). 166 | - `Json`: Sends the credentials to the OAuth 2 server using a JSON (`Content-Type: application/json`). The Client ID is stored in the field `client_id` and the Client Secret is stored in `client_secret`. The field names can be changed by passing arguments to the constructor like this: `new Json('MyClientId', 'MySecret');` (which would place the ID and secret into the fields `MyClientId` and `MySecret`). 167 | 168 | If the OAuth 2 server you are obtaining an access token from does not support the built-in methods, you can either extend one of the built-in signers, or create your own by implementing `kamermans\OAuth2\Signer\ClientCredentials\SignerInterface`, for example: 169 | 170 | ```php 171 | use kamermans\OAuth2\Signer\ClientCredentials\SignerInterface; 172 | 173 | class MyCustomAuth implements SignerInterface 174 | { 175 | public function sign($request, $clientId, $clientSecret) 176 | { 177 | if (Helper::guzzleIs('~', 6)) { 178 | $request = $request->withHeader('x-client-id', $clientId); 179 | $request = $request->withHeader('x-client-secret', $clientSecret); 180 | return $request; 181 | } 182 | 183 | $request->setHeader('x-client-id', $clientId); 184 | $request->setHeader('x-client-secret', $clientSecret); 185 | return $request; 186 | } 187 | } 188 | ``` 189 | 190 | #### Access Token Signers 191 | When making a request to a REST endpoint protected by OAuth 2, we need to *sign* the request by adding the access token to it. This library intercepts your requests, signs them with the current access token, and sends them on their way. 192 | 193 | The two most common ways to sign a request are included in `kamermans\OAuth2\Signer\AccessToken`: 194 | - `BearerAuth`: (default) Sends the access token using the HTTP `Authorization` header. 195 | - `BasicAuth`: Alias for `BearerAuth`. Don't use; exists for backwards compatibility only. 196 | - `QueryString`: Sends the access token by appending it to the query string. The default query string field name is `access_token`, and if that field is already present in the request, it will be overwritten. A different field name can be used by passing it to the constructor like this: `new QueryString('MyAccessToken')`, where `MyAccessToken` is the field name. 197 | 198 | > Note: Use of the `QueryString` signer is discouraged because your access token is exposed in the URL. Also, you should only connect to OAuth-powered services via `HTTPS` so your access token is encrypted in flight. 199 | 200 | You can create a custom access token signer by implementing `kamermans\OAuth2\Signer\AccessToken\SignerInterface`. 201 | 202 | ### Access Token Persistence 203 | > Note: OAuth Access tokens should be stored somewhere securely and/or encrypted. If an attacker gains access to your access token, they could have unrestricted access to whatever resources and scopes were allowed! 204 | 205 | By default, access tokens are not persisted anywhere. There are some built-in mechanisms for caching / persisting tokens (in `kamermans\OAuth2\Persistence`): 206 | - `NullTokenPersistence` (default) Disables persistence 207 | - `FileTokenPersitence` Takes the path to a file in which the access token will be saved. 208 | - `DoctrineCacheTokenPersistence` Takes a `Doctrine\Common\Cache\Cache` object and optionally a key name (default: `guzzle-oauth2-token`) where the access token will be saved. 209 | - `SimpleCacheTokenPersistence` Takes a PSR-16 SimpleCache and optionally a key name (default: `guzzle-oauth2-token`) where the access token will be saved. This allows any PSR-16 compatible cache to be used. 210 | - `Laravel5CacheTokenPersistence` Takes an `Illuminate\Contracts\Cache\Repository` object and optionally a key name (default: `guzzle-oauth2-token`) where the access token will be saved. 211 | - `ClosureTokenPersistence` Allows you to define a token persistence provider by providing closures to handle the persistence functions. 212 | 213 | If you want to use your own persistence layer, you should write your own class that implements `TokenPersistenceInterface` or use the `ClosureTokenPersistence` provider, which is described at the end of this section. 214 | 215 | To enable token persistence, you must use the `OAuth2Middleware::setTokenPersistence()` or `OAuth2Subscriber::setTokenPersistence()` method, like this: 216 | 217 | ```php 218 | use kamermans\OAuth2\Persistence\FileTokenPersistence; 219 | 220 | $token_path = '/tmp/access_token.json'; 221 | $token_persistence = new FileTokenPersistence($token_path); 222 | 223 | $grant_type = new ClientCredentials($reauth_client, $reauth_config); 224 | $oauth = new OAuth2Middleware($grant_type); 225 | $oauth->setTokenPersistence($token_persistence); 226 | ``` 227 | ### Closure-Based Token Persistence 228 | There are plenty of cases where you would like to use your own caching layer to store the OAuth2 data, but there is no adapter included that works with your cache provider. The `ClosureTokenPersistence` provider makes this case easier by allowing you to define closures that handle the OAuth2 persistence data, as shown in the example below. 229 | 230 | ```php 231 | // We'll store everything in an array, but you can use any provider you want 232 | $cache = []; 233 | $cache_key = "foo"; 234 | 235 | // Returns true if the item exists in cache 236 | $exists = function() use (&$cache, $cache_key) { 237 | return array_key_exists($cache_key, $cache); 238 | }; 239 | 240 | // Sets the given $value array in cache 241 | $set = function(array $value) use (&$cache, $cache_key) { 242 | $cache[$cache_key] = $value; 243 | }; 244 | 245 | // Gets the previously-stored value from cache (or null) 246 | $get = function() use (&$cache, $cache_key, $exists) { 247 | return $exists()? $cache[$cache_key]: null; 248 | }; 249 | 250 | // Deletes the previously-stored value from cache (if exists) 251 | $delete = function() use (&$cache, $cache_key, $exists) { 252 | if ($exists()) { 253 | unset($cache[$cache_key]); 254 | } 255 | }; 256 | 257 | $persistence = new ClosureTokenPersistence($set, $get, $delete, $exists); 258 | ``` 259 | 260 | > Note: The format of the token data is a PHP associative array. You can flatten the array with `serialize()` or `json_encode()` or whatever else you want before storing it, but remember to decode it back to an array in `get()` before returning it! Also, the above example is not very thread-safe, so if you have a high level of concurrency, you will need to find more atomic ways to handle this logic, or at least wrap things with `try/catch` and handle things gracefully. 261 | 262 | Please see the `src/Persistence/` directory for more information on persistence. 263 | 264 | ### Manually Setting an Access Token 265 | For a manually-obtained access token, you can use the `NullGrantType` and set the access token manually as follows: 266 | 267 | ```php 268 | use kamermans\OAuth2\GrantType\NullGrantType; 269 | 270 | $oauth = new OAuth2Middleware(new NullGrantType); 271 | $oauth->setAccessToken([ 272 | // Your access token goes here 273 | 'access_token' => 'abcdefghijklmnop', 274 | // You can specify 'expires_in` as well, but it doesn't make much sense in this scenario 275 | // You can also specify 'scope' => 'list of scopes' 276 | ]); 277 | ``` 278 | 279 | Note that if the access token is not set using `setAccessToken()`, a `kamermans\OAuth2\Exception\ReauthorizationException` will be thrown since the `NullGrantType` has no way to get a new access token. 280 | 281 | ### Using Refresh Tokens 282 | Refresh tokens are designed to allow a server to request a new access token on behalf of a user that is not present. For example, if some fictional app `Angry Rodents` wants to post something to the social media site `Grillbook` on behalf of the user, `John Doe`, the `Angry Rodents` app needs an access token for `Grillbook`. When `John Doe` first installs this app, it redirects him to the `Grillbook` site to authorize the `Angry Rodents` app to post on his behalf, and the `Angry Rodents` app receives an access token and a refresh token in the process. Eventually the access token expires, but `Angry Rodents` cannot use the original method (redirecting the user to ask for permission) every time the token expires, so instead, it sends the refresh token to `Grillbook`, which returns a new access token (and possibly a new refresh token). 283 | 284 | To use refresh tokens, you pass a `RefreshToken` grant type object as the second argument to `OAuth2Middleware` or `OAuth2Subscriber`. Normally refresh tokens are only used in the interactive `AuthorizationCode` grant type (where the user is present), but it is also possible to use them with the other grant types (this is discouraged in the OAuth 2.0 spec). For example, here we are using a refresh token with the `ClientCredentials` grant type: 285 | 286 | ```php 287 | // This grant type is used to get a new Access Token and Refresh Token when 288 | // no valid Access Token or Refresh Token is available 289 | $grant_type = new ClientCredentials($reauth_client, $reauth_config); 290 | 291 | // This grant type is used to get a new Access Token and Refresh Token when 292 | // only a valid Refresh Token is available 293 | $refresh_grant_type = new RefreshToken($reauth_client, $reauth_config); 294 | 295 | // Tell the middleware to use the two grant types 296 | $oauth = new OAuth2Middleware($grant_type, $refresh_grant_type); 297 | ``` 298 | 299 | > When using a refresh token to request a new access token, the server *may* send a new refresh token in the response. If a new refresh token was sent, it will be saved, otherwise the old refresh token will be retained. 300 | --------------------------------------------------------------------------------