← All changes
|
vendor_prefixed/league/oauth2-client/src/Provider/AbstractProvider.php
+102
-9
18.1
→
trunk
View file →
| @@ -16,8 +16,10 @@ | ||
| 16 | 16 | |
| 17 | 17 | use YoastSEO_Vendor\GuzzleHttp\Client as HttpClient; |
| 18 | 18 | use YoastSEO_Vendor\GuzzleHttp\ClientInterface as HttpClientInterface; |
| 19 | 19 | use YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException; |
| 20 | +use YoastSEO_Vendor\GuzzleHttp\Exception\GuzzleException; | |
| 21 | +use InvalidArgumentException; | |
| 20 | 22 | use YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant; |
| 21 | 23 | use YoastSEO_Vendor\League\OAuth2\Client\Grant\GrantFactory; |
| 22 | 24 | use YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\OptionProviderInterface; |
| 23 | 25 | use YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\PostAuthOptionProvider; |
| @@ -41,9 +43,9 @@ | ||
| 41 | 43 | use ArrayAccessorTrait; |
| 42 | 44 | use GuardedPropertyTrait; |
| 43 | 45 | use QueryBuilderTrait; |
| 44 | 46 | /** |
| 45 | - * @var string Key used in a token response to identify the resource owner. | |
| 47 | + * @var string|null Key used in a token response to identify the resource owner. | |
| 46 | 48 | */ |
| 47 | 49 | const ACCESS_TOKEN_RESOURCE_OWNER_ID = null; |
| 48 | 50 | /** |
| 49 | 51 | * @var string HTTP method used to fetch access tokens. |
| @@ -53,8 +55,19 @@ | ||
| 53 | 55 | * @var string HTTP method used to fetch access tokens. |
| 54 | 56 | */ |
| 55 | 57 | const METHOD_POST = 'POST'; |
| 56 | 58 | /** |
| 59 | + * @var string PKCE method used to fetch authorization token. | |
| 60 | + * The PKCE code challenge will be hashed with sha256 (recommended). | |
| 61 | + */ | |
| 62 | + const PKCE_METHOD_S256 = 'S256'; | |
| 63 | + /** | |
| 64 | + * @var string PKCE method used to fetch authorization token. | |
| 65 | + * The PKCE code challenge will be sent as plain text, this is NOT recommended. | |
| 66 | + * Only use `plain` if no other option is possible. | |
| 67 | + */ | |
| 68 | + const PKCE_METHOD_PLAIN = 'plain'; | |
| 69 | + /** | |
| 57 | 70 | * @var string |
| 58 | 71 | */ |
| 59 | 72 | protected $clientId; |
| 60 | 73 | /** |
| @@ -69,8 +82,12 @@ | ||
| 69 | 82 | * @var string |
| 70 | 83 | */ |
| 71 | 84 | protected $state; |
| 72 | 85 | /** |
| 86 | + * @var string|null | |
| 87 | + */ | |
| 88 | + protected $pkceCode = null; | |
| 89 | + /** | |
| 73 | 90 | * @var GrantFactory |
| 74 | 91 | */ |
| 75 | 92 | protected $grantFactory; |
| 76 | 93 | /** |
| @@ -227,8 +244,32 @@ | ||
| 227 | 244 | { |
| 228 | 245 | return $this->state; |
| 229 | 246 | } |
| 230 | 247 | /** |
| 248 | + * Set the value of the pkceCode parameter. | |
| 249 | + * | |
| 250 | + * When using PKCE this should be set before requesting an access token. | |
| 251 | + * | |
| 252 | + * @param string $pkceCode | |
| 253 | + * @return self | |
| 254 | + */ | |
| 255 | + public function setPkceCode($pkceCode) | |
| 256 | + { | |
| 257 | + $this->pkceCode = $pkceCode; | |
| 258 | + return $this; | |
| 259 | + } | |
| 260 | + /** | |
| 261 | + * Returns the current value of the pkceCode parameter. | |
| 262 | + * | |
| 263 | + * This can be accessed by the redirect handler during authorization. | |
| 264 | + * | |
| 265 | + * @return string|null | |
| 266 | + */ | |
| 267 | + public function getPkceCode() | |
| 268 | + { | |
| 269 | + return $this->pkceCode; | |
| 270 | + } | |
| 271 | + /** | |
| 231 | 272 | * Returns the base URL for authorizing a client. |
| 232 | 273 | * |
| 233 | 274 | * Eg. https://oauth.service.com/authorize |
| 234 | 275 | * |
| @@ -264,8 +305,20 @@ | ||
| 264 | 305 | // the amount of bytes by half to produce the correct length. |
| 265 | 306 | return \bin2hex(\random_bytes($length / 2)); |
| 266 | 307 | } |
| 267 | 308 | /** |
| 309 | + * Returns a new random string to use as PKCE code_verifier and | |
| 310 | + * hashed as code_challenge parameters in an authorization flow. | |
| 311 | + * Must be between 43 and 128 characters long. | |
| 312 | + * | |
| 313 | + * @param int $length Length of the random string to be generated. | |
| 314 | + * @return string | |
| 315 | + */ | |
| 316 | + protected function getRandomPkceCode($length = 64) | |
| 317 | + { | |
| 318 | + return \substr(\strtr(\base64_encode(\random_bytes($length)), '+/', '-_'), 0, $length); | |
| 319 | + } | |
| 320 | + /** | |
| 268 | 321 | * Returns the default scopes used by this provider. |
| 269 | 322 | * |
| 270 | 323 | * This should only be the scopes that are required to request the details |
| 271 | 324 | * of the resource owner, rather than all the available scopes. |
| @@ -283,12 +336,20 @@ | ||
| 283 | 336 | { |
| 284 | 337 | return ','; |
| 285 | 338 | } |
| 286 | 339 | /** |
| 340 | + * @return string|null | |
| 341 | + */ | |
| 342 | + protected function getPkceMethod() | |
| 343 | + { | |
| 344 | + return null; | |
| 345 | + } | |
| 346 | + /** | |
| 287 | 347 | * Returns authorization parameters based on provided options. |
| 288 | 348 | * |
| 289 | 349 | * @param array $options |
| 290 | 350 | * @return array Authorization parameters |
| 351 | + * @throws InvalidArgumentException | |
| 291 | 352 | */ |
| 292 | 353 | protected function getAuthorizationParameters(array $options) |
| 293 | 354 | { |
| 294 | 355 | if (empty($options['state'])) { |
| @@ -303,8 +364,20 @@ | ||
| 303 | 364 | $options['scope'] = \implode($separator, $options['scope']); |
| 304 | 365 | } |
| 305 | 366 | // Store the state as it may need to be accessed later on. |
| 306 | 367 | $this->state = $options['state']; |
| 368 | + $pkceMethod = $this->getPkceMethod(); | |
| 369 | + if (!empty($pkceMethod)) { | |
| 370 | + $this->pkceCode = $this->getRandomPkceCode(); | |
| 371 | + if ($pkceMethod === static::PKCE_METHOD_S256) { | |
| 372 | + $options['code_challenge'] = \trim(\strtr(\base64_encode(\hash('sha256', $this->pkceCode, \true)), '+/', '-_'), '='); | |
| 373 | + } elseif ($pkceMethod === static::PKCE_METHOD_PLAIN) { | |
| 374 | + $options['code_challenge'] = $this->pkceCode; | |
| 375 | + } else { | |
| 376 | + throw new \InvalidArgumentException('Unknown PKCE method "' . $pkceMethod . '".'); | |
| 377 | + } | |
| 378 | + $options['code_challenge_method'] = $pkceMethod; | |
| 379 | + } | |
| 307 | 380 | // Business code layer might set a different redirect_uri parameter |
| 308 | 381 | // depending on the context, leave it as-is |
| 309 | 382 | if (!isset($options['redirect_uri'])) { |
| 310 | 383 | $options['redirect_uri'] = $this->redirectUri; |
| @@ -326,8 +399,9 @@ | ||
| 326 | 399 | * Builds the authorization URL. |
| 327 | 400 | * |
| 328 | 401 | * @param array $options |
| 329 | 402 | * @return string Authorization URL |
| 403 | + * @throws InvalidArgumentException | |
| 330 | 404 | */ |
| 331 | 405 | public function getAuthorizationUrl(array $options = []) |
| 332 | 406 | { |
| 333 | 407 | $base = $this->getBaseAuthorizationUrl(); |
| @@ -340,10 +414,11 @@ | ||
| 340 | 414 | * |
| 341 | 415 | * @param array $options |
| 342 | 416 | * @param callable|null $redirectHandler |
| 343 | 417 | * @return mixed |
| 418 | + * @throws InvalidArgumentException | |
| 344 | 419 | */ |
| 345 | - public function authorize(array $options = [], callable $redirectHandler = null) | |
| 420 | + public function authorize(array $options = [], ?callable $redirectHandler = null) | |
| 346 | 421 | { |
| 347 | 422 | $url = $this->getAuthorizationUrl($options); |
| 348 | 423 | if ($redirectHandler) { |
| 349 | 424 | return $redirectHandler($url, $this); |
| @@ -442,17 +517,26 @@ | ||
| 442 | 517 | } |
| 443 | 518 | /** |
| 444 | 519 | * Requests an access token using a specified grant and option set. |
| 445 | 520 | * |
| 446 | - * @param mixed $grant | |
| 447 | - * @param array $options | |
| 521 | + * @param mixed $grant | |
| 522 | + * @param array<string, mixed> $options | |
| 523 | + * @return AccessTokenInterface | |
| 448 | 524 | * @throws IdentityProviderException |
| 449 | - * @return AccessTokenInterface | |
| 525 | + * @throws UnexpectedValueException | |
| 526 | + * @throws GuzzleException | |
| 450 | 527 | */ |
| 451 | 528 | public function getAccessToken($grant, array $options = []) |
| 452 | 529 | { |
| 453 | 530 | $grant = $this->verifyGrant($grant); |
| 531 | + if (isset($options['scope']) && \is_array($options['scope'])) { | |
| 532 | + $separator = $this->getScopeSeparator(); | |
| 533 | + $options['scope'] = \implode($separator, $options['scope']); | |
| 534 | + } | |
| 454 | 535 | $params = ['client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'redirect_uri' => $this->redirectUri]; |
| 536 | + if (!empty($this->pkceCode)) { | |
| 537 | + $params['code_verifier'] = $this->pkceCode; | |
| 538 | + } | |
| 455 | 539 | $params = $grant->prepareRequestParameters($params, $options); |
| 456 | 540 | $request = $this->getAccessTokenRequest($params); |
| 457 | 541 | $response = $this->getParsedResponse($request); |
| 458 | 542 | if (\false === \is_array($response)) { |
| @@ -478,9 +562,9 @@ | ||
| 478 | 562 | * Returns an authenticated PSR-7 request instance. |
| 479 | 563 | * |
| 480 | 564 | * @param string $method |
| 481 | 565 | * @param string $url |
| 482 | - * @param AccessTokenInterface|string $token | |
| 566 | + * @param AccessTokenInterface|string|null $token | |
| 483 | 567 | * @param array $options Any of "headers", "body", and "protocolVersion". |
| 484 | 568 | * @return RequestInterface |
| 485 | 569 | */ |
| 486 | 570 | public function getAuthenticatedRequest($method, $url, $token, array $options = []) |
| @@ -510,8 +594,9 @@ | ||
| 510 | 594 | * errors! It is recommended to wrap this method in a try/catch block. |
| 511 | 595 | * |
| 512 | 596 | * @param RequestInterface $request |
| 513 | 597 | * @return ResponseInterface |
| 598 | + * @throws GuzzleException | |
| 514 | 599 | */ |
| 515 | 600 | public function getResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) |
| 516 | 601 | { |
| 517 | 602 | return $this->getHttpClient()->send($request); |
| @@ -519,10 +604,12 @@ | ||
| 519 | 604 | /** |
| 520 | 605 | * Sends a request and returns the parsed response. |
| 521 | 606 | * |
| 522 | 607 | * @param RequestInterface $request |
| 608 | + * @return mixed | |
| 523 | 609 | * @throws IdentityProviderException |
| 524 | - * @return mixed | |
| 610 | + * @throws UnexpectedValueException | |
| 611 | + * @throws GuzzleException | |
| 525 | 612 | */ |
| 526 | 613 | public function getParsedResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) |
| 527 | 614 | { |
| 528 | 615 | try { |
| @@ -556,9 +643,9 @@ | ||
| 556 | 643 | * @return string Semi-colon separated join of content-type headers. |
| 557 | 644 | */ |
| 558 | 645 | protected function getContentType(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) |
| 559 | 646 | { |
| 560 | - return \join(';', (array) $response->getHeader('content-type')); | |
| 647 | + return \implode(';', $response->getHeader('content-type')); | |
| 561 | 648 | } |
| 562 | 649 | /** |
| 563 | 650 | * Parses the response according to its content-type header. |
| 564 | 651 | * |
| @@ -603,9 +690,9 @@ | ||
| 603 | 690 | * |
| 604 | 691 | * Custom mapping of expiration, etc should be done here. Always call the |
| 605 | 692 | * parent method when overloading this method. |
| 606 | 693 | * |
| 607 | - * @param mixed $result | |
| 694 | + * @param array<string, mixed> $result | |
| 608 | 695 | * @return array |
| 609 | 696 | */ |
| 610 | 697 | protected function prepareAccessTokenResponse(array $result) |
| 611 | 698 | { |
| @@ -641,8 +728,11 @@ | ||
| 641 | 728 | * Requests and returns the resource owner of given access token. |
| 642 | 729 | * |
| 643 | 730 | * @param AccessToken $token |
| 644 | 731 | * @return ResourceOwnerInterface |
| 732 | + * @throws IdentityProviderException | |
| 733 | + * @throws UnexpectedValueException | |
| 734 | + * @throws GuzzleException | |
| 645 | 735 | */ |
| 646 | 736 | public function getResourceOwner(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token) |
| 647 | 737 | { |
| 648 | 738 | $response = $this->fetchResourceOwnerDetails($token); |
| @@ -652,8 +742,11 @@ | ||
| 652 | 742 | * Requests resource owner details. |
| 653 | 743 | * |
| 654 | 744 | * @param AccessToken $token |
| 655 | 745 | * @return mixed |
| 746 | + * @throws IdentityProviderException | |
| 747 | + * @throws UnexpectedValueException | |
| 748 | + * @throws GuzzleException | |
| 656 | 749 | */ |
| 657 | 750 | protected function fetchResourceOwnerDetails(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token) |
| 658 | 751 | { |
| 659 | 752 | $url = $this->getResourceOwnerDetailsUrl($token); |