|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* Copyright (C) 2017 François Kooman <[email protected]>. |
|
4
|
|
|
* |
|
5
|
|
|
* This program is free software: you can redistribute it and/or modify |
|
6
|
|
|
* it under the terms of the GNU Affero General Public License as |
|
7
|
|
|
* published by the Free Software Foundation, either version 3 of the |
|
8
|
|
|
* License, or (at your option) any later version. |
|
9
|
|
|
* |
|
10
|
|
|
* This program is distributed in the hope that it will be useful, |
|
11
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13
|
|
|
* GNU Affero General Public License for more details. |
|
14
|
|
|
* |
|
15
|
|
|
* You should have received a copy of the GNU Affero General Public License |
|
16
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
17
|
|
|
*/ |
|
18
|
|
|
|
|
19
|
|
|
namespace fkooman\OAuth\Client; |
|
20
|
|
|
|
|
21
|
|
|
use DateInterval; |
|
22
|
|
|
use DateTime; |
|
23
|
|
|
use fkooman\OAuth\Client\Exception\OAuthException; |
|
24
|
|
|
use fkooman\OAuth\Client\Exception\OAuthServerException; |
|
25
|
|
|
use InvalidArgumentException; |
|
26
|
|
|
|
|
27
|
|
|
/** |
|
28
|
|
|
* OAuth 2.0 Client. Helper class to make it easy to obtain an access token |
|
29
|
|
|
* from an OAuth 2.0 provider. |
|
30
|
|
|
*/ |
|
31
|
|
|
class OAuth2Client |
|
32
|
|
|
{ |
|
33
|
|
|
/** @var Provider */ |
|
34
|
|
|
private $provider; |
|
35
|
|
|
|
|
36
|
|
|
/** @var HttpClientInterface */ |
|
37
|
|
|
private $httpClient; |
|
38
|
|
|
|
|
39
|
|
|
/** @var RandomInterface */ |
|
40
|
|
|
private $random; |
|
41
|
|
|
|
|
42
|
|
|
/** @var \DateTime */ |
|
43
|
|
|
private $dateTime; |
|
44
|
|
|
|
|
45
|
|
|
/** |
|
46
|
|
|
* Instantiate an OAuth 2.0 Client. |
|
47
|
|
|
* |
|
48
|
|
|
* @param Provider $provider the OAuth 2.0 provider configuration |
|
49
|
|
|
* @param HttpClientInterface $httpClient the HTTP client implementation |
|
50
|
|
|
* @param RandomInterface $random the random implementation |
|
51
|
|
|
*/ |
|
52
|
|
|
public function __construct(Provider $provider, HttpClientInterface $httpClient, RandomInterface $random = null, DateTime $dateTime = null) |
|
53
|
|
|
{ |
|
54
|
|
|
$this->provider = $provider; |
|
55
|
|
|
$this->httpClient = $httpClient; |
|
56
|
|
|
if (is_null($random)) { |
|
57
|
|
|
$random = new Random(); |
|
58
|
|
|
} |
|
59
|
|
|
$this->random = $random; |
|
60
|
|
|
if (is_null($dateTime)) { |
|
61
|
|
|
$dateTime = new DateTime(); |
|
62
|
|
|
} |
|
63
|
|
|
$this->dateTime = $dateTime; |
|
64
|
|
|
} |
|
65
|
|
|
|
|
66
|
|
|
/** |
|
67
|
|
|
* Obtain an authorization request URL to start the authorization process |
|
68
|
|
|
* at the OAuth provider. |
|
69
|
|
|
* |
|
70
|
|
|
* @param string $scope the space separated scope tokens |
|
71
|
|
|
* @param string $redirectUri the URL to redirect back to after coming back |
|
72
|
|
|
* from the OAuth provider (callback URL) |
|
73
|
|
|
* |
|
74
|
|
|
* @return string the authorization request URL |
|
75
|
|
|
* |
|
76
|
|
|
* @see https://tools.ietf.org/html/rfc6749#section-3.3 |
|
77
|
|
|
* @see https://tools.ietf.org/html/rfc6749#section-3.1.2 |
|
78
|
|
|
*/ |
|
79
|
|
|
public function getAuthorizationRequestUri($scope, $redirectUri) |
|
80
|
|
|
{ |
|
81
|
|
|
$queryParams = http_build_query( |
|
82
|
|
|
[ |
|
83
|
|
|
'client_id' => $this->provider->getId(), |
|
84
|
|
|
'redirect_uri' => $redirectUri, |
|
85
|
|
|
'scope' => $scope, |
|
86
|
|
|
'state' => $this->random->get(16), |
|
87
|
|
|
'response_type' => 'code', |
|
88
|
|
|
], |
|
89
|
|
|
'&' |
|
90
|
|
|
); |
|
91
|
|
|
|
|
92
|
|
|
return sprintf( |
|
93
|
|
|
'%s%s%s', |
|
94
|
|
|
$this->provider->getAuthorizationEndpoint(), |
|
95
|
|
|
false === strpos($this->provider->getAuthorizationEndpoint(), '?') ? '?' : '&', |
|
96
|
|
|
$queryParams |
|
97
|
|
|
); |
|
98
|
|
|
} |
|
99
|
|
|
|
|
100
|
|
|
/** |
|
101
|
|
|
* Obtain the access token from the OAuth provider after returning from the |
|
102
|
|
|
* OAuth provider on the redirectUri (callback URL). |
|
103
|
|
|
* |
|
104
|
|
|
* @param string $requestUri the original authorization |
|
105
|
|
|
* request URL as obtained by getAuthorzationRequestUri |
|
106
|
|
|
* @param string $responseCode the code passed to the 'code' |
|
107
|
|
|
* query parameter on the callback URL |
|
108
|
|
|
* @param string $responseState the state passed to the 'state' |
|
109
|
|
|
* query parameter on the callback URL |
|
110
|
|
|
* |
|
111
|
|
|
* @return AccessToken |
|
112
|
|
|
*/ |
|
113
|
|
|
public function getAccessToken($requestUri, $responseCode, $responseState) |
|
114
|
|
|
{ |
|
115
|
|
|
$requestParameters = self::parseRequestUri($requestUri); |
|
116
|
|
|
if ($responseState !== $requestParameters['state']) { |
|
117
|
|
|
// the OAuth state from the initial request MUST be the same as the |
|
118
|
|
|
// state used by the response |
|
119
|
|
|
throw new OAuthException('invalid OAuth state'); |
|
120
|
|
|
} |
|
121
|
|
|
|
|
122
|
|
|
if ($requestParameters['client_id'] !== $this->provider->getId()) { |
|
123
|
|
|
// the client_id used for the initial request differs from the |
|
124
|
|
|
// currently configured Provider, the client_id MUST be identical |
|
125
|
|
|
throw new OAuthException('unexpected client identifier'); |
|
126
|
|
|
} |
|
127
|
|
|
|
|
128
|
|
|
// prepare access_token request |
|
129
|
|
|
$tokenRequestData = [ |
|
130
|
|
|
'client_id' => $this->provider->getId(), |
|
131
|
|
|
'grant_type' => 'authorization_code', |
|
132
|
|
|
'code' => $responseCode, |
|
133
|
|
|
'redirect_uri' => $requestParameters['redirect_uri'], |
|
134
|
|
|
]; |
|
135
|
|
|
|
|
136
|
|
|
$responseData = $this->validateTokenResponse( |
|
137
|
|
|
$this->httpClient->post( |
|
138
|
|
|
$this->provider, |
|
139
|
|
|
$tokenRequestData |
|
140
|
|
|
), |
|
141
|
|
|
$requestParameters['scope'] |
|
142
|
|
|
); |
|
143
|
|
|
|
|
144
|
|
|
return new AccessToken( |
|
145
|
|
|
$responseData['access_token'], |
|
146
|
|
|
$responseData['token_type'], |
|
147
|
|
|
$responseData['scope'], |
|
148
|
|
|
$responseData['expires_at'] |
|
149
|
|
|
); |
|
150
|
|
|
} |
|
151
|
|
|
|
|
152
|
|
|
private static function parseRequestUri($requestUri) |
|
153
|
|
|
{ |
|
154
|
|
|
if (!is_string($requestUri)) { |
|
155
|
|
|
throw new InvalidArgumentException('"requestUri" MUST be string'); |
|
156
|
|
|
} |
|
157
|
|
|
|
|
158
|
|
|
if (false === strpos($requestUri, '?')) { |
|
159
|
|
|
throw new OAuthException('"requestUri" not valid, no query string'); |
|
160
|
|
|
} |
|
161
|
|
|
|
|
162
|
|
|
parse_str(explode('?', $requestUri)[1], $requestParameters); |
|
163
|
|
|
|
|
164
|
|
|
$requiredParameters = [ |
|
165
|
|
|
'client_id', |
|
166
|
|
|
'redirect_uri', |
|
167
|
|
|
'scope', |
|
168
|
|
|
'state', |
|
169
|
|
|
'response_type', |
|
170
|
|
|
]; |
|
171
|
|
|
|
|
172
|
|
|
// all of the above parameters were part of the requestUri, make sure |
|
173
|
|
|
// they are still there... |
|
174
|
|
View Code Duplication |
foreach ($requiredParameters as $requiredParameter) { |
|
|
|
|
|
|
175
|
|
|
if (!array_key_exists($requiredParameter, $requestParameters)) { |
|
176
|
|
|
throw new OAuthException( |
|
177
|
|
|
sprintf( |
|
178
|
|
|
'request URI not valid, missing required query parameter "%s"', |
|
179
|
|
|
$requiredParameter |
|
180
|
|
|
) |
|
181
|
|
|
); |
|
182
|
|
|
} |
|
183
|
|
|
} |
|
184
|
|
|
|
|
185
|
|
|
return $requestParameters; |
|
186
|
|
|
} |
|
187
|
|
|
|
|
188
|
|
|
private function validateTokenResponse(array $tokenResponse, $requestScope) |
|
189
|
|
|
{ |
|
190
|
|
|
// check if an error occurred |
|
191
|
|
|
if (array_key_exists('error', $tokenResponse)) { |
|
192
|
|
|
if (array_key_exists('error_description', $tokenResponse)) { |
|
193
|
|
|
throw new OAuthServerException(sprintf('%s: %s', $tokenResponse['error'], $tokenResponse['error_description'])); |
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
throw new OAuthServerException($tokenResponse['error']); |
|
197
|
|
|
} |
|
198
|
|
|
|
|
199
|
|
|
$requiredParameters = [ |
|
200
|
|
|
'access_token', |
|
201
|
|
|
'token_type', |
|
202
|
|
|
]; |
|
203
|
|
|
|
|
204
|
|
View Code Duplication |
foreach ($requiredParameters as $requiredParameter) { |
|
|
|
|
|
|
205
|
|
|
if (!array_key_exists($requiredParameter, $tokenResponse)) { |
|
206
|
|
|
throw new OAuthException( |
|
207
|
|
|
sprintf( |
|
208
|
|
|
'token response not valid, missing required parameter "%s"', |
|
209
|
|
|
$requiredParameter |
|
210
|
|
|
) |
|
211
|
|
|
); |
|
212
|
|
|
} |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
if (!array_key_exists('scope', $tokenResponse)) { |
|
216
|
|
|
// if the token endpoint does not return a 'scope' value, the |
|
217
|
|
|
// specification says the requested scope was granted |
|
218
|
|
|
$tokenResponse['scope'] = $requestScope; |
|
219
|
|
|
} |
|
220
|
|
|
|
|
221
|
|
|
$tokenResponse['expires_at'] = $this->calculateExpiresAt($tokenResponse); |
|
222
|
|
|
|
|
223
|
|
|
return $tokenResponse; |
|
224
|
|
|
} |
|
225
|
|
|
|
|
226
|
|
|
private function calculateExpiresAt(array $tokenResponse) |
|
227
|
|
|
{ |
|
228
|
|
|
$dateTime = clone $this->dateTime; |
|
229
|
|
|
if (array_key_exists('expires_in', $tokenResponse)) { |
|
230
|
|
|
return date_add($dateTime, new DateInterval(sprintf('PT%dS', $tokenResponse['expires_in']))); |
|
231
|
|
|
} |
|
232
|
|
|
|
|
233
|
|
|
// if the 'expires_in' field is not available, we default to 1 year |
|
234
|
|
|
return date_add($dateTime, new DateInterval('P1Y')); |
|
235
|
|
|
} |
|
236
|
|
|
} |
|
237
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.