Skip to content

Commit e5ecd56

Browse files
pauldekkersrtpgauvipyRaphael Gaschignard
authored
Allow loopback redirect URIs using ports as described in RFC8252 (#953)
* Allow loopback redirect URIs using ports as described in RFC8252 * Update Changelog and Authors * Docs update and adjustment for explicit port config on loopback * Wrap and clarify Changelog * Clarify documentation * Split out redirect uri logic for easier testing This adds some unit tests for loopback IP code in particular, as part of reviewing the change Co-authored-by: Raphael Gaschignard <[email protected]> Co-authored-by: Asif Saif Uddin <[email protected]> Co-authored-by: Raphael Gaschignard <[email protected]>
1 parent b4f418b commit e5ecd56

File tree

5 files changed

+77
-17
lines changed

5 files changed

+77
-17
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Jonas Nygaard Pedersen
3636
Jonathan Steffan
3737
Jun Zhou
3838
Kristian Rune Larsen
39+
Paul Dekkers
3940
Paul Oswald
4041
Pavel Tvrdík
4142
Rodney Richardson

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
* #524 Restrict usage of timezone aware expire dates to Django projects with USE_TZ set to True.
2525
* #955 Avoid doubling of `oauth2_provider` urls mountpath in json response for OIDC view `ConnectDiscoveryInfoView`.
2626
Breaks existing OIDC discovery output
27+
* #953 Allow loopback redirect URIs with random ports using http scheme, localhost address and no explicit port
28+
configuration in the allowed redirect_uris for Oauth2 Applications (RFC8252)
2729

2830
## [1.5.0] 2021-03-18
2931

docs/settings.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ Default: ``["http", "https"]``
5252
A list of schemes that the ``redirect_uri`` field will be validated against.
5353
Setting this to ``["https"]`` only in production is strongly recommended.
5454

55+
For Native Apps the ``http`` scheme can be safely used with loopback addresses in the
56+
Application (``[::1]`` or ``127.0.0.1``). In this case the ``redirect_uri`` can be
57+
configured without explicit port specification, so that the Application accepts randomly
58+
assigned ports.
59+
5560
Note that you may override ``Application.get_allowed_schemes()`` to set this on
5661
a per-application basis.
5762

oauth2_provider/models.py

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -125,23 +125,7 @@ def redirect_uri_allowed(self, uri):
125125
126126
:param uri: Url to check
127127
"""
128-
parsed_uri = urlparse(uri)
129-
uqs_set = set(parse_qsl(parsed_uri.query))
130-
for allowed_uri in self.redirect_uris.split():
131-
parsed_allowed_uri = urlparse(allowed_uri)
132-
133-
if (
134-
parsed_allowed_uri.scheme == parsed_uri.scheme
135-
and parsed_allowed_uri.netloc == parsed_uri.netloc
136-
and parsed_allowed_uri.path == parsed_uri.path
137-
):
138-
139-
aqs_set = set(parse_qsl(parsed_allowed_uri.query))
140-
141-
if aqs_set.issubset(uqs_set):
142-
return True
143-
144-
return False
128+
return redirect_to_uri_allowed(uri, self.redirect_uris.split())
145129

146130
def clean(self):
147131
from django.core.exceptions import ValidationError
@@ -674,3 +658,50 @@ def clear_expired():
674658

675659
access_tokens.delete()
676660
grants.delete()
661+
662+
663+
def redirect_to_uri_allowed(uri, allowed_uris):
664+
"""
665+
Checks if a given uri can be redirected to based on the provided allowed_uris configuration.
666+
667+
On top of exact matches, this function also handles loopback IPs based on RFC 8252.
668+
669+
:param uri: URI to check
670+
:param allowed_uris: A list of URIs that are allowed
671+
"""
672+
673+
parsed_uri = urlparse(uri)
674+
uqs_set = set(parse_qsl(parsed_uri.query))
675+
for allowed_uri in allowed_uris:
676+
parsed_allowed_uri = urlparse(allowed_uri)
677+
678+
# From RFC 8252 (Section 7.3)
679+
#
680+
# Loopback redirect URIs use the "http" scheme
681+
# [...]
682+
# The authorization server MUST allow any port to be specified at the
683+
# time of the request for loopback IP redirect URIs, to accommodate
684+
# clients that obtain an available ephemeral port from the operating
685+
# system at the time of the request.
686+
687+
allowed_uri_is_loopback = (
688+
parsed_allowed_uri.scheme == "http"
689+
and parsed_allowed_uri.hostname in ["127.0.0.1", "::1"]
690+
and parsed_allowed_uri.port is None
691+
)
692+
if (
693+
allowed_uri_is_loopback
694+
and parsed_allowed_uri.scheme == parsed_uri.scheme
695+
and parsed_allowed_uri.hostname == parsed_uri.hostname
696+
and parsed_allowed_uri.path == parsed_uri.path
697+
) or (
698+
parsed_allowed_uri.scheme == parsed_uri.scheme
699+
and parsed_allowed_uri.netloc == parsed_uri.netloc
700+
and parsed_allowed_uri.path == parsed_uri.path
701+
):
702+
703+
aqs_set = set(parse_qsl(parsed_allowed_uri.query))
704+
if aqs_set.issubset(uqs_set):
705+
return True
706+
707+
return False

tests/test_oauth2_backends.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.test import RequestFactory, TestCase
55

66
from oauth2_provider.backends import get_oauthlib_core
7+
from oauth2_provider.models import redirect_to_uri_allowed
78
from oauth2_provider.oauth2_backends import JSONOAuthLibCore, OAuthLibCore
89

910

@@ -110,3 +111,23 @@ def test_validate_authorization_request_unsafe_query(self):
110111

111112
oauthlib_core = get_oauthlib_core()
112113
oauthlib_core.verify_request(request, scopes=[])
114+
115+
116+
@pytest.mark.parametrize(
117+
"uri, expected_result",
118+
# localhost is _not_ a loopback URI
119+
[
120+
("http://localhost:3456", False),
121+
# only http scheme is supported for loopback URIs
122+
("https://127.0.0.1:3456", False),
123+
("http://127.0.0.1:3456", True),
124+
("http://[::1]", True),
125+
("http://[::1]:34", True),
126+
],
127+
)
128+
def test_uri_loopback_redirect_check(uri, expected_result):
129+
allowed_uris = ["http://127.0.0.1", "http://[::1]"]
130+
if expected_result:
131+
assert redirect_to_uri_allowed(uri, allowed_uris)
132+
else:
133+
assert not redirect_to_uri_allowed(uri, allowed_uris)

0 commit comments

Comments
 (0)