Skip to content

Commit c0b27fb

Browse files
committed
Merge branch 'allow-custom-uri-schemes' of https://github.com/RodneyRichardson/django-oauth-toolkit into fuffa
Conflicts: oauth2_provider/views/base.py
2 parents 060022f + 469833f commit c0b27fb

File tree

8 files changed

+128
-16
lines changed

8 files changed

+128
-16
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Stéphane Raimbault
1111
Emanuele Palazzetti
1212
David Fischer
1313
Ash Christopher
14+
Rodney Richardson

oauth2_provider/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = '0.7.2'
1+
__version__ = '0.7.3'
22

33
__author__ = "Massimiliano Pippi & Federico Frenguelli"
44

oauth2_provider/http.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.http import HttpResponseRedirect
2+
3+
from .settings import oauth2_settings
4+
5+
6+
class HttpResponseUriRedirect(HttpResponseRedirect):
7+
def __init__(self, redirect_to, *args, **kwargs):
8+
self.allowed_schemes = oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES
9+
super(HttpResponseUriRedirect, self).__init__(redirect_to, *args, **kwargs)

oauth2_provider/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
'ACCESS_TOKEN_EXPIRE_SECONDS': 36000,
4242
'APPLICATION_MODEL': getattr(settings, 'OAUTH2_PROVIDER_APPLICATION_MODEL', 'oauth2_provider.Application'),
4343
'REQUEST_APPROVAL_PROMPT': 'force',
44+
'ALLOWED_REDIRECT_URI_SCHEMES': ['http', 'https'],
4445

4546
# Special settings that will be evaluated at runtime
4647
'_SCOPES': [],
@@ -52,6 +53,7 @@
5253
'CLIENT_SECRET_GENERATOR_CLASS',
5354
'OAUTH2_VALIDATOR_CLASS',
5455
'SCOPES',
56+
'ALLOWED_REDIRECT_URI_SCHEMES',
5557
)
5658

5759
# List of settings that may be in string import notation.

oauth2_provider/tests/test_authorization_code.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ def setUp(self):
3434
self.test_user = UserModel.objects.create_user("test_user", "[email protected]", "123456")
3535
self.dev_user = UserModel.objects.create_user("dev_user", "[email protected]", "123456")
3636

37+
oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ['http', 'custom-scheme']
38+
3739
self.application = Application(
3840
name="Test Application",
39-
redirect_uris="http://localhost http://example.com http://example.it",
41+
redirect_uris="http://localhost http://example.com http://example.it custom-scheme://example.com",
4042
user=self.dev_user,
4143
client_type=Application.CLIENT_CONFIDENTIAL,
4244
authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
@@ -115,6 +117,34 @@ def test_pre_auth_valid_client(self):
115117
self.assertEqual(form['scope'].value(), "read write")
116118
self.assertEqual(form['client_id'].value(), self.application.client_id)
117119

120+
def test_pre_auth_valid_client_custom_redirect_uri_scheme(self):
121+
"""
122+
Test response for a valid client_id with response_type: code
123+
using a non-standard, but allowed, redirect_uri scheme.
124+
"""
125+
self.client.login(username="test_user", password="123456")
126+
127+
query_string = urlencode({
128+
'client_id': self.application.client_id,
129+
'response_type': 'code',
130+
'state': 'random_state_string',
131+
'scope': 'read write',
132+
'redirect_uri': 'custom-scheme://example.com',
133+
})
134+
url = "{url}?{qs}".format(url=reverse('oauth2_provider:authorize'), qs=query_string)
135+
136+
response = self.client.get(url)
137+
self.assertEqual(response.status_code, 200)
138+
139+
# check form is in context and form params are valid
140+
self.assertIn("form", response.context)
141+
142+
form = response.context["form"]
143+
self.assertEqual(form['redirect_uri'].value(), "custom-scheme://example.com")
144+
self.assertEqual(form['state'].value(), "random_state_string")
145+
self.assertEqual(form['scope'].value(), "read write")
146+
self.assertEqual(form['client_id'].value(), self.application.client_id)
147+
118148
def test_pre_auth_approval_prompt(self):
119149
"""
120150
@@ -330,6 +360,49 @@ def test_code_post_auth_malicious_redirect_uri(self):
330360
response = self.client.post(reverse('oauth2_provider:authorize'), data=form_data)
331361
self.assertEqual(response.status_code, 400)
332362

363+
def test_code_post_auth_allow_custom_redirect_uri_scheme(self):
364+
"""
365+
Test authorization code is given for an allowed request with response_type: code
366+
using a non-standard, but allowed, redirect_uri scheme.
367+
"""
368+
self.client.login(username="test_user", password="123456")
369+
370+
form_data = {
371+
'client_id': self.application.client_id,
372+
'state': 'random_state_string',
373+
'scope': 'read write',
374+
'redirect_uri': 'custom-scheme://example.com',
375+
'response_type': 'code',
376+
'allow': True,
377+
}
378+
379+
response = self.client.post(reverse('oauth2_provider:authorize'), data=form_data)
380+
self.assertEqual(response.status_code, 302)
381+
self.assertIn('custom-scheme://example.com?', response['Location'])
382+
self.assertIn('state=random_state_string', response['Location'])
383+
self.assertIn('code=', response['Location'])
384+
385+
def test_code_post_auth_deny_custom_redirect_uri_scheme(self):
386+
"""
387+
Test error when resource owner deny access
388+
using a non-standard, but allowed, redirect_uri scheme.
389+
"""
390+
self.client.login(username="test_user", password="123456")
391+
392+
form_data = {
393+
'client_id': self.application.client_id,
394+
'state': 'random_state_string',
395+
'scope': 'read write',
396+
'redirect_uri': 'custom-scheme://example.com',
397+
'response_type': 'code',
398+
'allow': False,
399+
}
400+
401+
response = self.client.post(reverse('oauth2_provider:authorize'), data=form_data)
402+
self.assertEqual(response.status_code, 302)
403+
self.assertIn('custom-scheme://example.com?', response['Location'])
404+
self.assertIn("error=access_denied", response['Location'])
405+
333406

334407
class TestAuthorizationCodeTokenView(BaseTest):
335408
def get_auth(self):

oauth2_provider/tests/test_validators.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,35 @@
33
from django.test import TestCase
44
from django.core.validators import ValidationError
55

6+
from ..settings import oauth2_settings
67
from ..validators import validate_uris
78

89

910
class TestValidators(TestCase):
1011
def test_validate_good_uris(self):
11-
good_urls = 'http://example.com/ http://example.it/?key=val http://example'
12+
good_uris = 'http://example.com/ http://example.it/?key=val http://example'
1213
# Check ValidationError not thrown
13-
validate_uris(good_urls)
14+
validate_uris(good_uris)
15+
16+
def test_validate_custom_uri_scheme(self):
17+
oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ['my-scheme', 'http']
18+
good_uris = 'my-scheme://example.com http://example.com'
19+
# Check ValidationError not thrown
20+
validate_uris(good_uris)
21+
22+
def test_validate_whitespace_separators(self):
23+
# Check that whitespace can be used as a separator
24+
good_uris = 'http://example\r\nhttp://example\thttp://example'
25+
# Check ValidationError not thrown
26+
validate_uris(good_uris)
1427

1528
def test_validate_bad_uris(self):
16-
bad_url = 'http://example.com/#fragment'
17-
self.assertRaises(ValidationError, validate_uris, bad_url)
18-
bad_url = 'http:/example.com'
19-
self.assertRaises(ValidationError, validate_uris, bad_url)
29+
bad_uri = 'http://example.com/#fragment'
30+
self.assertRaises(ValidationError, validate_uris, bad_uri)
31+
bad_uri = 'http:/example.com'
32+
self.assertRaises(ValidationError, validate_uris, bad_uri)
33+
bad_uri = 'my-scheme://example.com'
34+
self.assertRaises(ValidationError, validate_uris, bad_uri)
35+
bad_uri = 'sdklfsjlfjljdflksjlkfjsdkl'
36+
self.assertRaises(ValidationError, validate_uris, bad_uri)
37+

oauth2_provider/validators.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
from django.utils.six.moves.urllib.parse import urlsplit, urlunsplit
99
from django.core.validators import RegexValidator
1010

11+
from .settings import oauth2_settings
1112

1213
class URIValidator(RegexValidator):
1314
regex = re.compile(
14-
r'^(?:[a-z0-9\.\-]*)s?://' # http:// or https://
15+
r'^(?:[a-z][a-z0-9\.\-\+]*)://' # scheme...
1516
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
1617
r'(?!-)[A-Z\d-]{1,63}(?<!-)|' # also cover non-dotted domain
1718
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
@@ -41,16 +42,23 @@ def __call__(self, value):
4142

4243

4344
class RedirectURIValidator(URIValidator):
45+
def __init__(self, allowed_schemes):
46+
self.allowed_schemes = allowed_schemes
47+
4448
def __call__(self, value):
4549
super(RedirectURIValidator, self).__call__(value)
50+
value = force_text(value)
4651
if len(value.split('#')) > 1:
4752
raise ValidationError('Redirect URIs must not contain fragments')
53+
scheme, netloc, path, query, fragment = urlsplit(value)
54+
if scheme.lower() not in self.allowed_schemes:
55+
raise ValidationError('Redirect URI scheme is not allowed.')
4856

4957

5058
def validate_uris(value):
5159
"""
52-
This validator ensures that `value` contains valid blank-separated urls"
60+
This validator ensures that `value` contains valid blank-separated URIs"
5361
"""
54-
v = RedirectURIValidator()
62+
v = RedirectURIValidator(oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES)
5563
for uri in value.split():
5664
v(uri)

oauth2_provider/views/base.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22

3-
from django.http import HttpResponse, HttpResponseRedirect
3+
from django.http import HttpResponse
44
from django.views.decorators.debug import sensitive_post_parameters
55
from django.views.generic import View, FormView
66
from django.utils import timezone
@@ -13,6 +13,7 @@
1313
from ..settings import oauth2_settings
1414
from ..exceptions import OAuthToolkitError
1515
from ..forms import AllowForm
16+
from ..http import HttpResponseUriRedirect
1617
from ..models import get_application_model
1718
from .mixins import OAuthLibMixin
1819

@@ -43,7 +44,7 @@ def error_response(self, error, **kwargs):
4344
redirect, error_response = super(BaseAuthorizationView, self).error_response(error, **kwargs)
4445

4546
if redirect:
46-
return HttpResponseRedirect(error_response['url'])
47+
return HttpResponseUriRedirect(error_response['url'])
4748

4849
status = error_response['error'].status_code
4950
return self.render_to_response(error_response, status=status)
@@ -104,7 +105,7 @@ def form_valid(self, form):
104105
request=self.request, scopes=scopes, credentials=credentials, allow=allow)
105106
self.success_url = uri
106107
log.debug("Success url for the request: {0}".format(self.success_url))
107-
return super(AuthorizationView, self).form_valid(form)
108+
return HttpResponseUriRedirect(self.success_url)
108109

109110
except OAuthToolkitError as error:
110111
return self.error_response(error)
@@ -135,7 +136,7 @@ def get(self, request, *args, **kwargs):
135136
uri, headers, body, status = self.create_authorization_response(
136137
request=self.request, scopes=" ".join(scopes),
137138
credentials=credentials, allow=True)
138-
return HttpResponseRedirect(uri)
139+
return HttpResponseUriRedirect(uri)
139140

140141
elif require_approval == 'auto':
141142
tokens = request.user.accesstoken_set.filter(application=kwargs['application'],
@@ -146,7 +147,7 @@ def get(self, request, *args, **kwargs):
146147
uri, headers, body, status = self.create_authorization_response(
147148
request=self.request, scopes=" ".join(scopes),
148149
credentials=credentials, allow=True)
149-
return HttpResponseRedirect(uri)
150+
return HttpResponseUriRedirect(uri)
150151

151152
return self.render_to_response(self.get_context_data(**kwargs))
152153

0 commit comments

Comments
 (0)