Skip to content

Commit ca88f5e

Browse files
committed
Allow wildcard for redirect_urls in OAuth2 apps
1 parent 0db5143 commit ca88f5e

File tree

12 files changed

+276
-83
lines changed

12 files changed

+276
-83
lines changed

.github/workflows/pip_audit.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ jobs:
2929
run: |
3030
python -m venv env/
3131
source env/bin/activate
32+
python -m pip install --upgrade pip
3233
python -m pip install . -rrequirements.txt
3334
# See: https://github.com/advisories/GHSA-r9hx-vwmv-q579
3435
pip install --upgrade setuptools

.github/workflows/pip_compile.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
python-version: 3.9
2828
- name: install
2929
run: |
30+
pip install --upgrade pip
3031
pip install pip-tools
3132
pip-compile --quiet requirements.in
3233
pip-compile --quiet requirements-dev.in

ansible_wisdom/main/settings/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"""
1212

1313
import os
14+
import sys
1415
from pathlib import Path
1516

1617
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -170,6 +171,15 @@
170171
'REFRESH_TOKEN_EXPIRE_SECONDS': 1_209_600, # = 2 weeks
171172
}
172173

174+
#
175+
# We need to run 'manage.py migrate' before adding our own OAuth2 application model.
176+
# See https://django-oauth-toolkit.readthedocs.io/en/latest/advanced_topics.html
177+
# #extending-the-application-model
178+
#
179+
if sys.argv[1:2] not in [['migrate'], ['test']]:
180+
INSTALLED_APPS.append('wildcard_oauth2')
181+
OAUTH2_PROVIDER_APPLICATION_MODEL = 'wildcard_oauth2.Application'
182+
173183
# OAUTH: todo
174184
# - remove ansible_wisdom/users/auth.py module
175185
# - remove ansible_wisdom/users/views.py module
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
The module offers a custom OAuth2 Application that allows wildcard URLs.
3+
(From: https://github.com/open-craft/oauth2-wildcard-application)
4+
"""
5+
6+
7+
default_app_config = (
8+
'wildcard_oauth2.apps.WildcardOauth2ApplicationConfig' # pylint: disable=invalid-name
9+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""
2+
Django App Configuration
3+
"""
4+
5+
from django.apps import AppConfig
6+
7+
8+
class WildcardOauth2ApplicationConfig(AppConfig):
9+
"""
10+
Configures wildcard_oauth2 as a Django app plugin
11+
"""
12+
13+
name = 'wildcard_oauth2'
14+
verbose_name = "Wildcard OAuth2 Application"
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
A custom OAuth2 application that allows wildcard for redirect_uris
3+
4+
https://github.com/jazzband/django-oauth-toolkit/issues/443#issuecomment-420255286
5+
"""
6+
import re
7+
from urllib.parse import parse_qsl, urlparse
8+
9+
from django.core.exceptions import ValidationError
10+
from oauth2_provider.models import AbstractApplication
11+
from oauth2_provider.settings import oauth2_settings
12+
13+
14+
def validate_uris(value):
15+
"""Ensure that `value` contains valid blank-separated URIs."""
16+
urls = value.split()
17+
for url in urls:
18+
obj = urlparse(url)
19+
if obj.fragment:
20+
raise ValidationError('Redirect URIs must not contain fragments')
21+
if obj.scheme.lower() not in oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES:
22+
raise ValidationError('Redirect URI scheme is not allowed.')
23+
if not obj.netloc:
24+
raise ValidationError('Redirect URI must contain a domain.')
25+
26+
27+
class Application(AbstractApplication):
28+
"""Subclass of application to allow for regular expressions for the redirect uri."""
29+
30+
@staticmethod
31+
def _uri_is_allowed(allowed_uri, uri):
32+
"""Check that the URI conforms to these rules."""
33+
schemes_match = allowed_uri.scheme == uri.scheme
34+
netloc_matches_pattern = re.fullmatch(allowed_uri.netloc, uri.netloc)
35+
paths_match = allowed_uri.path == uri.path
36+
37+
return all([schemes_match, netloc_matches_pattern, paths_match])
38+
39+
def __init__(self, *args, **kwargs):
40+
"""Relax the validator to allow for uris with regular expressions."""
41+
self._meta.get_field('redirect_uris').validators = [
42+
validate_uris,
43+
]
44+
super().__init__(*args, **kwargs)
45+
46+
def redirect_uri_allowed(self, uri):
47+
"""
48+
Check if given url is one of the items in :attr:`redirect_uris` string.
49+
A Redirect uri domain may be a regular expression e.g. `^(.*).example.com$` will
50+
match all subdomains of example.com.
51+
A Redirect uri may be `https://(.*).example.com/some/path/?q=x`
52+
:param uri: Url to check
53+
"""
54+
for allowed_uri in self.redirect_uris.split():
55+
parsed_allowed_uri = urlparse(allowed_uri)
56+
parsed_uri = urlparse(uri)
57+
58+
if self._uri_is_allowed(parsed_allowed_uri, parsed_uri):
59+
aqs_set = set(parse_qsl(parsed_allowed_uri.query))
60+
uqs_set = set(parse_qsl(parsed_uri.query))
61+
62+
if aqs_set.issubset(uqs_set):
63+
return True
64+
65+
return False
66+
67+
def clean(self):
68+
uris_with_wildcard = [uri for uri in self.redirect_uris.split(' ') if '*' in uri]
69+
if uris_with_wildcard:
70+
self.redirect_uris = ' '.join(
71+
[uri for uri in self.redirect_uris.split(' ') if '*' not in uri]
72+
)
73+
super().clean()
74+
if uris_with_wildcard:
75+
self.redirect_uris += ' ' + ' '.join(uris_with_wildcard)
76+
77+
class Meta:
78+
db_table = 'oauth2_provider_application'
79+
app_label = 'wildcard_oauth2'

ansible_wisdom/wildcard_oauth2/tests/__init__.py

Whitespace-only changes.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from django.core.exceptions import ValidationError
2+
from django.test import TestCase
3+
4+
from ..models import Application, validate_uris
5+
6+
redirect_uris = [
7+
'vscode://redhat.ansible',
8+
r'https://(.*)\.github\.dev/extension-auth-callback?(.*)',
9+
]
10+
11+
12+
class _AppLabel:
13+
app_label = 'wildcard_oauth2'
14+
15+
16+
class WildcardOAuth2Test(TestCase):
17+
def setUp(self):
18+
self.app = Application(redirect_uris=' '.join(redirect_uris))
19+
20+
def test_standalone_vscode_callback_uri(self):
21+
rc = self.app.redirect_uri_allowed('vscode://redhat.ansible')
22+
self.assertTrue(rc)
23+
self.app.clean()
24+
25+
def test_invalid_callback_uri(self):
26+
rc = self.app.redirect_uri_allowed('vscode://othercompany.ansible')
27+
self.assertFalse(rc)
28+
29+
def test_valid_codespases_callback_uri(self):
30+
rc = self.app.redirect_uri_allowed(
31+
'https://estruyf-opulent-capybara-4grqx5g7953754v.github.dev/'
32+
'extension-auth-callback?state=5d6adcfd65b9595ea01f177eccf938c7'
33+
)
34+
self.assertTrue(rc)
35+
36+
def test_invalid_codespases_callback_uri(self):
37+
rc = self.app.redirect_uri_allowed(
38+
'https://estruyf-opulent-capybara-4grqx5g7953754v.github.com/'
39+
'extension-auth-callback?state=5d6adcfd65b9595ea01f177eccf938c7'
40+
)
41+
self.assertFalse(rc)
42+
43+
44+
class ValidateUrisTest(TestCase):
45+
def test_uri_no_error(self):
46+
validate_uris('https://example.com/callback')
47+
48+
def test_uri_containing_fragment(self):
49+
try:
50+
validate_uris('https://example.com/callback#fragment')
51+
except ValidationError as e:
52+
self.assertEqual(e.message, 'Redirect URIs must not contain fragments')
53+
54+
def test_uri_containing_invalid_scheme(self):
55+
try:
56+
validate_uris('myapp://example.com/callback')
57+
except ValidationError as e:
58+
self.assertEqual(e.message, 'Redirect URI scheme is not allowed.')
59+
60+
def test_uri_containing_no_domain(self):
61+
try:
62+
validate_uris('vscode:redhat.ansible')
63+
except ValidationError as e:
64+
self.assertEqual(e.message, 'Redirect URI must contain a domain.')

requirements-dev.txt

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,32 @@
66
#
77
black==22.10.0
88
# via -r requirements-dev.in
9-
build==0.10.0
9+
build==1.0.3
1010
# via pip-tools
11-
cfgv==3.3.1
11+
cfgv==3.4.0
1212
# via pre-commit
13-
click==8.1.3
13+
click==8.1.7
1414
# via
1515
# black
1616
# pip-tools
1717
coverage==7.2.1
1818
# via -r requirements-dev.in
19-
distlib==0.3.6
19+
distlib==0.3.7
2020
# via virtualenv
2121
enum-compat==0.0.3
2222
# via torch-model-archiver
23-
filelock==3.12.0
23+
filelock==3.13.1
2424
# via virtualenv
2525
flake8==6.0.0
2626
# via -r requirements-dev.in
27-
grpcio==1.54.2
27+
grpcio==1.59.2
2828
# via grpcio-tools
2929
grpcio-tools==1.54.2
3030
# via -r requirements-dev.in
31-
identify==2.5.24
31+
identify==2.5.31
3232
# via pre-commit
33+
importlib-metadata==6.8.0
34+
# via build
3335
isort==5.10.1
3436
# via -r requirements-dev.in
3537
mccabe==0.7.0
@@ -38,47 +40,50 @@ mypy-extensions==1.0.0
3840
# via black
3941
nodeenv==1.8.0
4042
# via pre-commit
41-
packaging==23.1
43+
packaging==23.2
4244
# via build
43-
pathspec==0.11.1
45+
pathspec==0.11.2
4446
# via
4547
# black
4648
# yamllint
47-
pip-tools==6.13.0
49+
pip-tools==7.3.0
4850
# via -r requirements-dev.in
49-
platformdirs==3.5.1
51+
platformdirs==3.11.0
5052
# via
5153
# black
5254
# virtualenv
53-
pre-commit==3.3.2
55+
pre-commit==3.5.0
5456
# via -r requirements-dev.in
55-
protobuf==4.23.2
57+
protobuf==4.25.0
5658
# via grpcio-tools
5759
pycodestyle==2.10.0
5860
# via flake8
5961
pyflakes==3.0.1
6062
# via flake8
6163
pyproject-hooks==1.0.0
6264
# via build
63-
pyyaml==6.0
65+
pyyaml==6.0.1
6466
# via
6567
# pre-commit
6668
# yamllint
6769
tomli==2.0.1
6870
# via
6971
# black
7072
# build
73+
# pip-tools
7174
# pyproject-hooks
72-
torch-model-archiver==0.8.0
75+
torch-model-archiver==0.9.0
7376
# via -r requirements-dev.in
74-
typing-extensions==4.6.3
77+
typing-extensions==4.8.0
7578
# via black
76-
virtualenv==20.23.0
79+
virtualenv==20.24.6
7780
# via pre-commit
78-
wheel==0.40.0
81+
wheel==0.41.3
7982
# via pip-tools
8083
yamllint==1.29.0
8184
# via -r requirements-dev.in
85+
zipp==3.17.0
86+
# via importlib-metadata
8287

8388
# The following packages are considered to be unsafe in a requirements file:
8489
# pip

requirements.in

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ ansible-lint==6.20.3
99
boto3==1.26.84
1010
# UPDATED MANUALLY: waiting for parent package to be updated
1111
cryptography==41.0.4
12-
datasets==2.10.1
12+
datasets==2.14.6
1313
Django==4.2.7
1414
django-extensions==3.2.1
1515
django-health-check==3.17.0
@@ -28,6 +28,8 @@ opensearch-py==2.1.1
2828
pillow==10.0.1
2929
protobuf==4.22.1
3030
psycopg[binary]==3.1.8
31+
# pin pyarrow on 14.0.1 to address GHSA-5wvp-7f3h-6wmm
32+
pyarrow==14.0.1
3133
pydantic==1.10.2
3234
pytz
3335
pyOpenSSL==23.2.0

0 commit comments

Comments
 (0)