diff --git a/composer.json b/composer.json index 48a95701..cb54606c 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "zendframework/zend-diactoros": "^1.3.2", "phpstan/phpstan": "^0.9.2", "phpstan/phpstan-phpunit": "^0.9.4", - "phpstan/phpstan-strict-rules": "^0.9.0" + "phpstan/phpstan-strict-rules": "^0.9.0", + "roave/security-advisories": "dev-master" }, "repositories": [ { @@ -46,6 +47,12 @@ "email": "hello@alexbilbie.com", "homepage": "http://www.alexbilbie.com", "role": "Developer" + }, + { + "name": "Andy Millington", + "email": "andrew@noexceptions.io", + "homepage": "https://www.noexceptions.io", + "role": "Developer" } ], "replace": { diff --git a/examples/composer.json b/examples/composer.json index ec7387cf..a3ef9b7a 100644 --- a/examples/composer.json +++ b/examples/composer.json @@ -1,14 +1,13 @@ { "require": { - "slim/slim": "3.0.*" + "slim/slim": "^3.0.0" }, "require-dev": { "league/event": "^2.1", - "lcobucci/jwt": "^3.1", - "paragonie/random_compat": "^2.0", + "lcobucci/jwt": "^3.2", "psr/http-message": "^1.0", - "defuse/php-encryption": "^2.1", - "zendframework/zend-diactoros": "^1.0" + "defuse/php-encryption": "^2.2", + "zendframework/zend-diactoros": "^2.0.0" }, "autoload": { "psr-4": { diff --git a/examples/composer.lock b/examples/composer.lock index 7210f31e..6ba8ec13 100644 --- a/examples/composer.lock +++ b/examples/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "9813ed7c3b6dcf107f44df9392935b8f", + "content-hash": "97f2878428e37d1d8e5418cc85cbfa3d", "packages": [ { "name": "container-interop/container-interop", @@ -39,21 +39,24 @@ }, { "name": "nikic/fast-route", - "version": "v0.6.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/nikic/FastRoute.git", - "reference": "31fa86924556b80735f98b294a7ffdfb26789f22" + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/FastRoute/zipball/31fa86924556b80735f98b294a7ffdfb26789f22", - "reference": "31fa86924556b80735f98b294a7ffdfb26789f22", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", "shasum": "" }, "require": { "php": ">=5.4.0" }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, "type": "library", "autoload": { "psr-4": { @@ -78,29 +81,33 @@ "router", "routing" ], - "time": "2015-06-18T19:15:47+00:00" + "time": "2018-02-13T20:26:39+00:00" }, { "name": "pimple/pimple", - "version": "v3.0.2", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/silexphp/Pimple.git", - "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a" + "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a30f7d6e57565a2e1a316e1baf2a483f788b258a", - "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32", + "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=5.3.0", + "psr/container": "^1.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "3.2.x-dev" } }, "autoload": { @@ -124,7 +131,7 @@ "container", "dependency injection" ], - "time": "2015-09-11T15:10:35+00:00" + "time": "2018-01-21T07:42:36+00:00" }, { "name": "psr/container", @@ -227,27 +234,32 @@ }, { "name": "slim/slim", - "version": "3.0.0", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/slimphp/Slim.git", - "reference": "3b06f0f2d84dabbe81b6cea46ace46a3e883253e" + "reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slimphp/Slim/zipball/3b06f0f2d84dabbe81b6cea46ace46a3e883253e", - "reference": "3b06f0f2d84dabbe81b6cea46ace46a3e883253e", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/d378e70431e78ee92ee32ddde61ecc72edf5dc0a", + "reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a", "shasum": "" }, "require": { - "container-interop/container-interop": "^1.1", - "nikic/fast-route": "^0.6", + "container-interop/container-interop": "^1.2", + "nikic/fast-route": "^1.0", "php": ">=5.5.0", "pimple/pimple": "^3.0", + "psr/container": "^1.0", "psr/http-message": "^1.0" }, + "provide": { + "psr/http-message-implementation": "1.0" + }, "require-dev": { - "phpunit/phpunit": "^4.0" + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^2.5" }, "type": "library", "autoload": { @@ -282,38 +294,38 @@ } ], "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", - "homepage": "http://slimframework.com", + "homepage": "https://slimframework.com", "keywords": [ "api", "framework", "micro", "router" ], - "time": "2015-12-07T14:11:09+00:00" + "time": "2018-09-16T10:54:21+00:00" } ], "packages-dev": [ { "name": "defuse/php-encryption", - "version": "v2.1.0", + "version": "v2.2.1", "source": { "type": "git", "url": "https://github.com/defuse/php-encryption.git", - "reference": "5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689" + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/defuse/php-encryption/zipball/5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689", - "reference": "5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/0f407c43b953d571421e0020ba92082ed5fb7620", + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620", "shasum": "" }, "require": { "ext-openssl": "*", - "paragonie/random_compat": "~2.0", + "paragonie/random_compat": ">= 2", "php": ">=5.4.0" }, "require-dev": { - "nikic/php-parser": "^2.0|^3.0", + "nikic/php-parser": "^2.0|^3.0|^4.0", "phpunit/phpunit": "^4|^5" }, "bin": [ @@ -354,20 +366,20 @@ "security", "symmetric key cryptography" ], - "time": "2017-05-18T21:28:48+00:00" + "time": "2018-07-24T23:27:56+00:00" }, { "name": "lcobucci/jwt", - "version": "3.2.1", + "version": "3.2.4", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "ddce703826f9c5229781933b1a39069e38e6a0f3" + "reference": "c9704b751315d21735dc98d78d4f37bd73596da7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/ddce703826f9c5229781933b1a39069e38e6a0f3", - "reference": "ddce703826f9c5229781933b1a39069e38e6a0f3", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/c9704b751315d21735dc98d78d4f37bd73596da7", + "reference": "c9704b751315d21735dc98d78d4f37bd73596da7", "shasum": "" }, "require": { @@ -412,7 +424,7 @@ "JWS", "jwt" ], - "time": "2016-10-31T20:09:32+00:00" + "time": "2018-08-03T11:23:50+00:00" }, { "name": "league/event", @@ -466,33 +478,29 @@ }, { "name": "paragonie/random_compat", - "version": "v2.0.10", + "version": "v9.99.99", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d" + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/634bae8e911eefa89c1abfbf1b66da679ac8f54d", - "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", "shasum": "" }, "require": { - "php": ">=5.2.0" + "php": "^7" }, "require-dev": { - "phpunit/phpunit": "4.*|5.*" + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" }, "suggest": { "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, "type": "library", - "autoload": { - "files": [ - "lib/random.php" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -507,10 +515,129 @@ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", "keywords": [ "csprng", + "polyfill", "pseudorandom", "random" ], - "time": "2017-03-13T16:27:32+00:00" + "time": "2018-07-02T15:55:56+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "378bfe27931ecc54ff824a20d6f6bfc303bbd04c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/378bfe27931ecc54ff824a20d6f6bfc303bbd04c", + "reference": "378bfe27931ecc54ff824a20d6f6bfc303bbd04c", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2018-07-30T21:54:04+00:00" + }, + { + "name": "zendframework/zend-diactoros", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-diactoros.git", + "reference": "0bae78192e634774b5584f0210c1232da82cb1ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/0bae78192e634774b5584f0210c1232da82cb1ff", + "reference": "0bae78192e634774b5584f0210c1232da82cb1ff", + "shasum": "" + }, + "require": { + "php": "^7.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-dom": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^0.5.0", + "php-http/psr7-integration-tests": "dev-master", + "phpunit/phpunit": "^7.0.2", + "zendframework/zend-coding-standard": "~1.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev", + "dev-develop": "2.1.x-dev", + "dev-release-1.8": "1.8.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/marshal_uri_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], + "psr-4": { + "Zend\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "keywords": [ + "http", + "psr", + "psr-7" + ], + "time": "2018-09-27T19:49:04+00:00" } ], "aliases": [], diff --git a/examples/src/Repositories/RefreshTokenRepository.php b/examples/src/Repositories/RefreshTokenRepository.php index 39a0b8cd..007529cd 100644 --- a/examples/src/Repositories/RefreshTokenRepository.php +++ b/examples/src/Repositories/RefreshTokenRepository.php @@ -18,7 +18,7 @@ class RefreshTokenRepository implements RefreshTokenRepositoryInterface /** * {@inheritdoc} */ - public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntityInterface) + public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity) { // Some logic to persist the refresh token in a database } diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php index 99f1626a..9b5486c3 100644 --- a/src/Grant/AbstractGrant.php +++ b/src/Grant/AbstractGrant.php @@ -174,7 +174,7 @@ abstract class AbstractGrant implements GrantTypeInterface list($basicAuthUser, $basicAuthPassword) = $this->getBasicAuthCredentials($request); $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser); - if (is_null($clientId)) { + if ($clientId === null) { throw OAuthServerException::invalidRequest('client_id'); } @@ -217,13 +217,13 @@ abstract class AbstractGrant implements GrantTypeInterface ClientEntityInterface $client, ServerRequestInterface $request ) { - if (is_string($client->getRedirectUri()) + if (\is_string($client->getRedirectUri()) && (strcmp($client->getRedirectUri(), $redirectUri) !== 0) ) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient(); - } elseif (is_array($client->getRedirectUri()) - && in_array($redirectUri, $client->getRedirectUri(), true) === false + } elseif (\is_array($client->getRedirectUri()) + && \in_array($redirectUri, $client->getRedirectUri(), true) === false ) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient(); @@ -233,8 +233,8 @@ abstract class AbstractGrant implements GrantTypeInterface /** * Validate scopes in the request. * - * @param string $scopes - * @param string $redirectUri + * @param string|array $scopes + * @param string $redirectUri * * @throws OAuthServerException * @@ -242,13 +242,13 @@ abstract class AbstractGrant implements GrantTypeInterface */ public function validateScopes($scopes, $redirectUri = null) { - $scopesList = array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), function ($scope) { - return !empty($scope); - }); + if (!\is_array($scopes)) { + $scopes = $this->convertScopesQueryStringToArray($scopes); + } $validScopes = []; - foreach ($scopesList as $scopeItem) { + foreach ($scopes as $scopeItem) { $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem); if ($scope instanceof ScopeEntityInterface === false) { @@ -261,6 +261,20 @@ abstract class AbstractGrant implements GrantTypeInterface return $validScopes; } + /** + * Converts a scopes query string to an array to easily iterate for validation. + * + * @param string $scopes + * + * @return array + */ + private function convertScopesQueryStringToArray($scopes) + { + return array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), function ($scope) { + return !empty($scope); + }); + } + /** * Retrieve request parameter. * @@ -274,7 +288,7 @@ abstract class AbstractGrant implements GrantTypeInterface { $requestParameters = (array) $request->getParsedBody(); - return isset($requestParameters[$parameter]) ? $requestParameters[$parameter] : $default; + return $requestParameters[$parameter] ?? $default; } /** diff --git a/src/Grant/AuthCodeGrant.php b/src/Grant/AuthCodeGrant.php index cfa8309b..db2dee47 100644 --- a/src/Grant/AuthCodeGrant.php +++ b/src/Grant/AuthCodeGrant.php @@ -10,7 +10,6 @@ namespace League\OAuth2\Server\Grant; use League\OAuth2\Server\Entities\ClientEntityInterface; -use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; @@ -37,6 +36,8 @@ class AuthCodeGrant extends AbstractAuthorizeGrant * @param AuthCodeRepositoryInterface $authCodeRepository * @param RefreshTokenRepositoryInterface $refreshTokenRepository * @param \DateInterval $authCodeTTL + * + * @throws \Exception */ public function __construct( AuthCodeRepositoryInterface $authCodeRepository, @@ -78,47 +79,13 @@ class AuthCodeGrant extends AbstractAuthorizeGrant throw OAuthServerException::invalidRequest('code'); } - // Validate the authorization code try { $authCodePayload = json_decode($this->decrypt($encryptedAuthCode)); - if (time() > $authCodePayload->expire_time) { - throw OAuthServerException::invalidRequest('code', 'Authorization code has expired'); - } - if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) { - throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked'); - } + $this->validateAuthorizationCode($authCodePayload, $client, $request); - if ($authCodePayload->client_id !== $client->getIdentifier()) { - throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client'); - } - - // The redirect URI is required in this request - $redirectUri = $this->getRequestParameter('redirect_uri', $request, null); - if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) { - throw OAuthServerException::invalidRequest('redirect_uri'); - } - - if ($authCodePayload->redirect_uri !== $redirectUri) { - throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI'); - } - - $scopes = []; - foreach ($authCodePayload->scopes as $scopeId) { - $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId); - - if ($scope instanceof ScopeEntityInterface === false) { - // @codeCoverageIgnoreStart - throw OAuthServerException::invalidScope($scopeId); - // @codeCoverageIgnoreEnd - } - - $scopes[] = $scope; - } - - // Finalize the requested scopes $scopes = $this->scopeRepository->finalizeScopes( - $scopes, + $this->validateScopes($authCodePayload->scopes), $this->getIdentifier(), $client, $authCodePayload->user_id @@ -130,6 +97,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant // Validate code challenge if ($this->enableCodeExchangeProof === true) { $codeVerifier = $this->getRequestParameter('code_verifier', $request, null); + if ($codeVerifier === null) { throw OAuthServerException::invalidRequest('code_verifier'); } @@ -190,6 +158,41 @@ class AuthCodeGrant extends AbstractAuthorizeGrant return $responseType; } + /** + * Validate the authorization code. + * + * @param \stdClass $authCodePayload + * @param ClientEntityInterface $client + * @param ServerRequestInterface $request + */ + private function validateAuthorizationCode( + $authCodePayload, + ClientEntityInterface $client, + ServerRequestInterface $request + ) { + if (time() > $authCodePayload->expire_time) { + throw OAuthServerException::invalidRequest('code', 'Authorization code has expired'); + } + + if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) { + throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked'); + } + + if ($authCodePayload->client_id !== $client->getIdentifier()) { + throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client'); + } + + // The redirect URI is required in this request + $redirectUri = $this->getRequestParameter('redirect_uri', $request, null); + if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) { + throw OAuthServerException::invalidRequest('redirect_uri'); + } + + if ($authCodePayload->redirect_uri !== $redirectUri) { + throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI'); + } + } + /** * Return the grant identifier that can be used in matching up requests. * @@ -223,7 +226,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant $this->getServerParameter('PHP_AUTH_USER', $request) ); - if (is_null($clientId)) { + if ($clientId === null) { throw OAuthServerException::invalidRequest('client_id'); } @@ -243,12 +246,12 @@ class AuthCodeGrant extends AbstractAuthorizeGrant if ($redirectUri !== null) { $this->validateRedirectUri($redirectUri, $client, $request); - } elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1 - || empty($client->getRedirectUri())) { + } elseif (empty($client->getRedirectUri()) || + (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient(); } else { - $redirectUri = is_array($client->getRedirectUri()) + $redirectUri = \is_array($client->getRedirectUri()) ? $client->getRedirectUri()[0] : $client->getRedirectUri(); } @@ -279,7 +282,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain'); - if (in_array($codeChallengeMethod, ['plain', 'S256'], true) === false) { + if (\in_array($codeChallengeMethod, ['plain', 'S256'], true) === false) { throw OAuthServerException::invalidRequest( 'code_challenge_method', 'Code challenge method must be `plain` or `S256`' @@ -311,11 +314,8 @@ class AuthCodeGrant extends AbstractAuthorizeGrant throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); } - $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null) - ? is_array($authorizationRequest->getClient()->getRedirectUri()) - ? $authorizationRequest->getClient()->getRedirectUri()[0] - : $authorizationRequest->getClient()->getRedirectUri() - : $authorizationRequest->getRedirectUri(); + $finalRedirectUri = $authorizationRequest->getRedirectUri() + ?? $this->getClientRedirectUri($authorizationRequest); // The user approved the client, redirect them back with an auth code if ($authorizationRequest->isAuthorizationApproved() === true) { @@ -367,4 +367,18 @@ class AuthCodeGrant extends AbstractAuthorizeGrant ) ); } + + /** + * Get the client redirect URI if not set in the request. + * + * @param AuthorizationRequest $authorizationRequest + * + * @return string + */ + private function getClientRedirectUri(AuthorizationRequest $authorizationRequest) + { + return \is_array($authorizationRequest->getClient()->getRedirectUri()) + ? $authorizationRequest->getClient()->getRedirectUri()[0] + : $authorizationRequest->getClient()->getRedirectUri(); + } }