Skip to content

Commit a120eb3

Browse files
Allow custom scheme in rediect_uri
Updated validate_uris, added ALLOWED_REDIRECT_URI_SCHEMES settings, and HttpResponseUriRedirect class.
1 parent 5ed4f50 commit a120eb3

File tree

7 files changed

+126
-15
lines changed

7 files changed

+126
-15
lines changed

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
@@ -32,9 +32,11 @@ def setUp(self):
3232
self.test_user = UserModel.objects.create_user("test_user", "[email protected]", "123456")
3333
self.dev_user = UserModel.objects.create_user("dev_user", "[email protected]", "123456")
3434

35+
oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ['http', 'custom-scheme']
36+
3537
self.application = Application(
3638
name="Test Application",
37-
redirect_uris="http://localhost http://example.com http://example.it",
39+
redirect_uris="http://localhost http://example.com http://example.it custom-scheme://example.com",
3840
user=self.dev_user,
3941
client_type=Application.CLIENT_CONFIDENTIAL,
4042
authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
@@ -92,6 +94,34 @@ def test_pre_auth_valid_client(self):
9294
self.assertEqual(form['scope'].value(), "read write")
9395
self.assertEqual(form['client_id'].value(), self.application.client_id)
9496

97+
def test_pre_auth_valid_client_custom_redirect_uri_scheme(self):
98+
"""
99+
Test response for a valid client_id with response_type: code
100+
using a non-standard, but allowed, redirect_uri scheme.
101+
"""
102+
self.client.login(username="test_user", password="123456")
103+
104+
query_string = urlencode({
105+
'client_id': self.application.client_id,
106+
'response_type': 'code',
107+
'state': 'random_state_string',
108+
'scope': 'read write',
109+
'redirect_uri': 'custom-scheme://example.com',
110+
})
111+
url = "{url}?{qs}".format(url=reverse('oauth2_provider:authorize'), qs=query_string)
112+
113+
response = self.client.get(url)
114+
self.assertEqual(response.status_code, 200)
115+
116+
# check form is in context and form params are valid
117+
self.assertIn("form", response.context)
118+
119+
form = response.context["form"]
120+
self.assertEqual(form['redirect_uri'].value(), "custom-scheme://example.com")
121+
self.assertEqual(form['state'].value(), "random_state_string")
122+
self.assertEqual(form['scope'].value(), "read write")
123+
self.assertEqual(form['client_id'].value(), self.application.client_id)
124+
95125
def test_pre_auth_approval_prompt(self):
96126
"""
97127
@@ -307,6 +337,49 @@ def test_code_post_auth_malicious_redirect_uri(self):
307337
response = self.client.post(reverse('oauth2_provider:authorize'), data=form_data)
308338
self.assertEqual(response.status_code, 400)
309339

340+
def test_code_post_auth_allow_custom_redirect_uri_scheme(self):
341+
"""
342+
Test authorization code is given for an allowed request with response_type: code
343+
using a non-standard, but allowed, redirect_uri scheme.
344+
"""
345+
self.client.login(username="test_user", password="123456")
346+
347+
form_data = {
348+
'client_id': self.application.client_id,
349+
'state': 'random_state_string',
350+
'scope': 'read write',
351+
'redirect_uri': 'custom-scheme://example.com',
352+
'response_type': 'code',
353+
'allow': True,
354+
}
355+
356+
response = self.client.post(reverse('oauth2_provider:authorize'), data=form_data)
357+
self.assertEqual(response.status_code, 302)
358+
self.assertIn('custom-scheme://example.com?', response['Location'])
359+
self.assertIn('state=random_state_string', response['Location'])
360+
self.assertIn('code=', response['Location'])
361+
362+
def test_code_post_auth_deny_custom_redirect_uri_scheme(self):
363+
"""
364+
Test error when resource owner deny access
365+
using a non-standard, but allowed, redirect_uri scheme.
366+
"""
367+
self.client.login(username="test_user", password="123456")
368+
369+
form_data = {
370+
'client_id': self.application.client_id,
371+
'state': 'random_state_string',
372+
'scope': 'read write',
373+
'redirect_uri': 'custom-scheme://example.com',
374+
'response_type': 'code',
375+
'allow': False,
376+
}
377+
378+
response = self.client.post(reverse('oauth2_provider:authorize'), data=form_data)
379+
self.assertEqual(response.status_code, 302)
380+
self.assertIn('custom-scheme://example.com?', response['Location'])
381+
self.assertIn("error=access_denied", response['Location'])
382+
310383

311384
class TestAuthorizationCodeTokenView(BaseTest):
312385
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: 5 additions & 4 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.generic import View, FormView
55
from django.utils import timezone
66

@@ -11,6 +11,7 @@
1111
from ..settings import oauth2_settings
1212
from ..exceptions import OAuthToolkitError
1313
from ..forms import AllowForm
14+
from ..http import HttpResponseUriRedirect
1415
from ..models import get_application_model
1516
from .mixins import OAuthLibMixin
1617

@@ -41,7 +42,7 @@ def error_response(self, error, **kwargs):
4142
redirect, error_response = super(BaseAuthorizationView, self).error_response(error, **kwargs)
4243

4344
if redirect:
44-
return HttpResponseRedirect(error_response['url'])
45+
return HttpResponseUriRedirect(error_response['url'])
4546

4647
status = error_response['error'].status_code
4748
return self.render_to_response(error_response, status=status)
@@ -100,7 +101,7 @@ def form_valid(self, form):
100101
request=self.request, scopes=scopes, credentials=credentials, allow=allow)
101102
self.success_url = uri
102103
log.debug("Success url for the request: {0}".format(self.success_url))
103-
return super(AuthorizationView, self).form_valid(form)
104+
return HttpResponseUriRedirect(self.success_url)
104105

105106
except OAuthToolkitError as error:
106107
return self.error_response(error)
@@ -130,7 +131,7 @@ def get(self, request, *args, **kwargs):
130131
uri, headers, body, status = self.create_authorization_response(
131132
request=self.request, scopes=" ".join(scopes),
132133
credentials=credentials, allow=True)
133-
return HttpResponseRedirect(uri)
134+
return HttpResponseUriRedirect(uri)
134135

135136
return self.render_to_response(self.get_context_data(**kwargs))
136137

0 commit comments

Comments
 (0)