allowing different ports for loopback redirect uris
MODIFY
src/Grant/AbstractGrant.php
src/Grant/AbstractGrant.php
@@ -24,6 +24,7 @@ use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
+use League\OAuth2\Server\RedirectUriValidators\Rfc8252RedirectUriValidator;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
@@ -262,15 +263,8 @@ abstract class AbstractGrant implements GrantTypeInterface
ClientEntityInterface $client,
ServerRequestInterface $request
) {
- if (\is_string($client->getRedirectUri())
- && (\strcmp($client->getRedirectUri(), $redirectUri) !== 0)
- ) {
- $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
- throw OAuthServerException::invalidClient($request);
- } elseif (\is_array($client->getRedirectUri())
- && \in_array($redirectUri, $client->getRedirectUri(), true) === false
- ) {
- $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
+ $validator = new Rfc8252RedirectUriValidator($client);
+ if (!$validator->validateRedirectUri($redirectUri)) {
throw OAuthServerException::invalidClient($request);
}
}

ADD
src/RedirectUriValidators/Rfc8252RedirectUriValidator.php
+<?php
+
+
+namespace League\OAuth2\Server\RedirectUriValidators;
+
+
+use League\OAuth2\Server\Entities\ClientEntityInterface;
+
+class Rfc8252RedirectUriValidator
+{
+
+ private $client;
+
+ public function __construct(ClientEntityInterface $client) {
+ $this->client = $client;
+ }
+
+ public function validateRedirectUri(string $redirectUri) {
+ $parsedUrl = $this->parseUrl($redirectUri);
+ if ($this->isLoopbackAddress($parsedUrl)) {
+ return $this->allowDifferentPort($parsedUrl);
+ } else {
+ return $this->matchFullUri($redirectUri);
+ }
+ }
+
+ private function isLoopbackAddress(array $parsedUri) {
+ return $parsedUri['scheme'] === 'http'
+ && (\in_array($parsedUri['host'], ['127.0.0.1', '[::1]'], true));
+ }
+
+ private function matchFullUri(string $redirectUri) {
+ return \in_array($redirectUri, $this->getClientRedirectUris(), true);
+ }
+
+ private function allowDifferentPort(array $parsedUrl) {
+ foreach ($this->getClientRedirectUris() as $clientRedirectUri) {
+ if ($parsedUrl == $this->parseUrl($clientRedirectUri)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private function parseUrl(string $url) {
+ $parsedUrl = parse_url($url);
+ $parsedUrl['port'] = 80;
+
+ return $parsedUrl;
+ }
+
+ private function getClientRedirectUris() {
+ $clientRedirectUri = $this->client->getRedirectUri();
+ if (\is_string($clientRedirectUri)) {
+ return [ $clientRedirectUri ];
+ } else if (\is_array($clientRedirectUri)) {
+ return $clientRedirectUri;
+ } else {
+ return [];
+ }
+ }
+
+}

ADD
tests/RedirectUriValidators/Rfc8252RedirectUriValidatorTest.php
+<?php
+
+
+namespace LeagueTests\RedirectUriValidators;
+
+
+use League\OAuth2\Server\RedirectUriValidators\Rfc8252RedirectUriValidator;
+use LeagueTests\Stubs\ClientEntity;
+use PHPUnit\Framework\TestCase;
+
+class Rfc8252RedirectUriValidatorTest extends TestCase
+{
+ public function testInvalidNonLoopbackUri()
+ {
+ $client = new ClientEntity();
+ $client->setRedirectUri([
+ 'https://example.com:8443/endpoint',
+ 'https://example.com/different/endpoint',
+ ]);
+ $redirectUri = 'https://example.com/endpoint';
+
+ $validator = new Rfc8252RedirectUriValidator($client);
+
+ $this->assertFalse($validator->validateRedirectUri($redirectUri),
+ 'Non loopback URI must match in every part');
+ }
+
+ public function testValidNonLoopbackUri()
+ {
+ $client = new ClientEntity();
+ $client->setRedirectUri([
+ 'https://example.com:8443/endpoint',
+ 'https://example.com/different/endpoint',
+ ]);
+ $redirectUri = 'https://example.com:8443/endpoint';
+
+ $validator = new Rfc8252RedirectUriValidator($client);
+
+ $this->assertTrue($validator->validateRedirectUri($redirectUri),
+ 'Redirect URI must be valid when matching in every part');
+ }
+
+ public function testInvalidLoopbackUri()
+ {
+ $client = new ClientEntity();
+ $client->setRedirectUri('http://127.0.0.1:8443/endpoint');
+
+ $redirectUri = 'http://127.0.0.1:8443/different/endpoint';
+
+ $validator = new Rfc8252RedirectUriValidator($client);
+
+ $this->assertFalse($validator->validateRedirectUri($redirectUri),
+ 'Valid loopback redirect URI can change only the port number');
+ }
+
+ public function testValidLoopbackUri()
+ {
+ $client = new ClientEntity();
+ $client->setRedirectUri('http://127.0.0.1:8443/endpoint');
+
+ $redirectUri = 'http://127.0.0.1:8080/endpoint';
+
+ $validator = new Rfc8252RedirectUriValidator($client);
+
+ $this->assertTrue($validator->validateRedirectUri($redirectUri),
+ 'Loopback redirect URI can change the port number');
+ }
+}

allowing different ports for loopback redirect uris