1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Copyright 2016 François Kooman <[email protected]>. |
4
|
|
|
* |
5
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
6
|
|
|
* you may not use this file except in compliance with the License. |
7
|
|
|
* You may obtain a copy of the License at |
8
|
|
|
* |
9
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0 |
10
|
|
|
* |
11
|
|
|
* Unless required by applicable law or agreed to in writing, software |
12
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, |
13
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
14
|
|
|
* See the License for the specific language governing permissions and |
15
|
|
|
* limitations under the License. |
16
|
|
|
*/ |
17
|
|
|
|
18
|
|
|
namespace fkooman\OAuth\Client; |
19
|
|
|
|
20
|
|
|
use fkooman\OAuth\Client\Exception\OAuthException; |
21
|
|
|
use InvalidArgumentException; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* OAuth 2.0 Client. Helper class to make it easy to obtain an access token |
25
|
|
|
* from an OAuth 2.0 provider. |
26
|
|
|
*/ |
27
|
|
|
class OAuth2Client |
28
|
|
|
{ |
29
|
|
|
/** @var Provider */ |
30
|
|
|
private $provider; |
31
|
|
|
|
32
|
|
|
/** @var HttpClientInterface */ |
33
|
|
|
private $httpClient; |
34
|
|
|
|
35
|
|
|
/** @var RandomInterface */ |
36
|
|
|
private $random; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Instantiate an OAuth 2.0 Client. |
40
|
|
|
* |
41
|
|
|
* @param Provider $provider the OAuth 2.0 provider configuration |
42
|
|
|
* @param HttpClientInterface $httpClient the HTTP client implementation |
43
|
|
|
* @param RandomInterface $random the random implementation |
44
|
|
|
*/ |
45
|
|
|
public function __construct(Provider $provider, HttpClientInterface $httpClient, RandomInterface $random = null) |
46
|
|
|
{ |
47
|
|
|
$this->provider = $provider; |
48
|
|
|
$this->httpClient = $httpClient; |
49
|
|
|
if (is_null($random)) { |
50
|
|
|
$random = new Random(); |
51
|
|
|
} |
52
|
|
|
$this->random = $random; |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Obtain an authorization request URL to start the authorization process |
57
|
|
|
* at the OAuth provider. |
58
|
|
|
* |
59
|
|
|
* @param string $scope the space separated scope tokens |
60
|
|
|
* @param string $redirectUri the URL to redirect back to after coming back |
61
|
|
|
* from the OAuth provider (callback URL) |
62
|
|
|
* |
63
|
|
|
* @return string the authorization request URL |
64
|
|
|
* |
65
|
|
|
* @see https://tools.ietf.org/html/rfc6749#section-3.3 |
66
|
|
|
* @see https://tools.ietf.org/html/rfc6749#section-3.1.2 |
67
|
|
|
*/ |
68
|
|
|
public function getAuthorizationRequestUri($scope, $redirectUri) |
69
|
|
|
{ |
70
|
|
|
$queryParams = http_build_query( |
71
|
|
|
[ |
72
|
|
|
'client_id' => $this->provider->getId(), |
73
|
|
|
'redirect_uri' => $redirectUri, |
74
|
|
|
'scope' => $scope, |
75
|
|
|
'state' => $this->random->get(), |
76
|
|
|
'response_type' => 'code', |
77
|
|
|
], |
78
|
|
|
'&' |
79
|
|
|
); |
80
|
|
|
|
81
|
|
|
return sprintf( |
82
|
|
|
'%s%s%s', |
83
|
|
|
$this->provider->getAuthorizationEndpoint(), |
84
|
|
|
false === strpos($this->provider->getAuthorizationEndpoint(), '?') ? '?' : '&', |
85
|
|
|
$queryParams |
86
|
|
|
); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Obtain the access token from the OAuth provider after returning from the |
91
|
|
|
* OAuth provider on the redirectUri (callback URL). |
92
|
|
|
* |
93
|
|
|
* @param string $requestUri the original authorization |
94
|
|
|
* request URL as obtained by getAuthorzationRequestUri |
95
|
|
|
* @param string $responseCode the code passed to the 'code' |
96
|
|
|
* query parameter on the callback URL |
97
|
|
|
* @param string $responseState the state passed to the 'state' |
98
|
|
|
* query parameter on the callback URL |
99
|
|
|
* |
100
|
|
|
* @return AccessToken |
101
|
|
|
*/ |
102
|
|
|
public function getAccessToken($requestUri, $responseCode, $responseState) |
103
|
|
|
{ |
104
|
|
|
$requestParameters = self::parseRequestUri($requestUri); |
105
|
|
|
if ($responseState !== $requestParameters['state']) { |
106
|
|
|
// the OAuth state from the initial request MUST be the same as the |
107
|
|
|
// state used by the response |
108
|
|
|
throw new OAuthException('invalid OAuth state'); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
if ($requestParameters['client_id'] !== $this->provider->getId()) { |
112
|
|
|
// the client_id used for the initial request differs from the |
113
|
|
|
// currently configured Provider, the client_id MUST be identical |
114
|
|
|
throw new OAuthException('unexpected client identifier'); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
// prepare access_token request |
118
|
|
|
$tokenRequestData = [ |
119
|
|
|
'client_id' => $this->provider->getId(), |
120
|
|
|
'grant_type' => 'authorization_code', |
121
|
|
|
'code' => $responseCode, |
122
|
|
|
'redirect_uri' => $requestParameters['redirect_uri'], |
123
|
|
|
]; |
124
|
|
|
|
125
|
|
|
$responseData = self::validateTokenResponse( |
126
|
|
|
$this->httpClient->post( |
127
|
|
|
$this->provider, |
128
|
|
|
$tokenRequestData |
129
|
|
|
), |
130
|
|
|
$requestParameters['scope'] |
131
|
|
|
); |
132
|
|
|
|
133
|
|
|
return new AccessToken( |
134
|
|
|
$responseData['access_token'], |
135
|
|
|
$responseData['token_type'], |
136
|
|
|
$responseData['scope'], |
137
|
|
|
$responseData['expires_in'] |
138
|
|
|
); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
private static function parseRequestUri($requestUri) |
142
|
|
|
{ |
143
|
|
|
if (!is_string($requestUri)) { |
144
|
|
|
throw new InvalidArgumentException('"requestUri" MUST be string'); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
if (false === strpos($requestUri, '?')) { |
148
|
|
|
throw new OAuthException('"requestUri" not valid, no query string'); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
parse_str(explode('?', $requestUri)[1], $requestParameters); |
152
|
|
|
|
153
|
|
|
$requiredParameters = [ |
154
|
|
|
'client_id', |
155
|
|
|
'redirect_uri', |
156
|
|
|
'scope', |
157
|
|
|
'state', |
158
|
|
|
'response_type', |
159
|
|
|
]; |
160
|
|
|
|
161
|
|
|
// all of the above parameters were part of the requestUri, make sure |
162
|
|
|
// they are still there... |
163
|
|
View Code Duplication |
foreach ($requiredParameters as $requiredParameter) { |
|
|
|
|
164
|
|
|
if (!array_key_exists($requiredParameter, $requestParameters)) { |
165
|
|
|
throw new OAuthException( |
166
|
|
|
sprintf( |
167
|
|
|
'request URI not valid, missing required query parameter "%s"', |
168
|
|
|
$requiredParameter |
169
|
|
|
) |
170
|
|
|
); |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
return $requestParameters; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
private static function validateTokenResponse(array $tokenResponse, $requestScope) |
178
|
|
|
{ |
179
|
|
|
$requiredParameters = [ |
180
|
|
|
'access_token', |
181
|
|
|
'token_type', |
182
|
|
|
]; |
183
|
|
|
|
184
|
|
View Code Duplication |
foreach ($requiredParameters as $requiredParameter) { |
|
|
|
|
185
|
|
|
if (!array_key_exists($requiredParameter, $tokenResponse)) { |
186
|
|
|
throw new OAuthException( |
187
|
|
|
sprintf( |
188
|
|
|
'token response not valid, missing required parameter "%s"', |
189
|
|
|
$requiredParameter |
190
|
|
|
) |
191
|
|
|
); |
192
|
|
|
} |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
if (!array_key_exists('scope', $tokenResponse)) { |
196
|
|
|
// if the token endpoint does not return a 'scope' value, the |
197
|
|
|
// specification says the requested scope was granted |
198
|
|
|
$tokenResponse['scope'] = $requestScope; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
if (!array_key_exists('expires_in', $tokenResponse)) { |
202
|
|
|
// if the 'expires_in' field is not available, we make it null |
203
|
|
|
// here, the client will just have to try to see if the token is |
204
|
|
|
// still valid... |
205
|
|
|
$tokenResponse['expires_in'] = null; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
return $tokenResponse; |
209
|
|
|
} |
210
|
|
|
} |
211
|
|
|
|
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.