Skip to content

Commit 93ace54

Browse files
authored
Merge pull request #1203 from exeba/rfc8252-compliance
Rfc8252 compliance
2 parents b1ca467 + 7826675 commit 93ace54

File tree

5 files changed

+215
-8
lines changed

5 files changed

+215
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
66

77
## [Unreleased]
88
### Added
9+
- The server will now validate redirect uris according to rfc8252 (PR #1203)
910
- Events emitted now include the refresh token and access token payloads (PR #1211)
1011

1112
### Fixed

src/Grant/AbstractGrant.php

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use League\OAuth2\Server\Entities\ScopeEntityInterface;
2525
use League\OAuth2\Server\Exception\OAuthServerException;
2626
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
27+
use League\OAuth2\Server\RedirectUriValidators\RedirectUriValidator;
2728
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
2829
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
2930
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
@@ -270,14 +271,8 @@ protected function validateRedirectUri(
270271
ClientEntityInterface $client,
271272
ServerRequestInterface $request
272273
) {
273-
if (\is_string($client->getRedirectUri())
274-
&& (\strcmp($client->getRedirectUri(), $redirectUri) !== 0)
275-
) {
276-
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
277-
throw OAuthServerException::invalidClient($request);
278-
} elseif (\is_array($client->getRedirectUri())
279-
&& \in_array($redirectUri, $client->getRedirectUri(), true) === false
280-
) {
274+
$validator = new RedirectUriValidator($client->getRedirectUri());
275+
if (!$validator->validateRedirectUri($redirectUri)) {
281276
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
282277
throw OAuthServerException::invalidClient($request);
283278
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
/**
3+
* @author Sebastiano Degan <[email protected]>
4+
* @copyright Copyright (c) Alex Bilbie
5+
* @license http://mit-license.org/
6+
*
7+
* @link https://github.com/thephpleague/oauth2-server
8+
*/
9+
10+
namespace League\OAuth2\Server\RedirectUriValidators;
11+
12+
class RedirectUriValidator implements RedirectUriValidatorInterface
13+
{
14+
/**
15+
* @var array
16+
*/
17+
private $allowedRedirectUris;
18+
19+
/**
20+
* New validator instance for the given uri
21+
*
22+
* @param string|array $allowedRedirectUris
23+
*/
24+
public function __construct($allowedRedirectUri)
25+
{
26+
if (\is_string($allowedRedirectUri)) {
27+
$this->allowedRedirectUris = [$allowedRedirectUri];
28+
} elseif (\is_array($allowedRedirectUri)) {
29+
$this->allowedRedirectUris = $allowedRedirectUri;
30+
} else {
31+
$this->allowedRedirectUris = [];
32+
}
33+
}
34+
35+
/**
36+
* Validates the redirect uri.
37+
*
38+
* @param string $redirectUri
39+
*
40+
* @return bool Return true if valid, false otherwise
41+
*/
42+
public function validateRedirectUri($redirectUri)
43+
{
44+
if ($this->isLoopbackUri($redirectUri)) {
45+
return $this->matchUriExcludingPort($redirectUri);
46+
}
47+
48+
return $this->matchExactUri($redirectUri);
49+
}
50+
51+
/**
52+
* According to section 7.3 of rfc8252, loopback uris are:
53+
* - "http://127.0.0.1:{port}/{path}" for IPv4
54+
* - "http://[::1]:{port}/{path}" for IPv6
55+
*
56+
* @param string $redirectUri
57+
*
58+
* @return bool
59+
*/
60+
private function isLoopbackUri($redirectUri)
61+
{
62+
$parsedUrl = \parse_url($redirectUri);
63+
64+
return $parsedUrl['scheme'] === 'http'
65+
&& (\in_array($parsedUrl['host'], ['127.0.0.1', '[::1]'], true));
66+
}
67+
68+
/**
69+
* Find an exact match among allowed uris
70+
*
71+
* @param string $redirectUri
72+
*
73+
* @return bool Return true if an exact match is found, false otherwise
74+
*/
75+
private function matchExactUri($redirectUri)
76+
{
77+
return \in_array($redirectUri, $this->allowedRedirectUris, true);
78+
}
79+
80+
/**
81+
* Find a match among allowed uris, allowing for different port numbers
82+
*
83+
* @param string $redirectUri
84+
*
85+
* @return bool Return true if a match is found, false otherwise
86+
*/
87+
private function matchUriExcludingPort($redirectUri)
88+
{
89+
$parsedUrl = $this->parseUrlAndRemovePort($redirectUri);
90+
91+
foreach ($this->allowedRedirectUris as $allowedRedirectUri) {
92+
if ($parsedUrl === $this->parseUrlAndRemovePort($allowedRedirectUri)) {
93+
return true;
94+
}
95+
}
96+
97+
return false;
98+
}
99+
100+
/**
101+
* Parse an url like \parse_url, excluding the port
102+
*
103+
* @param string $url
104+
*
105+
* @return array
106+
*/
107+
private function parseUrlAndRemovePort($url)
108+
{
109+
$parsedUrl = \parse_url($url);
110+
unset($parsedUrl['port']);
111+
112+
return $parsedUrl;
113+
}
114+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
/**
3+
* @author Sebastiano Degan <[email protected]>
4+
* @copyright Copyright (c) Alex Bilbie
5+
* @license http://mit-license.org/
6+
*
7+
* @link https://github.com/thephpleague/oauth2-server
8+
*/
9+
10+
namespace League\OAuth2\Server\RedirectUriValidators;
11+
12+
interface RedirectUriValidatorInterface
13+
{
14+
/**
15+
* Validates the redirect uri.
16+
*
17+
* @param string $redirectUri
18+
*
19+
* @return bool Return true if valid, false otherwise
20+
*/
21+
public function validateRedirectUri($redirectUri);
22+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace LeagueTests\RedirectUriValidators;
4+
5+
use League\OAuth2\Server\RedirectUriValidators\RedirectUriValidator;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class RedirectUriValidatorTest extends TestCase
9+
{
10+
public function testInvalidNonLoopbackUri()
11+
{
12+
$validator = new RedirectUriValidator([
13+
'https://example.com:8443/endpoint',
14+
'https://example.com/different/endpoint',
15+
]);
16+
17+
$invalidRedirectUri = 'https://example.com/endpoint';
18+
19+
$this->assertFalse(
20+
$validator->validateRedirectUri($invalidRedirectUri),
21+
'Non loopback URI must match in every part'
22+
);
23+
}
24+
25+
public function testValidNonLoopbackUri()
26+
{
27+
$validator = new RedirectUriValidator([
28+
'https://example.com:8443/endpoint',
29+
'https://example.com/different/endpoint',
30+
]);
31+
32+
$validRedirectUri = 'https://example.com:8443/endpoint';
33+
34+
$this->assertTrue(
35+
$validator->validateRedirectUri($validRedirectUri),
36+
'Redirect URI must be valid when matching in every part'
37+
);
38+
}
39+
40+
public function testInvalidLoopbackUri()
41+
{
42+
$validator = new RedirectUriValidator('http://127.0.0.1:8443/endpoint');
43+
44+
$invalidRedirectUri = 'http://127.0.0.1:8443/different/endpoint';
45+
46+
$this->assertFalse(
47+
$validator->validateRedirectUri($invalidRedirectUri),
48+
'Valid loopback redirect URI can change only the port number'
49+
);
50+
}
51+
52+
public function testValidLoopbackUri()
53+
{
54+
$validator = new RedirectUriValidator('http://127.0.0.1:8443/endpoint');
55+
56+
$validRedirectUri = 'http://127.0.0.1:8080/endpoint';
57+
58+
$this->assertTrue(
59+
$validator->validateRedirectUri($validRedirectUri),
60+
'Loopback redirect URI can change the port number'
61+
);
62+
}
63+
64+
public function testValidIpv6LoopbackUri()
65+
{
66+
$validator = new RedirectUriValidator('http://[::1]:8443/endpoint');
67+
68+
$validRedirectUri = 'http://[::1]:8080/endpoint';
69+
70+
$this->assertTrue(
71+
$validator->validateRedirectUri($validRedirectUri),
72+
'Loopback redirect URI can change the port number'
73+
);
74+
}
75+
}

0 commit comments

Comments
 (0)