4
* This file is part of the league/oauth2-client library
6
* For the full copyright and license information, please view the LICENSE
7
* file that was distributed with this source code.
9
* @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com>
10
* @license http://opensource.org/licenses/MIT MIT
11
* @link http://thephpleague.com/oauth2-client/ Documentation
12
* @link https://packagist.org/packages/league/oauth2-client Packagist
13
* @link https://github.com/thephpleague/oauth2-client GitHub
15
namespace YoastSEO_Vendor\League\OAuth2\Client\Provider;
17
use YoastSEO_Vendor\GuzzleHttp\Client as HttpClient;
18
use YoastSEO_Vendor\GuzzleHttp\ClientInterface as HttpClientInterface;
19
use YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException;
20
use YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant;
21
use YoastSEO_Vendor\League\OAuth2\Client\Grant\GrantFactory;
22
use YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\OptionProviderInterface;
23
use YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\PostAuthOptionProvider;
24
use YoastSEO_Vendor\League\OAuth2\Client\Provider\Exception\IdentityProviderException;
25
use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken;
26
use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessTokenInterface;
27
use YoastSEO_Vendor\League\OAuth2\Client\Tool\ArrayAccessorTrait;
28
use YoastSEO_Vendor\League\OAuth2\Client\Tool\GuardedPropertyTrait;
29
use YoastSEO_Vendor\League\OAuth2\Client\Tool\QueryBuilderTrait;
30
use YoastSEO_Vendor\League\OAuth2\Client\Tool\RequestFactory;
31
use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
32
use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface;
33
use UnexpectedValueException;
35
* Represents a service provider (authorization server).
37
* @link http://tools.ietf.org/html/rfc6749#section-1.1 Roles (RFC 6749, §1.1)
39
abstract class AbstractProvider
41
use ArrayAccessorTrait;
42
use GuardedPropertyTrait;
43
use QueryBuilderTrait;
45
* @var string Key used in a token response to identify the resource owner.
47
const ACCESS_TOKEN_RESOURCE_OWNER_ID = null;
49
* @var string HTTP method used to fetch access tokens.
51
const METHOD_GET = 'GET';
53
* @var string HTTP method used to fetch access tokens.
55
const METHOD_POST = 'POST';
63
protected $clientSecret;
67
protected $redirectUri;
75
protected $grantFactory;
79
protected $requestFactory;
81
* @var HttpClientInterface
83
protected $httpClient;
85
* @var OptionProviderInterface
87
protected $optionProvider;
89
* Constructs an OAuth 2.0 service provider.
91
* @param array $options An array of options to set on this provider.
92
* Options include `clientId`, `clientSecret`, `redirectUri`, and `state`.
93
* Individual providers may introduce more options, as needed.
94
* @param array $collaborators An array of collaborators that may be used to
95
* override this provider's default behavior. Collaborators include
96
* `grantFactory`, `requestFactory`, and `httpClient`.
97
* Individual providers may introduce more collaborators, as needed.
99
public function __construct(array $options = [], array $collaborators = [])
101
// We'll let the GuardedPropertyTrait handle mass assignment of incoming
102
// options, skipping any blacklisted properties defined in the provider
103
$this->fillProperties($options);
104
if (empty($collaborators['grantFactory'])) {
105
$collaborators['grantFactory'] = new \YoastSEO_Vendor\League\OAuth2\Client\Grant\GrantFactory();
107
$this->setGrantFactory($collaborators['grantFactory']);
108
if (empty($collaborators['requestFactory'])) {
109
$collaborators['requestFactory'] = new \YoastSEO_Vendor\League\OAuth2\Client\Tool\RequestFactory();
111
$this->setRequestFactory($collaborators['requestFactory']);
112
if (empty($collaborators['httpClient'])) {
113
$client_options = $this->getAllowedClientOptions($options);
114
$collaborators['httpClient'] = new \YoastSEO_Vendor\GuzzleHttp\Client(\array_intersect_key($options, \array_flip($client_options)));
116
$this->setHttpClient($collaborators['httpClient']);
117
if (empty($collaborators['optionProvider'])) {
118
$collaborators['optionProvider'] = new \YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\PostAuthOptionProvider();
120
$this->setOptionProvider($collaborators['optionProvider']);
123
* Returns the list of options that can be passed to the HttpClient
125
* @param array $options An array of options to set on this provider.
126
* Options include `clientId`, `clientSecret`, `redirectUri`, and `state`.
127
* Individual providers may introduce more options, as needed.
128
* @return array The options to pass to the HttpClient constructor
130
protected function getAllowedClientOptions(array $options)
132
$client_options = ['timeout', 'proxy'];
133
// Only allow turning off ssl verification if it's for a proxy
134
if (!empty($options['proxy'])) {
135
$client_options[] = 'verify';
137
return $client_options;
140
* Sets the grant factory instance.
142
* @param GrantFactory $factory
145
public function setGrantFactory(\YoastSEO_Vendor\League\OAuth2\Client\Grant\GrantFactory $factory)
147
$this->grantFactory = $factory;
151
* Returns the current grant factory instance.
153
* @return GrantFactory
155
public function getGrantFactory()
157
return $this->grantFactory;
160
* Sets the request factory instance.
162
* @param RequestFactory $factory
165
public function setRequestFactory(\YoastSEO_Vendor\League\OAuth2\Client\Tool\RequestFactory $factory)
167
$this->requestFactory = $factory;
171
* Returns the request factory instance.
173
* @return RequestFactory
175
public function getRequestFactory()
177
return $this->requestFactory;
180
* Sets the HTTP client instance.
182
* @param HttpClientInterface $client
185
public function setHttpClient(\YoastSEO_Vendor\GuzzleHttp\ClientInterface $client)
187
$this->httpClient = $client;
191
* Returns the HTTP client instance.
193
* @return HttpClientInterface
195
public function getHttpClient()
197
return $this->httpClient;
200
* Sets the option provider instance.
202
* @param OptionProviderInterface $provider
205
public function setOptionProvider(\YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\OptionProviderInterface $provider)
207
$this->optionProvider = $provider;
211
* Returns the option provider instance.
213
* @return OptionProviderInterface
215
public function getOptionProvider()
217
return $this->optionProvider;
220
* Returns the current value of the state parameter.
222
* This can be accessed by the redirect handler during authorization.
226
public function getState()
231
* Returns the base URL for authorizing a client.
233
* Eg. https://oauth.service.com/authorize
237
public abstract function getBaseAuthorizationUrl();
239
* Returns the base URL for requesting an access token.
241
* Eg. https://oauth.service.com/token
243
* @param array $params
246
public abstract function getBaseAccessTokenUrl(array $params);
248
* Returns the URL for requesting the resource owner's details.
250
* @param AccessToken $token
253
public abstract function getResourceOwnerDetailsUrl(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token);
255
* Returns a new random string to use as the state parameter in an
256
* authorization flow.
258
* @param int $length Length of the random string to be generated.
261
protected function getRandomState($length = 32)
263
// Converting bytes to hex will always double length. Hence, we can reduce
264
// the amount of bytes by half to produce the correct length.
265
return \bin2hex(\random_bytes($length / 2));
268
* Returns the default scopes used by this provider.
270
* This should only be the scopes that are required to request the details
271
* of the resource owner, rather than all the available scopes.
275
protected abstract function getDefaultScopes();
277
* Returns the string that should be used to separate scopes when building
278
* the URL for requesting an access token.
280
* @return string Scope separator, defaults to ','
282
protected function getScopeSeparator()
287
* Returns authorization parameters based on provided options.
289
* @param array $options
290
* @return array Authorization parameters
292
protected function getAuthorizationParameters(array $options)
294
if (empty($options['state'])) {
295
$options['state'] = $this->getRandomState();
297
if (empty($options['scope'])) {
298
$options['scope'] = $this->getDefaultScopes();
300
$options += ['response_type' => 'code', 'approval_prompt' => 'auto'];
301
if (\is_array($options['scope'])) {
302
$separator = $this->getScopeSeparator();
303
$options['scope'] = \implode($separator, $options['scope']);
305
// Store the state as it may need to be accessed later on.
306
$this->state = $options['state'];
307
// Business code layer might set a different redirect_uri parameter
308
// depending on the context, leave it as-is
309
if (!isset($options['redirect_uri'])) {
310
$options['redirect_uri'] = $this->redirectUri;
312
$options['client_id'] = $this->clientId;
316
* Builds the authorization URL's query string.
318
* @param array $params Query parameters
319
* @return string Query string
321
protected function getAuthorizationQuery(array $params)
323
return $this->buildQueryString($params);
326
* Builds the authorization URL.
328
* @param array $options
329
* @return string Authorization URL
331
public function getAuthorizationUrl(array $options = [])
333
$base = $this->getBaseAuthorizationUrl();
334
$params = $this->getAuthorizationParameters($options);
335
$query = $this->getAuthorizationQuery($params);
336
return $this->appendQuery($base, $query);
339
* Redirects the client for authorization.
341
* @param array $options
342
* @param callable|null $redirectHandler
345
public function authorize(array $options = [], callable $redirectHandler = null)
347
$url = $this->getAuthorizationUrl($options);
348
if ($redirectHandler) {
349
return $redirectHandler($url, $this);
351
// @codeCoverageIgnoreStart
352
\header('Location: ' . $url);
354
// @codeCoverageIgnoreEnd
357
* Appends a query string to a URL.
359
* @param string $url The URL to append the query to
360
* @param string $query The HTTP query string
361
* @return string The resulting URL
363
protected function appendQuery($url, $query)
365
$query = \trim($query, '?&');
367
$glue = \strstr($url, '?') === \false ? '?' : '&';
368
return $url . $glue . $query;
373
* Returns the method to use when requesting an access token.
375
* @return string HTTP method
377
protected function getAccessTokenMethod()
379
return self::METHOD_POST;
382
* Returns the key used in the access token response to identify the resource owner.
384
* @return string|null Resource owner identifier key
386
protected function getAccessTokenResourceOwnerId()
388
return static::ACCESS_TOKEN_RESOURCE_OWNER_ID;
391
* Builds the access token URL's query string.
393
* @param array $params Query parameters
394
* @return string Query string
396
protected function getAccessTokenQuery(array $params)
398
return $this->buildQueryString($params);
401
* Checks that a provided grant is valid, or attempts to produce one if the
402
* provided grant is a string.
404
* @param AbstractGrant|string $grant
405
* @return AbstractGrant
407
protected function verifyGrant($grant)
409
if (\is_string($grant)) {
410
return $this->grantFactory->getGrant($grant);
412
$this->grantFactory->checkGrant($grant);
416
* Returns the full URL to use when requesting an access token.
418
* @param array $params Query parameters
421
protected function getAccessTokenUrl(array $params)
423
$url = $this->getBaseAccessTokenUrl($params);
424
if ($this->getAccessTokenMethod() === self::METHOD_GET) {
425
$query = $this->getAccessTokenQuery($params);
426
return $this->appendQuery($url, $query);
431
* Returns a prepared request for requesting an access token.
433
* @param array $params Query string parameters
434
* @return RequestInterface
436
protected function getAccessTokenRequest(array $params)
438
$method = $this->getAccessTokenMethod();
439
$url = $this->getAccessTokenUrl($params);
440
$options = $this->optionProvider->getAccessTokenOptions($this->getAccessTokenMethod(), $params);
441
return $this->getRequest($method, $url, $options);
444
* Requests an access token using a specified grant and option set.
446
* @param mixed $grant
447
* @param array $options
448
* @throws IdentityProviderException
449
* @return AccessTokenInterface
451
public function getAccessToken($grant, array $options = [])
453
$grant = $this->verifyGrant($grant);
454
$params = ['client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'redirect_uri' => $this->redirectUri];
455
$params = $grant->prepareRequestParameters($params, $options);
456
$request = $this->getAccessTokenRequest($params);
457
$response = $this->getParsedResponse($request);
458
if (\false === \is_array($response)) {
459
throw new \UnexpectedValueException('Invalid response received from Authorization Server. Expected JSON.');
461
$prepared = $this->prepareAccessTokenResponse($response);
462
$token = $this->createAccessToken($prepared, $grant);
466
* Returns a PSR-7 request instance that is not authenticated.
468
* @param string $method
470
* @param array $options
471
* @return RequestInterface
473
public function getRequest($method, $url, array $options = [])
475
return $this->createRequest($method, $url, null, $options);
478
* Returns an authenticated PSR-7 request instance.
480
* @param string $method
482
* @param AccessTokenInterface|string $token
483
* @param array $options Any of "headers", "body", and "protocolVersion".
484
* @return RequestInterface
486
public function getAuthenticatedRequest($method, $url, $token, array $options = [])
488
return $this->createRequest($method, $url, $token, $options);
491
* Creates a PSR-7 request instance.
493
* @param string $method
495
* @param AccessTokenInterface|string|null $token
496
* @param array $options
497
* @return RequestInterface
499
protected function createRequest($method, $url, $token, array $options)
501
$defaults = ['headers' => $this->getHeaders($token)];
502
$options = \array_merge_recursive($defaults, $options);
503
$factory = $this->getRequestFactory();
504
return $factory->getRequestWithOptions($method, $url, $options);
507
* Sends a request instance and returns a response instance.
509
* WARNING: This method does not attempt to catch exceptions caused by HTTP
510
* errors! It is recommended to wrap this method in a try/catch block.
512
* @param RequestInterface $request
513
* @return ResponseInterface
515
public function getResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request)
517
return $this->getHttpClient()->send($request);
520
* Sends a request and returns the parsed response.
522
* @param RequestInterface $request
523
* @throws IdentityProviderException
526
public function getParsedResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request)
529
$response = $this->getResponse($request);
530
} catch (\YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException $e) {
531
$response = $e->getResponse();
533
$parsed = $this->parseResponse($response);
534
$this->checkResponse($response, $parsed);
538
* Attempts to parse a JSON response.
540
* @param string $content JSON content from response body
541
* @return array Parsed JSON data
542
* @throws UnexpectedValueException if the content could not be parsed
544
protected function parseJson($content)
546
$content = \json_decode($content, \true);
547
if (\json_last_error() !== \JSON_ERROR_NONE) {
548
throw new \UnexpectedValueException(\sprintf("Failed to parse JSON response: %s", \json_last_error_msg()));
553
* Returns the content type header of a response.
555
* @param ResponseInterface $response
556
* @return string Semi-colon separated join of content-type headers.
558
protected function getContentType(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response)
560
return \join(';', (array) $response->getHeader('content-type'));
563
* Parses the response according to its content-type header.
565
* @throws UnexpectedValueException
566
* @param ResponseInterface $response
569
protected function parseResponse(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response)
571
$content = (string) $response->getBody();
572
$type = $this->getContentType($response);
573
if (\strpos($type, 'urlencoded') !== \false) {
574
\parse_str($content, $parsed);
577
// Attempt to parse the string as JSON regardless of content type,
578
// since some providers use non-standard content types. Only throw an
579
// exception if the JSON could not be parsed when it was expected to.
581
return $this->parseJson($content);
582
} catch (\UnexpectedValueException $e) {
583
if (\strpos($type, 'json') !== \false) {
586
if ($response->getStatusCode() == 500) {
587
throw new \UnexpectedValueException('An OAuth server error was encountered that did not contain a JSON body', 0, $e);
593
* Checks a provider response for errors.
595
* @throws IdentityProviderException
596
* @param ResponseInterface $response
597
* @param array|string $data Parsed response data
600
protected abstract function checkResponse(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response, $data);
602
* Prepares an parsed access token response for a grant.
604
* Custom mapping of expiration, etc should be done here. Always call the
605
* parent method when overloading this method.
607
* @param mixed $result
610
protected function prepareAccessTokenResponse(array $result)
612
if ($this->getAccessTokenResourceOwnerId() !== null) {
613
$result['resource_owner_id'] = $this->getValueByKey($result, $this->getAccessTokenResourceOwnerId());
618
* Creates an access token from a response.
620
* The grant that was used to fetch the response can be used to provide
621
* additional context.
623
* @param array $response
624
* @param AbstractGrant $grant
625
* @return AccessTokenInterface
627
protected function createAccessToken(array $response, \YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant $grant)
629
return new \YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken($response);
632
* Generates a resource owner object from a successful resource owner
635
* @param array $response
636
* @param AccessToken $token
637
* @return ResourceOwnerInterface
639
protected abstract function createResourceOwner(array $response, \YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token);
641
* Requests and returns the resource owner of given access token.
643
* @param AccessToken $token
644
* @return ResourceOwnerInterface
646
public function getResourceOwner(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token)
648
$response = $this->fetchResourceOwnerDetails($token);
649
return $this->createResourceOwner($response, $token);
652
* Requests resource owner details.
654
* @param AccessToken $token
657
protected function fetchResourceOwnerDetails(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token)
659
$url = $this->getResourceOwnerDetailsUrl($token);
660
$request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token);
661
$response = $this->getParsedResponse($request);
662
if (\false === \is_array($response)) {
663
throw new \UnexpectedValueException('Invalid response received from Authorization Server. Expected JSON.');
668
* Returns the default headers used by this provider.
670
* Typically this is used to set 'Accept' or 'Content-Type' headers.
674
protected function getDefaultHeaders()
679
* Returns the authorization headers used by this provider.
681
* Typically this is "Bearer" or "MAC". For more information see:
682
* http://tools.ietf.org/html/rfc6749#section-7.1
684
* No default is provided, providers must overload this method to activate
685
* authorization headers.
687
* @param mixed|null $token Either a string or an access token instance
690
protected function getAuthorizationHeaders($token = null)
695
* Returns all headers used by this provider for a request.
697
* The request will be authenticated if an access token is provided.
699
* @param mixed|null $token object or string
702
public function getHeaders($token = null)
705
return \array_merge($this->getDefaultHeaders(), $this->getAuthorizationHeaders($token));
707
return $this->getDefaultHeaders();