Skip to content

Commit

Permalink
Added ALLOWED_SCHEMES setting for Allowed Orgins validation
Browse files Browse the repository at this point in the history
  • Loading branch information
akanstantsinau authored and dopry committed Oct 19, 2023
1 parent 0fc16f7 commit 94213ea
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 4 deletions.
11 changes: 11 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ assigned ports.
Note that you may override ``Application.get_allowed_schemes()`` to set this on
a per-application basis.

ALLOWED_SCHEMES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Default: ``["https"]``

A list of schemes that the ``allowed_origins`` field will be validated against.
Setting this to ``["https"]`` only in production is strongly recommended.
Adding ``"http"`` to the list is considered to be safe only for local development and testing.
Note that `OAUTHLIB_INSECURE_TRANSPORT <https://oauthlib.readthedocs.io/en/latest/oauth2/security.html#envvar-OAUTHLIB_INSECURE_TRANSPORT>`_
environment variable should be also set to allow http origins.


APPLICATION_MODEL
~~~~~~~~~~~~~~~~~
Expand Down
9 changes: 6 additions & 3 deletions oauth2_provider/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
from .scopes import get_scopes_backend
from .settings import oauth2_settings
from .utils import jwk_from_pem
from .validators import RedirectURIValidator, URIValidator, WildcardSet

from .validators import RedirectURIValidator, URIValidator, WildcardSet, AllowedURIValidator

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -218,7 +217,7 @@ def clean(self):
allowed_origins = self.allowed_origins.strip().split()
if allowed_origins:
# oauthlib allows only https scheme for CORS
validator = URIValidator({"https"})
validator = AllowedURIValidator(oauth2_settings.ALLOWED_SCHEMES, "Origin")
for uri in allowed_origins:
validator(uri)

Expand Down Expand Up @@ -808,6 +807,10 @@ def is_origin_allowed(origin, allowed_origins):
"""

parsed_origin = urlparse(origin)

if parsed_origin.scheme not in oauth2_settings.ALLOWED_SCHEMES:
return False

for allowed_origin in allowed_origins:
parsed_allowed_origin = urlparse(allowed_origin)
if (
Expand Down
1 change: 1 addition & 0 deletions oauth2_provider/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"REFRESH_TOKEN_ADMIN_CLASS": "oauth2_provider.admin.RefreshTokenAdmin",
"REQUEST_APPROVAL_PROMPT": "force",
"ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https"],
"ALLOWED_SCHEMES": ["https"],
"OIDC_ENABLED": False,
"OIDC_ISS_ENDPOINT": "",
"OIDC_USERINFO_ENDPOINT": "",
Expand Down
26 changes: 26 additions & 0 deletions oauth2_provider/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,32 @@ def __call__(self, value):
raise ValidationError("Redirect URIs must not contain fragments")


class AllowedURIValidator(URIValidator):
def __init__(self, schemes, name, allow_path=False, allow_query=False, allow_fragments=False):
"""
:params schemes: List of allowed schemes. E.g.: ["https"]
:params name: Name of the validater URI required for validation message. E.g.: "Origin"
:params allow_path: If URI can contain path part
:params allow_query: If URI can contain query part
:params allow_fragments: If URI can contain fragments part
"""
super().__init__(schemes=schemes)
self.name = name
self.allow_path = allow_path
self.allow_query = allow_query
self.allow_fragments = allow_fragments

def __call__(self, value):
super().__call__(value)
value = force_str(value)
scheme, netloc, path, query, fragment = urlsplit(value)
if path and not self.allow_path:
raise ValidationError("{} URIs must not contain path".format(self.name))
if query and not self.allow_query:
raise ValidationError("{} URIs must not contain query".format(self.name))
if fragment and not self.allow_fragments:
raise ValidationError("{} URIs must not contain fragments".format(self.name))

##
# WildcardSet is a special set that contains everything.
# This is required in order to move validation of the scheme from
Expand Down
71 changes: 70 additions & 1 deletion tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.core.validators import ValidationError
from django.test import TestCase

from oauth2_provider.validators import RedirectURIValidator
from oauth2_provider.validators import RedirectURIValidator, AllowedURIValidator


@pytest.mark.usefixtures("oauth2_settings")
Expand Down Expand Up @@ -36,6 +36,11 @@ def test_validate_custom_uri_scheme(self):
# Check ValidationError not thrown
validator(uri)

validator = AllowedURIValidator(["my-scheme", "https", "git+ssh"], "Origin")
for uri in good_uris:
# Check ValidationError not thrown
validator(uri)

def test_validate_bad_uris(self):
validator = RedirectURIValidator(allowed_schemes=["https"])
self.oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ["https", "good"]
Expand All @@ -61,3 +66,67 @@ def test_validate_bad_uris(self):
for uri in bad_uris:
with self.assertRaises(ValidationError):
validator(uri)

def test_validate_good_origin_uris(self):
"""
Test AllowedURIValidator validates origin URIs if they match requirements
"""
validator = AllowedURIValidator(
["https"],
"Origin",
allow_path=False,
allow_query=False,
allow_fragments=False,
)
good_uris = [
"https://example.com",
"https://example.com:8080",
"https://example",
"https://localhost",
"https://1.1.1.1",
"https://127.0.0.1",
"https://255.255.255.255",
]
for uri in good_uris:
# Check ValidationError not thrown
validator(uri)

def test_validate_bad_origin_uris(self):
"""
Test AllowedURIValidator rejects origin URIs if they do not match requirements
"""
validator = AllowedURIValidator(
["https"],
"Origin",
allow_path=False,
allow_query=False,
allow_fragments=False,
)
bad_uris = [
"http:/example.com",
"HTTP://localhost",
"HTTP://example.com",
"HTTP://example.com.",
"http://example.com/#fragment",
"123://example.com",
"http://fe80::1",
"git+ssh://example.com",
"my-scheme://example.com",
"uri-without-a-scheme",
"https://example.com/#fragment",
"good://example.com/#fragment",
" ",
"",
# Bad IPv6 URL, urlparse behaves differently for these
'https://["><script>alert()</script>',
# Origin uri should not contain path, query of fragment parts
# https://www.rfc-editor.org/rfc/rfc6454#section-7.1
"https:/example.com/",
"https:/example.com/test",
"https:/example.com/?q=test",
"https:/example.com/#test",
]

for uri in bad_uris:
with self.assertRaises(ValidationError):
validator(uri)

0 comments on commit 94213ea

Please sign in to comment.