PluginProbe
Loginizer / 1.9.9
Loginizer v1.9.9
2.1.0 2.0.9 2.0.8 1.9.8 1.9.9 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 trunk 1.0 1.0.1 1.0.2 1.1.0 1.1.1 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 All 74 releases
loginizer / lib / hybridauth / Adapter / OAuth2.php

OAuth2.php in Loginizer 1.9.9, at lib/hybridauth/Adapter/OAuth2.php

745 lines 22.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*!
3 * Hybridauth
4 * https://hybridauth.github.io | https://github.com/hybridauth/hybridauth
5 * (c) 2017 Hybridauth authors | https://hybridauth.github.io/license.html
6 */
7
8 namespace Hybridauth\Adapter;
9
10 use Hybridauth\Exception\Exception;
11 use Hybridauth\Exception\InvalidApplicationCredentialsException;
12 use Hybridauth\Exception\InvalidAuthorizationStateException;
13 use Hybridauth\Exception\InvalidAuthorizationCodeException;
14 use Hybridauth\Exception\AuthorizationDeniedException;
15 use Hybridauth\Exception\InvalidAccessTokenException;
16 use Hybridauth\Data;
17 use Hybridauth\HttpClient;
18
19 /**
20 * This class can be used to simplify the authorization flow of OAuth 2 based service providers.
21 *
22 * Subclasses (i.e., providers adapters) can either use the already provided methods or override
23 * them when necessary.
24 */
25 abstract class OAuth2 extends AbstractAdapter implements AdapterInterface
26 {
27 /**
28 * Client Identifier
29 *
30 * RFC6749: client_id REQUIRED. The client identifier issued to the client during
31 * the registration process described by Section 2.2.
32 *
33 * http://tools.ietf.org/html/rfc6749#section-2.2
34 *
35 * @var string
36 */
37 protected $clientId = '';
38
39 /**
40 * Client Secret
41 *
42 * RFC6749: client_secret REQUIRED. The client secret. The client MAY omit the
43 * parameter if the client secret is an empty string.
44 *
45 * http://tools.ietf.org/html/rfc6749#section-2.2
46 *
47 * @var string
48 */
49 protected $clientSecret = '';
50
51 /**
52 * Access Token Scope
53 *
54 * RFC6749: The authorization and token endpoints allow the client to specify the
55 * scope of the access request using the "scope" request parameter.
56 *
57 * http://tools.ietf.org/html/rfc6749#section-3.3
58 *
59 * @var string
60 */
61 protected $scope = '';
62
63 /**
64 * Base URL to provider API
65 *
66 * This var will be used to build urls when sending signed requests
67 *
68 * @var string
69 */
70 protected $apiBaseUrl = '';
71
72 /**
73 * Authorization Endpoint
74 *
75 * RFC6749: The authorization endpoint is used to interact with the resource
76 * owner and obtain an authorization grant.
77 *
78 * http://tools.ietf.org/html/rfc6749#section-3.1
79 *
80 * @var string
81 */
82 protected $authorizeUrl = '';
83
84 /**
85 * Access Token Endpoint
86 *
87 * RFC6749: The token endpoint is used by the client to obtain an access token by
88 * presenting its authorization grant or refresh token.
89 *
90 * http://tools.ietf.org/html/rfc6749#section-3.2
91 *
92 * @var string
93 */
94 protected $accessTokenUrl = '';
95
96 /**
97 * TokenInfo endpoint
98 *
99 * Access token validation. OPTIONAL.
100 *
101 * @var string
102 */
103 protected $accessTokenInfoUrl = '';
104
105 /**
106 * IPD API Documentation
107 *
108 * OPTIONAL.
109 *
110 * @var string
111 */
112 protected $apiDocumentation = '';
113
114 /**
115 * Redirection Endpoint or Callback
116 *
117 * RFC6749: After completing its interaction with the resource owner, the
118 * authorization server directs the resource owner's user-agent back to
119 * the client.
120 *
121 * http://tools.ietf.org/html/rfc6749#section-3.1.2
122 *
123 * @var string
124 */
125 protected $callback = '';
126
127 /**
128 * Authorization Url Parameters
129 *
130 * @var array
131 */
132 protected $AuthorizeUrlParameters = [];
133
134
135 /**
136 * Authorization Url Parameter encoding type
137 * @see https://www.php.net/manual/de/function.http-build-query.php
138 *
139 * @var string
140 */
141 protected $AuthorizeUrlParametersEncType = PHP_QUERY_RFC1738;
142
143 /**
144 * Authorization Request State
145 *
146 * @var bool
147 */
148 protected $supportRequestState = true;
149
150 /**
151 * Access Token name
152 *
153 * While most providers will use 'access_token' as name for the Access Token attribute, other do not.
154 * On the latter case, this should be set by sub classes.
155 *
156 * @var string
157 */
158 protected $accessTokenName = 'access_token';
159
160 /**
161 * Authorization Request HTTP method.
162 *
163 * @see exchangeCodeForAccessToken()
164 *
165 * @var string
166 */
167 protected $tokenExchangeMethod = 'POST';
168
169 /**
170 * Authorization Request URL parameters.
171 *
172 * Sub classes may change add any additional parameter when necessary.
173 *
174 * @see exchangeCodeForAccessToken()
175 *
176 * @var array
177 */
178 protected $tokenExchangeParameters = [];
179
180 /**
181 * Authorization Request HTTP headers.
182 *
183 * Sub classes may add any additional header when necessary.
184 *
185 * @see exchangeCodeForAccessToken()
186 *
187 * @var array
188 */
189 protected $tokenExchangeHeaders = [];
190
191 /**
192 * Refresh Token Request HTTP method.
193 *
194 * @see refreshAccessToken()
195 *
196 * @var string
197 */
198 protected $tokenRefreshMethod = 'POST';
199
200 /**
201 * Refresh Token Request URL parameters.
202 *
203 * Sub classes may change add any additional parameter when necessary.
204 *
205 * @see refreshAccessToken()
206 *
207 * @var array|null
208 */
209 protected $tokenRefreshParameters = null;
210
211 /**
212 * Refresh Token Request HTTP headers.
213 *
214 * Sub classes may add any additional header when necessary.
215 *
216 * @see refreshAccessToken()
217 *
218 * @var array
219 */
220 protected $tokenRefreshHeaders = [];
221
222 /**
223 * Authorization Request URL parameters.
224 *
225 * Sub classes may change add any additional parameter when necessary.
226 *
227 * @see apiRequest()
228 *
229 * @var array
230 */
231 protected $apiRequestParameters = [];
232
233 /**
234 * Authorization Request HTTP headers.
235 *
236 * Sub classes may add any additional header when necessary.
237 *
238 * @see apiRequest()
239 *
240 * @var array
241 */
242 protected $apiRequestHeaders = [];
243
244 /**
245 * {@inheritdoc}
246 */
247 protected function configure()
248 {
249 $this->clientId = $this->config->filter('keys')->get('id') ?: $this->config->filter('keys')->get('key');
250 $this->clientSecret = $this->config->filter('keys')->get('secret');
251
252 if (!$this->clientId || !$this->clientSecret) {
253 throw new InvalidApplicationCredentialsException(
254 'Your application id is required in order to connect to ' . $this->providerId
255 );
256 }
257
258 $this->scope = $this->config->exists('scope') ? $this->config->get('scope') : $this->scope;
259
260 if ($this->config->exists('tokens')) {
261 $this->setAccessToken($this->config->get('tokens'));
262 }
263
264 if ($this->config->exists('supportRequestState')) {
265 $this->supportRequestState = $this->config->get('supportRequestState');
266 }
267
268 $this->setCallback($this->config->get('callback'));
269 $this->setApiEndpoints($this->config->get('endpoints'));
270 }
271
272 /**
273 * {@inheritdoc}
274 */
275 protected function initialize()
276 {
277 $this->AuthorizeUrlParameters = [
278 'response_type' => 'code',
279 'client_id' => $this->clientId,
280 'redirect_uri' => $this->callback,
281 'scope' => $this->scope,
282 ];
283
284 $this->tokenExchangeParameters = [
285 'client_id' => $this->clientId,
286 'client_secret' => $this->clientSecret,
287 'grant_type' => 'authorization_code',
288 'redirect_uri' => $this->callback
289 ];
290
291 $refreshToken = $this->getStoredData('refresh_token');
292 if (!empty($refreshToken)) {
293 $this->tokenRefreshParameters = [
294 'grant_type' => 'refresh_token',
295 'refresh_token' => $refreshToken,
296 ];
297 }
298
299 $this->apiRequestHeaders = [
300 'Authorization' => 'Bearer ' . $this->getStoredData('access_token')
301 ];
302 }
303
304 /**
305 * {@inheritdoc}
306 */
307 public function authenticate()
308 {
309 $this->logger->info(sprintf('%s::authenticate()', get_class($this)));
310
311 if ($this->isConnected()) {
312 return true;
313 }
314
315 try {
316 $this->authenticateCheckError();
317
318 $code = filter_input($_SERVER['REQUEST_METHOD'] === 'POST' ? INPUT_POST : INPUT_GET, 'code');
319
320 if (empty($code)) {
321 $this->authenticateBegin();
322 } else {
323 $this->authenticateFinish();
324 }
325 } catch (Exception $e) {
326 $this->clearStoredData();
327
328 throw $e;
329 }
330
331 return null;
332 }
333
334 /**
335 * {@inheritdoc}
336 */
337 public function isConnected()
338 {
339 if ((bool)$this->getStoredData('access_token')) {
340 return (!$this->hasAccessTokenExpired() || $this->isRefreshTokenAvailable());
341 }
342 return false;
343 }
344
345 /**
346 * If we can use a refresh token, then an expired token does not stop us being connected.
347 *
348 * @return bool
349 */
350 public function isRefreshTokenAvailable()
351 {
352 return is_array($this->tokenRefreshParameters);
353 }
354
355 /**
356 * Authorization Request Error Response
357 *
358 * RFC6749: If the request fails due to a missing, invalid, or mismatching
359 * redirection URI, or if the client identifier is missing or invalid,
360 * the authorization server SHOULD inform the resource owner of the error.
361 *
362 * http://tools.ietf.org/html/rfc6749#section-4.1.2.1
363 *
364 * @throws \Hybridauth\Exception\InvalidAuthorizationCodeException
365 * @throws \Hybridauth\Exception\AuthorizationDeniedException
366 */
367 protected function authenticateCheckError()
368 {
369 $error = filter_input(INPUT_GET, 'error', FILTER_SANITIZE_SPECIAL_CHARS);
370
371 if (!empty($error)) {
372 $error_description = filter_input(INPUT_GET, 'error_description', FILTER_SANITIZE_SPECIAL_CHARS);
373 $error_uri = filter_input(INPUT_GET, 'error_uri', FILTER_SANITIZE_SPECIAL_CHARS);
374
375 $collated_error = sprintf('Provider returned an error: %s %s %s', $error, $error_description, $error_uri);
376
377 if ($error == 'access_denied') {
378 throw new AuthorizationDeniedException($collated_error);
379 }
380
381 throw new InvalidAuthorizationCodeException($collated_error);
382 }
383 }
384
385 /**
386 * Initiate the authorization protocol
387 *
388 * Build Authorization URL for Authorization Request and redirect the user-agent to the
389 * Authorization Server.
390 */
391 protected function authenticateBegin()
392 {
393 $authUrl = $this->getAuthorizeUrl();
394
395 $this->logger->debug(sprintf('%s::authenticateBegin(), redirecting user to:', get_class($this)), [$authUrl]);
396
397 HttpClient\Util::redirect($authUrl);
398 }
399
400 /**
401 * Finalize the authorization process
402 *
403 * @throws \Hybridauth\Exception\HttpClientFailureException
404 * @throws \Hybridauth\Exception\HttpRequestFailedException
405 * @throws InvalidAccessTokenException
406 * @throws InvalidAuthorizationStateException
407 */
408 protected function authenticateFinish()
409 {
410 $this->logger->debug(
411 sprintf('%s::authenticateFinish(), callback url:', get_class($this)),
412 [HttpClient\Util::getCurrentUrl(true)]
413 );
414
415 $state = filter_input($_SERVER['REQUEST_METHOD'] === 'POST' ? INPUT_POST : INPUT_GET, 'state');
416 $code = filter_input($_SERVER['REQUEST_METHOD'] === 'POST' ? INPUT_POST : INPUT_GET, 'code');
417
418 /**
419 * Authorization Request State
420 *
421 * RFC6749: state : RECOMMENDED. An opaque value used by the client to maintain
422 * state between the request and callback. The authorization server includes
423 * this value when redirecting the user-agent back to the client.
424 *
425 * http://tools.ietf.org/html/rfc6749#section-4.1.1
426 */
427 if ($this->supportRequestState
428 && (!$state || $this->getStoredData('authorization_state') != $state)
429 ) {
430 $this->deleteStoredData('authorization_state');
431 throw new InvalidAuthorizationStateException(
432 'The authorization state [state=' . substr(htmlentities($state), 0, 100) . '] '
433 . 'of this page is either invalid or has already been consumed.'
434 );
435 }
436
437 /**
438 * Authorization Request Code
439 *
440 * RFC6749: If the resource owner grants the access request, the authorization
441 * server issues an authorization code and delivers it to the client:
442 *
443 * http://tools.ietf.org/html/rfc6749#section-4.1.2
444 */
445 $response = $this->exchangeCodeForAccessToken($code);
446
447 $this->validateAccessTokenExchange($response);
448
449 $this->initialize();
450 }
451
452 /**
453 * Build Authorization URL for Authorization Request
454 *
455 * RFC6749: The client constructs the request URI by adding the following
456 * $parameters to the query component of the authorization endpoint URI:
457 *
458 * - response_type REQUIRED. Value MUST be set to "code".
459 * - client_id REQUIRED.
460 * - redirect_uri OPTIONAL.
461 * - scope OPTIONAL.
462 * - state RECOMMENDED.
463 *
464 * http://tools.ietf.org/html/rfc6749#section-4.1.1
465 *
466 * Sub classes may redefine this method when necessary.
467 *
468 * @param array $parameters
469 *
470 * @return string Authorization URL
471 */
472 protected function getAuthorizeUrl($parameters = [])
473 {
474 $this->AuthorizeUrlParameters = !empty($parameters)
475 ? $parameters
476 : array_replace(
477 (array)$this->AuthorizeUrlParameters,
478 (array)$this->config->get('authorize_url_parameters')
479 );
480
481 if ($this->supportRequestState) {
482 if (!isset($this->AuthorizeUrlParameters['state'])) {
483 $this->AuthorizeUrlParameters['state'] = 'HA-' . str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890');
484 }
485
486 $this->storeData('authorization_state', $this->AuthorizeUrlParameters['state']);
487 }
488
489 $queryParams = http_build_query($this->AuthorizeUrlParameters, '', '&', $this->AuthorizeUrlParametersEncType);
490 return $this->authorizeUrl . '?' . $queryParams;
491 }
492
493 /**
494 * Access Token Request
495 *
496 * This method will exchange the received $code in loginFinish() with an Access Token.
497 *
498 * RFC6749: The client makes a request to the token endpoint by sending the
499 * following parameters using the "application/x-www-form-urlencoded"
500 * with a character encoding of UTF-8 in the HTTP request entity-body:
501 *
502 * - grant_type REQUIRED. Value MUST be set to "authorization_code".
503 * - code REQUIRED. The authorization code received from the authorization server.
504 * - redirect_uri REQUIRED.
505 * - client_id REQUIRED.
506 *
507 * http://tools.ietf.org/html/rfc6749#section-4.1.3
508 *
509 * @param string $code
510 *
511 * @return string Raw Provider API response
512 * @throws \Hybridauth\Exception\HttpClientFailureException
513 * @throws \Hybridauth\Exception\HttpRequestFailedException
514 */
515 protected function exchangeCodeForAccessToken($code)
516 {
517 $this->tokenExchangeParameters['code'] = $code;
518
519 $response = $this->httpClient->request(
520 $this->accessTokenUrl,
521 $this->tokenExchangeMethod,
522 $this->tokenExchangeParameters,
523 $this->tokenExchangeHeaders
524 );
525
526 $this->validateApiResponse('Unable to exchange code for API access token');
527
528 return $response;
529 }
530
531 /**
532 * Validate Access Token Response
533 *
534 * RFC6749: If the access token request is valid and authorized, the
535 * authorization server issues an access token and optional refresh token.
536 * If the request client authentication failed or is invalid, the authorization
537 * server returns an error response as described in Section 5.2.
538 *
539 * Example of a successful response:
540 *
541 * HTTP/1.1 200 OK
542 * Content-Type: application/json;charset=UTF-8
543 * Cache-Control: no-store
544 * Pragma: no-cache
545 *
546 * {
547 * "access_token":"2YotnFZFEjr1zCsicMWpAA",
548 * "token_type":"example",
549 * "expires_in":3600,
550 * "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
551 * "example_parameter":"example_value"
552 * }
553 *
554 * http://tools.ietf.org/html/rfc6749#section-4.1.4
555 *
556 * This method uses Data_Parser to attempt to decodes the raw $response (usually JSON)
557 * into a data collection.
558 *
559 * @param string $response
560 *
561 * @return \Hybridauth\Data\Collection
562 * @throws InvalidAccessTokenException
563 */
564 protected function validateAccessTokenExchange($response)
565 {
566 $data = (new Data\Parser())->parse($response);
567
568 $collection = new Data\Collection($data);
569
570 if (!$collection->exists('access_token')) {
571 throw new InvalidAccessTokenException(
572 'Provider returned no access_token: ' . htmlentities($response)
573 );
574 }
575
576 $this->storeData('access_token', $collection->get('access_token'));
577 $this->storeData('token_type', $collection->get('token_type'));
578
579 if ($collection->get('refresh_token')) {
580 $this->storeData('refresh_token', $collection->get('refresh_token'));
581 }
582
583 // calculate when the access token expire
584 if ($collection->exists('expires_in')) {
585 $expires_at = time() + (int)$collection->get('expires_in');
586
587 $this->storeData('expires_in', $collection->get('expires_in'));
588 $this->storeData('expires_at', $expires_at);
589 }
590
591 $this->deleteStoredData('authorization_state');
592
593 $this->initialize();
594
595 return $collection;
596 }
597
598 /**
599 * Refreshing an Access Token
600 *
601 * RFC6749: If the authorization server issued a refresh token to the
602 * client, the client makes a refresh request to the token endpoint by
603 * adding the following parameters ... in the HTTP request entity-body:
604 *
605 * - grant_type REQUIRED. Value MUST be set to "refresh_token".
606 * - refresh_token REQUIRED. The refresh token issued to the client.
607 * - scope OPTIONAL.
608 *
609 * http://tools.ietf.org/html/rfc6749#section-6
610 *
611 * This method is similar to exchangeCodeForAccessToken(). The only
612 * difference is here we exchange refresh_token for a new access_token.
613 *
614 * @param array $parameters
615 *
616 * @return string|null Raw Provider API response, or null if we cannot refresh
617 * @throws \Hybridauth\Exception\HttpClientFailureException
618 * @throws \Hybridauth\Exception\HttpRequestFailedException
619 * @throws InvalidAccessTokenException
620 */
621 public function refreshAccessToken($parameters = [])
622 {
623 $this->tokenRefreshParameters = !empty($parameters)
624 ? $parameters
625 : $this->tokenRefreshParameters;
626
627 if (!$this->isRefreshTokenAvailable()) {
628 return null;
629 }
630
631 $response = $this->httpClient->request(
632 $this->accessTokenUrl,
633 $this->tokenRefreshMethod,
634 $this->tokenRefreshParameters,
635 $this->tokenRefreshHeaders
636 );
637
638 $this->validateApiResponse('Unable to refresh the access token');
639
640 $this->validateRefreshAccessToken($response);
641
642 return $response;
643 }
644
645 /**
646 * Check whether access token has expired
647 *
648 * @param int|null $time
649 * @return bool|null
650 */
651 public function hasAccessTokenExpired($time = null)
652 {
653 if ($time === null) {
654 $time = time();
655 }
656
657 $expires_at = $this->getStoredData('expires_at');
658 if (!$expires_at) {
659 return null;
660 }
661
662 return $expires_at <= $time;
663 }
664
665 /**
666 * Validate Refresh Access Token Request
667 *
668 * RFC6749: If valid and authorized, the authorization server issues an
669 * access token as described in Section 5.1. If the request failed
670 * verification or is invalid, the authorization server returns an error
671 * response as described in Section 5.2.
672 *
673 * http://tools.ietf.org/html/rfc6749#section-6
674 * http://tools.ietf.org/html/rfc6749#section-5.1
675 * http://tools.ietf.org/html/rfc6749#section-5.2
676 *
677 * This method simply use validateAccessTokenExchange(), however sub
678 * classes may redefine it when necessary.
679 *
680 * @param $response
681 *
682 * @return \Hybridauth\Data\Collection
683 * @throws InvalidAccessTokenException
684 */
685 protected function validateRefreshAccessToken($response)
686 {
687 return $this->validateAccessTokenExchange($response);
688 }
689
690 /**
691 * Send a signed request to provider API
692 *
693 * RFC6749: Accessing Protected Resources: The client accesses protected
694 * resources by presenting the access token to the resource server. The
695 * resource server MUST validate the access token and ensure that it has
696 * not expired and that its scope covers the requested resource.
697 *
698 * Note: Since the specifics of error responses is beyond the scope of
699 * RFC6749 and OAuth specifications, Hybridauth will consider any HTTP
700 * status code that is different than '200 OK' as an ERROR.
701 *
702 * http://tools.ietf.org/html/rfc6749#section-7
703 *
704 * @param string $url
705 * @param string $method
706 * @param array $parameters
707 * @param array $headers
708 * @param bool $multipart
709 *
710 * @return mixed
711 * @throws \Hybridauth\Exception\HttpClientFailureException
712 * @throws \Hybridauth\Exception\HttpRequestFailedException
713 * @throws InvalidAccessTokenException
714 */
715 public function apiRequest($url, $method = 'GET', $parameters = [], $headers = [], $multipart = false)
716 {
717 // refresh tokens if needed
718 $this->maintainToken();
719 if ($this->hasAccessTokenExpired() === true) {
720 $this->refreshAccessToken();
721 }
722
723 if (strrpos($url, 'http://') !== 0 && strrpos($url, 'https://') !== 0) {
724 $url = rtrim($this->apiBaseUrl, '/') . '/' . ltrim($url, '/');
725 }
726
727 $parameters = array_replace($this->apiRequestParameters, (array)$parameters);
728 $headers = array_replace($this->apiRequestHeaders, (array)$headers);
729
730 $response = $this->httpClient->request(
731 $url,
732 $method, // HTTP Request Method. Defaults to GET.
733 $parameters, // Request Parameters
734 $headers, // Request Headers
735 $multipart // Is request multipart
736 );
737
738 $this->validateApiResponse('Signed API request to ' . $url . ' has returned an error');
739
740 $response = (new Data\Parser())->parse($response);
741
742 return $response;
743 }
744 }
745