Skip to content

Commit 9653887

Browse files
committed
Move scheme validation to OAuth2Application.clean()
1 parent 39a2a4e commit 9653887

File tree

5 files changed

+38
-31
lines changed

5 files changed

+38
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* **Compatibility**: Django 2.0 is the new minimum required version.
55
* **New feature**: Added TokenMatchesOASRequirements Permissions.
66
* validators.URIValidator has been updated to match URLValidator behaviour more closely.
7+
* Moved `redirect_uris` validation to the application clean() method.
78

89

910
### 1.1.2 [2018-05-12]

oauth2_provider/migrations/0001_initial.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
1818
fields=[
1919
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
2020
('client_id', models.CharField(default=oauth2_provider.generators.generate_client_id, unique=True, max_length=100, db_index=True)),
21-
('redirect_uris', models.TextField(help_text='Allowed URIs list, space separated', blank=True, validators=[oauth2_provider.validators.validate_uris])),
21+
('redirect_uris', models.TextField(help_text='Allowed URIs list, space separated', blank=True)),
2222
('client_type', models.CharField(max_length=32, choices=[('confidential', 'Confidential'), ('public', 'Public')])),
2323
('authorization_grant_type', models.CharField(max_length=32, choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials')])),
2424
('client_secret', models.CharField(default=oauth2_provider.generators.generate_client_secret, max_length=255, db_index=True, blank=True)),

oauth2_provider/models.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .generators import generate_client_id, generate_client_secret
1313
from .scopes import get_scopes_backend
1414
from .settings import oauth2_settings
15-
from .validators import validate_uris
15+
from .validators import RedirectURIValidator, WildcardSet
1616

1717

1818
class AbstractApplication(models.Model):
@@ -65,7 +65,6 @@ class AbstractApplication(models.Model):
6565

6666
redirect_uris = models.TextField(
6767
blank=True, help_text=_("Allowed URIs list, space separated"),
68-
validators=[validate_uris]
6968
)
7069
client_type = models.CharField(max_length=32, choices=CLIENT_TYPES)
7170
authorization_grant_type = models.CharField(
@@ -125,12 +124,29 @@ def redirect_uri_allowed(self, uri):
125124

126125
def clean(self):
127126
from django.core.exceptions import ValidationError
128-
if not self.redirect_uris \
129-
and self.authorization_grant_type \
130-
in (AbstractApplication.GRANT_AUTHORIZATION_CODE,
131-
AbstractApplication.GRANT_IMPLICIT):
132-
error = _("Redirect_uris could not be empty with {grant_type} grant_type")
133-
raise ValidationError(error.format(grant_type=self.authorization_grant_type))
127+
128+
grant_types = (
129+
AbstractApplication.GRANT_AUTHORIZATION_CODE,
130+
AbstractApplication.GRANT_IMPLICIT,
131+
)
132+
133+
redirect_uris = self.redirect_uris.strip().split()
134+
allowed_schemes = set(s.lower() for s in self.get_allowed_schemes())
135+
136+
if redirect_uris:
137+
validator = RedirectURIValidator(WildcardSet())
138+
for uri in redirect_uris:
139+
validator(uri)
140+
scheme = urlparse(uri).scheme
141+
if scheme not in allowed_schemes:
142+
raise ValidationError(_(
143+
"Unauthorized redirect scheme: {scheme}"
144+
).format(scheme=scheme))
145+
146+
elif self.authorization_grant_type in grant_types:
147+
raise ValidationError(_(
148+
"redirect_uris cannot be empty with grant_type {grant_type}"
149+
).format(grant_type=self.authorization_grant_type))
134150

135151
def get_absolute_url(self):
136152
return reverse("oauth2_provider:detail", args=[str(self.id)])

oauth2_provider/validators.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
import re
2-
from urllib.parse import urlsplit, urlunsplit
2+
from urllib.parse import urlsplit
33

44
from django.core.exceptions import ValidationError
55
from django.core.validators import URLValidator
66
from django.utils.encoding import force_text
7-
from django.utils.translation import ugettext_lazy as _
8-
9-
from .settings import oauth2_settings
107

118

129
class URIValidator(URLValidator):
1310
scheme_re = r"^(?:[a-z][a-z0-9\.\-\+]*)://"
1411

1512
dotless_domain_re = r"(?!-)[A-Z\d-]{1,63}(?<!-)"
1613
host_re = "|".join((
17-
r"(?:"+ URLValidator.host_re,
14+
r"(?:" + URLValidator.host_re,
1815
URLValidator.ipv4_re,
1916
URLValidator.ipv6_re,
2017
dotless_domain_re + ")"
@@ -35,17 +32,16 @@ def __call__(self, value):
3532
scheme, netloc, path, query, fragment = urlsplit(value)
3633
if fragment and not self.allow_fragments:
3734
raise ValidationError("Redirect URIs must not contain fragments")
38-
if scheme.lower() not in self.schemes:
39-
raise ValidationError("Redirect URI scheme is not allowed.")
4035

4136

42-
def validate_uris(value):
37+
##
38+
# WildcardSet is a special set that contains everything.
39+
# This is required in order to move validation of the scheme from
40+
# URLValidator (the base class of URIValidator), to OAuth2Application.clean().
41+
42+
class WildcardSet(set):
4343
"""
44-
This validator ensures that `value` contains valid blank-separated URIs"
44+
A set that always returns True on `in`.
4545
"""
46-
v = RedirectURIValidator(oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES)
47-
uris = value.split()
48-
if not uris:
49-
raise ValidationError("Redirect URI cannot be empty")
50-
for uri in uris:
51-
v(uri)
46+
def __contains__(self, item):
47+
return True

tests/test_validators.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.test import TestCase
33

44
from oauth2_provider.settings import oauth2_settings
5-
from oauth2_provider.validators import RedirectURIValidator, validate_uris
5+
from oauth2_provider.validators import RedirectURIValidator
66

77

88
class TestValidators(TestCase):
@@ -35,12 +35,6 @@ def test_validate_custom_uri_scheme(self):
3535
# Check ValidationError not thrown
3636
validator(uri)
3737

38-
def test_validate_whitespace_separators(self):
39-
# Check that whitespace can be used as a separator
40-
good_uris = "https://example.com\r\nhttps://example.com\thttps://example.com"
41-
# Check ValidationError not thrown
42-
validate_uris(good_uris)
43-
4438
def test_validate_bad_uris(self):
4539
validator = RedirectURIValidator(allowed_schemes=["https"])
4640
oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ["https", "good"]

0 commit comments

Comments
 (0)