Skip to content

Commit 6f80eed

Browse files
authored
Merge pull request #16 from upwork/v2.4.0
feat(grant): add support of Client Credentials Grant
2 parents bc7fc95 + d5fa45a commit 6f80eed

File tree

10 files changed

+160
-116
lines changed

10 files changed

+160
-116
lines changed

.docgen

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
apigen generate -s src/ -d docs_html
1+
apigen src --output docs

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
fail-fast: false
1717
matrix:
1818
os: [ubuntu-latest]
19-
php: [ '7.3', '7.4' ]
19+
php: [ '7.3', '7.4', '8.1' ]
2020

2121
name: PHP ${{ matrix.python }}
2222
steps:

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Release History
22

3+
## 2.4.0
4+
* Add support of Client Credentials Grant
5+
36
## 2.3.1
47
* Bug fixes
58

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "upwork/php-upwork-oauth2",
33
"description": "PHP bindings for Upwork API (OAuth2)",
4-
"version": "v2.3.1",
4+
"version": "v2.4.0",
55
"type": "library",
66
"keywords": ["upwork", "php", "api", "oauth2"],
77
"homepage": "http://www.upwork.com",
@@ -25,7 +25,7 @@
2525
"require": {
2626
"php": ">=5.6.0",
2727
"ext-json": "*",
28-
"league/oauth2-client": "^2.3"
28+
"league/oauth2-client": "^2.7"
2929
},
3030
"require-dev": {
3131
"phpunit/phpunit": "^9",

example/example.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222
array(
2323
'clientId' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', // SETUP YOUR CONSUMER KEY
2424
'clientSecret' => 'xxxxxxxxxxxx', // SETUP KEY SECRET
25-
'redirectUri' => 'https://a.callback.url/',
25+
//'grantType' => 'client_credentials', // used in Client Credentials Grant
26+
'redirectUri' => 'https://a.callback.url/', // used in Code Authorization Grant
2627
'accessToken' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', // WARNING: keep this up-to-date!
27-
'refreshToken' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', // WARNING: keep this up-to-date!
28+
'refreshToken' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', // WARNING: keep this up-to-date! // NOT needed for Client Credentials Grant
2829
'expiresIn' => 'xxxxxxxxxx', // WARNING: keep this up-to-date!
2930
//'debug' => true, // enables debug mode
3031
//'authType' => 'MyOAuthPHPLib' // your own authentication type, see AuthTypes directory

src/Upwork/API/AuthTypes/AbstractOAuth.php

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ abstract class AbstractOAuth
3636
* @var Client Secret (a.k.a. Key Secret)
3737
*/
3838
static protected $_clientSecret = null;
39+
/**
40+
* @var Grant Type
41+
*/
42+
static protected $_grantType = null;
3943
/**
4044
* @var Redirect URI
4145
*/
@@ -136,65 +140,86 @@ public function auth()
136140
{
137141
ApiDebug::p('running auth process in ' . __CLASS__);
138142

139-
if (self::$_accessToken === null && self::$_refreshToken === null && self::$_authzCode === null) {
140-
$authUrl = $this->getInstance()->getAuthorizationUrl();
141-
ApiDebug::p('Got authorization URL from OAuth2 instance', $authUrl);
142-
143-
// save the state - useful for web-based apps to prevent CSRF attacks
144-
self::$_state = $this->getInstance()->getState();
145-
ApiDebug::p('Saving state', self::$_state);
146-
147-
if (self::$_mode === 'web') {
148-
// authorize web application via browser
149-
header('Location: ' . $authUrl);
150-
} elseif (self::$_mode === 'nonweb') {
151-
// authorize nonweb application
152-
ApiDebug::p('found [nonweb] mode, need to authorize application manually');
153-
154-
$prompt = 'Visit ' . $authUrl . "\n " .
155-
'and provide received code (verifier) for further authorization' . "\n" .
156-
'$ ';
157-
if (PHP_OS == 'WINNT') {
158-
echo $prompt;
159-
$authzCode = stream_get_line(STDIN, 1024, PHP_EOL);
160-
} else {
161-
$authzCode = readline($prompt);
143+
if (self::$_accessToken === null && self::$_refreshToken === null && self::$_authzCode === null) {
144+
if (self::$_grantType == 'authorization_code') { // Code authorization grant type
145+
$authUrl = $this->getInstance()->getAuthorizationUrl();
146+
ApiDebug::p('Got authorization URL from OAuth2 instance', $authUrl);
147+
148+
// save the state - useful for web-based apps to prevent CSRF attacks
149+
self::$_state = $this->getInstance()->getState();
150+
ApiDebug::p('Saving state', self::$_state);
151+
152+
if (self::$_mode === 'web') {
153+
// authorize web application via browser
154+
header('Location: ' . $authUrl);
155+
} elseif (self::$_mode === 'nonweb') {
156+
// authorize nonweb application
157+
ApiDebug::p('found [nonweb] mode, need to authorize application manually');
158+
159+
$prompt = 'Visit ' . $authUrl . "\n " .
160+
'and provide received code (verifier) for further authorization' . "\n" .
161+
'$ ';
162+
if (PHP_OS == 'WINNT') {
163+
echo $prompt;
164+
$authzCode = stream_get_line(STDIN, 1024, PHP_EOL);
165+
} else {
166+
$authzCode = readline($prompt);
167+
}
168+
169+
// get access token
170+
$this->_setupTokens($authzCode, self::$_grantType);
171+
}
172+
} else if (self::$_grantType == 'client_credentials') {
173+
if (self::$_mode === 'web') {
174+
// authorize web application via browser
175+
header('Location: ' . $authUrl);
176+
} elseif (self::$_mode === 'nonweb') {
177+
ApiDebug::p('found [nonweb] mode for client credentials grant');
178+
// Client credentials grant type
179+
$this->_setupTokens(null, self::$_grantType);
162180
}
163-
164-
// get access token
165-
$this->_setupTokens($authzCode);
166181
}
167-
} elseif (self::$_accessToken === null && self::$_authzCode !== null) {
182+
} elseif (self::$_accessToken === null && self::$_authzCode !== null) {
168183
// get access token, web-based callback
169-
$this->_setupTokens(self::$_authzCode);
184+
$this->_setupTokens(self::$_authzCode, 'authorization_code');
170185
} else {
171186
// access_token isset
172-
// check if expired and refresh if needed
173-
if (self::$_expiresIn <= time()) {
174-
if (self::$_refreshToken === null) {
175-
throw new ApiException('Access token has expired but refresh token is not specified. Can not refresh.');
176-
}
177-
$this->_refreshTokens(self::$_refreshToken);
178-
}
187+
// check if expired and refresh if needed
188+
if (self::$_expiresIn <= time()) {
189+
if (self::$_grantType === 'authorization_code') {
190+
if (self::$_refreshToken === null) {
191+
throw new ApiException('Access token has expired but refresh token is not specified. Can not refresh.');
192+
}
193+
$this->_refreshTokens(self::$_refreshToken);
194+
} else if (self::$_grantType === 'client_credentials') {
195+
$this->_setupTokens(null, self::$_grantType);
196+
}
197+
}
179198
}
180199

181-
ApiDebug::p('Tokens info', array(self::$_accessToken, self::$_refreshToken, self::$_expiresIn));
182-
183-
return array(
184-
'access_token' => self::$_accessToken,
185-
'refresh_token' => self::$_refreshToken,
186-
'expires_in' => self::$_expiresIn
187-
);
200+
ApiDebug::p('Tokens info', array(self::$_accessToken, self::$_refreshToken, self::$_expiresIn));
201+
202+
return (self::$_grantType === 'authorization_code')
203+
? array(
204+
'access_token' => self::$_accessToken,
205+
'refresh_token' => self::$_refreshToken,
206+
'expires_in' => self::$_expiresIn
207+
)
208+
: array(
209+
'access_token' => self::$_accessToken,
210+
'expires_in' => self::$_expiresIn
211+
);
188212
}
189213

190214
/**
191215
* Get access token
192216
*
193217
* @param string $authzCode Authorization Code, got after authorization
218+
* @param string $grantType Grant Type
194219
* @access private
195220
* @return array
196221
*/
197-
abstract protected function _setupTokens($authzCode);
222+
abstract protected function _setupTokens($authzCode, $grantType);
198223

199224
/**
200225
* Get OAuth instance

src/Upwork/API/AuthTypes/OAuth2ClientLib.php

Lines changed: 58 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ final class OAuth2ClientLib extends AbstractOAuth implements ApiClient
4141
*/
4242
public function getInstance()
4343
{
44-
return (self::$_provider instanceof \League\OAuth2\Client\Provider\GenericProvider)
45-
? self::$_provider
46-
: $this->_getOAuthInstance();
44+
return (self::$_provider instanceof \League\OAuth2\Client\Provider\GenericProvider)
45+
? self::$_provider
46+
: $this->_getOAuthInstance();
4747
}
4848

4949
/**
@@ -59,46 +59,46 @@ public function getInstance()
5959
*/
6060
public function request($type, $url, $params = array(), string $tenantId = null)
6161
{
62-
ApiDebug::p('running request from ' . __CLASS__);
63-
64-
switch ($type) {
65-
case \League\OAuth2\Client\Provider\AbstractProvider::METHOD_POST:
66-
if (self::$_epoint == UPWORK_GRAPHQL_EP_NAME) {
67-
$options = array('headers' => array('content-type' => 'application/json'));
68-
is_null($tenantId) || $options['headers']['X-Upwork-API-TenantId'] = $tenantId;
69-
$options['body'] = $params;
70-
} else {
71-
$options = array('headers' => array('content-type' => 'application/x-www-form-urlencoded'));
72-
$options['body'] = http_build_query($params, null, '&', \PHP_QUERY_RFC3986);
73-
}
74-
75-
$url = ApiUtils::getFullUrl($url, self::$_epoint);
76-
break;
77-
case \League\OAuth2\Client\Provider\AbstractProvider::METHOD_GET:
78-
$options = array();
79-
$url = ApiUtils::getFullUrl($url, self::$_epoint, (($type == 'GET' ? $params : null)));
80-
break;
81-
default:
82-
throw new ApiException('Unsupported HTTP method.');
83-
}
84-
85-
$options['headers']['user-agent'] = ApiConfig::UPWORK_LIBRARY_USER_AGENT;
86-
$request = $this->getInstance()->getAuthenticatedRequest($type, $url, self::$_accessToken, $options);
87-
88-
ApiDebug::p('prepared request', $request);
89-
90-
try {
91-
// do not use getParsedResponse, it returns an array
92-
// but we need a raw json that will be decoded and returned as StdClass object
93-
$response = $this->getInstance()->getResponse($request);
62+
ApiDebug::p('running request from ' . __CLASS__);
63+
64+
switch ($type) {
65+
case \League\OAuth2\Client\Provider\AbstractProvider::METHOD_POST:
66+
if (self::$_epoint == UPWORK_GRAPHQL_EP_NAME) {
67+
$options = array('headers' => array('content-type' => 'application/json'));
68+
is_null($tenantId) || $options['headers']['X-Upwork-API-TenantId'] = $tenantId;
69+
$options['body'] = $params;
70+
} else {
71+
$options = array('headers' => array('content-type' => 'application/x-www-form-urlencoded'));
72+
$options['body'] = http_build_query($params, null, '&', \PHP_QUERY_RFC3986);
73+
}
74+
75+
$url = ApiUtils::getFullUrl($url, self::$_epoint);
76+
break;
77+
case \League\OAuth2\Client\Provider\AbstractProvider::METHOD_GET:
78+
$options = array();
79+
$url = ApiUtils::getFullUrl($url, self::$_epoint, (($type == 'GET' ? $params : null)));
80+
break;
81+
default:
82+
throw new ApiException('Unsupported HTTP method.');
83+
}
84+
85+
$options['headers']['user-agent'] = ApiConfig::UPWORK_LIBRARY_USER_AGENT;
86+
$request = $this->getInstance()->getAuthenticatedRequest($type, $url, self::$_accessToken, $options);
87+
88+
ApiDebug::p('prepared request', $request);
89+
90+
try {
91+
// do not use getParsedResponse, it returns an array
92+
// but we need a raw json that will be decoded and returned as StdClass object
93+
$response = $this->getInstance()->getResponse($request);
9494
} catch (\GuzzleHttp\Exception\ClientException $e) {
9595
$eResponse = $e->getResponse();
9696
$response = $eResponse->getBody()->getContents();
97-
} catch (\Exception $e) {
98-
$response = $e->getResponseBody();
99-
}
100-
101-
ApiDebug::p('got response from server', $response);
97+
} catch (\Exception $e) {
98+
$response = $e->getResponseBody();
99+
}
100+
101+
ApiDebug::p('got response from server', $response);
102102

103103
return (string) $response->getBody();
104104
}
@@ -107,14 +107,15 @@ public function request($type, $url, $params = array(), string $tenantId = null)
107107
* Get access token
108108
*
109109
* @param string $authzCode Authorization code (a received verifier)
110+
* @param string $grantType Grant Type
110111
* @access protected
111112
* @return array
112113
*/
113-
protected function _setupTokens($authzCode)
114+
protected function _setupTokens($authzCode, $grantType)
114115
{
115116
ApiDebug::p('requesting access token');
116117

117-
return $this->_requestTokens('authorization_code', array('code' => $authzCode));
118+
return $this->_requestTokens($grantType, array('code' => $authzCode));
118119
}
119120

120121
/**
@@ -126,9 +127,9 @@ protected function _setupTokens($authzCode)
126127
*/
127128
protected function _refreshTokens($refreshToken)
128129
{
129-
ApiDebug::p('refreshing the existing access token');
130+
ApiDebug::p('refreshing the existing access token');
130131

131-
return $this->_requestTokens('refresh_token', array('refresh_token' => $refreshToken));
132+
return $this->_requestTokens('refresh_token', array('refresh_token' => $refreshToken));
132133
}
133134

134135
/**
@@ -147,11 +148,11 @@ protected function _getOAuthInstance($authType = null)
147148
'clientSecret' => self::$_clientSecret,
148149
'redirectUri' => self::$_redirectUri,
149150
'urlAuthorize' => ApiUtils::getFullUrl(self::URL_AUTH, ''),
150-
'urlAccessToken' => ApiUtils::getFullUrl(self::URL_ATOKEN, 'api'),
151-
'urlResourceOwnerDetails' => ''
151+
'urlAccessToken' => ApiUtils::getFullUrl(self::URL_ATOKEN, 'api'),
152+
'urlResourceOwnerDetails' => ''
152153
);
153154

154-
self::$_provider = new \League\OAuth2\Client\Provider\GenericProvider($options);
155+
self::$_provider = new \League\OAuth2\Client\Provider\GenericProvider($options);
155156

156157
return self::$_provider;
157158
}
@@ -170,21 +171,24 @@ private function _requestTokens(string $type, array $options)
170171

171172
$accessTokenInfo = array();
172173

173-
$accessToken = $this->getInstance()->getHttpClient()->getConfig()['handler']->push(
174+
$accessToken = $this->getInstance()->getHttpClient()->getConfig()['handler']->push(
174175
Middleware::mapRequest(function (RequestInterface $request) {
175176
return $request->withHeader('User-Agent', ApiConfig::UPWORK_LIBRARY_USER_AGENT);
176177
}));
177178
$accessToken = $this->getInstance()->getAccessToken($type, $options);
178179

179180
$accessTokenInfo['access_token'] = $accessToken->getToken();
180-
$accessTokenInfo['refresh_token'] = $accessToken->getRefreshToken();
181-
$accessTokenInfo['expires_in'] = $accessToken->getExpires();
182-
183-
ApiDebug::p('got access token info', $accessTokenInfo);
181+
$accessTokenInfo['expires_in'] = $accessToken->getExpires();
184182

185183
self::$_accessToken = $accessTokenInfo['access_token'];
186-
self::$_refreshToken = $accessTokenInfo['refresh_token'];
187-
self::$_expiresIn = $accessTokenInfo['expires_in'];
184+
self::$_expiresIn = $accessTokenInfo['expires_in'];
185+
186+
if ($type === 'authorization_code') {
187+
$accessTokenInfo['refresh_token'] = $accessToken->getRefreshToken();
188+
self::$_refreshToken = $accessTokenInfo['refresh_token'];
189+
}
190+
191+
ApiDebug::p('got access token info', $accessTokenInfo);
188192

189193
return $accessTokenInfo;
190194
}

0 commit comments

Comments
 (0)