Skip to content

Commit fc291ce

Browse files
authored
Security BCP: Remove OOB (#1124)
* Remove OOB * Indicate that this is a security fix.
1 parent e761ebc commit fc291ce

File tree

4 files changed

+8
-147
lines changed

4 files changed

+8
-147
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3636
### Fixed
3737
* #1108 OIDC: Fix `validate_bearer_token()` to properly set `request.scopes` to the list of granted scopes.
3838

39+
### Removed
40+
* #1124 (**Breaking**, **Security**) Removes support for insecure `urn:ietf:wg:oauth:2.0:oob` and `urn:ietf:wg:oauth:2.0:oob:auto` which are replaced
41+
by [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252) "OAuth 2.0 for Native Apps" BCP. Google has
42+
[deprecated use of oob](https://developers.googleblog.com/2022/02/making-oauth-flows-safer.html?m=1#disallowed-oob) with
43+
a final end date of 2022-10-03. If you still rely on oob support in django-oauth-toolkit, do not upgrade to this release.
44+
3945
## [1.7.0] 2022-01-23
4046

4147
### Added

oauth2_provider/templates/oauth2_provider/authorized-oob.html

Lines changed: 0 additions & 23 deletions
This file was deleted.

oauth2_provider/views/base.py

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import json
22
import logging
3-
import urllib.parse
43

54
from django.contrib.auth.mixins import LoginRequiredMixin
6-
from django.http import HttpResponse, JsonResponse
7-
from django.shortcuts import render
8-
from django.urls import reverse
5+
from django.http import HttpResponse
96
from django.utils import timezone
107
from django.utils.decorators import method_decorator
118
from django.views.decorators.csrf import csrf_exempt
@@ -207,42 +204,13 @@ def get(self, request, *args, **kwargs):
207204
credentials=credentials,
208205
allow=True,
209206
)
210-
return self.redirect(uri, application, token)
207+
return self.redirect(uri, application)
211208

212209
except OAuthToolkitError as error:
213210
return self.error_response(error, application)
214211

215212
return self.render_to_response(self.get_context_data(**kwargs))
216213

217-
def redirect(self, redirect_to, application, token=None):
218-
219-
if not redirect_to.startswith("urn:ietf:wg:oauth:2.0:oob"):
220-
return super().redirect(redirect_to, application)
221-
222-
parsed_redirect = urllib.parse.urlparse(redirect_to)
223-
code = urllib.parse.parse_qs(parsed_redirect.query)["code"][0]
224-
225-
if redirect_to.startswith("urn:ietf:wg:oauth:2.0:oob:auto"):
226-
227-
response = {
228-
"access_token": code,
229-
"token_uri": redirect_to,
230-
"client_id": application.client_id,
231-
"client_secret": application.client_secret,
232-
"revoke_uri": reverse("oauth2_provider:revoke-token"),
233-
}
234-
235-
return JsonResponse(response)
236-
237-
else:
238-
return render(
239-
request=self.request,
240-
template_name="oauth2_provider/authorized-oob.html",
241-
context={
242-
"code": code,
243-
},
244-
)
245-
246214

247215
@method_decorator(csrf_exempt, name="dispatch")
248216
class TokenView(OAuthLibMixin, View):

tests/test_authorization_code.py

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import datetime
33
import hashlib
44
import json
5-
import re
65
from urllib.parse import parse_qs, urlparse
76

87
import pytest
@@ -32,8 +31,6 @@
3231
RefreshToken = get_refresh_token_model()
3332
UserModel = get_user_model()
3433

35-
URI_OOB = "urn:ietf:wg:oauth:2.0:oob"
36-
URI_OOB_AUTO = "urn:ietf:wg:oauth:2.0:oob:auto"
3734
CLEARTEXT_SECRET = "1234567890abcdefghijklmnopqrstuvwxyz"
3835

3936

@@ -56,7 +53,6 @@ def setUp(self):
5653
name="Test Application",
5754
redirect_uris=(
5855
"http://localhost http://example.com http://example.org custom-scheme://example.com"
59-
" " + URI_OOB + " " + URI_OOB_AUTO
6056
),
6157
user=self.dev_user,
6258
client_type=Application.CLIENT_CONFIDENTIAL,
@@ -1532,92 +1528,6 @@ def test_code_exchange_succeed_when_redirect_uri_match_with_multiple_query_param
15321528
self.assertEqual(content["scope"], "read write")
15331529
self.assertEqual(content["expires_in"], self.oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS)
15341530

1535-
def test_oob_as_html(self):
1536-
"""
1537-
Test out-of-band authentication.
1538-
"""
1539-
self.client.login(username="test_user", password="123456")
1540-
1541-
authcode_data = {
1542-
"client_id": self.application.client_id,
1543-
"state": "random_state_string",
1544-
"scope": "read write",
1545-
"redirect_uri": URI_OOB,
1546-
"response_type": "code",
1547-
"allow": True,
1548-
}
1549-
1550-
response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data)
1551-
self.assertEqual(response.status_code, 200)
1552-
self.assertRegex(response["Content-Type"], r"^text/html")
1553-
1554-
content = response.content.decode("utf-8")
1555-
1556-
# "A lot of applications, for legacy reasons, use this and regex
1557-
# to extract the token, risking summoning zalgo in the process."
1558-
# -- https://github.com/jazzband/django-oauth-toolkit/issues/235
1559-
1560-
matches = re.search(r".*<code>([^<>]*)</code>", content)
1561-
self.assertIsNotNone(matches, msg="OOB response contains code inside <code> tag")
1562-
self.assertEqual(len(matches.groups()), 1, msg="OOB response contains multiple <code> tags")
1563-
authorization_code = matches.groups()[0]
1564-
1565-
token_request_data = {
1566-
"grant_type": "authorization_code",
1567-
"code": authorization_code,
1568-
"redirect_uri": URI_OOB,
1569-
"client_id": self.application.client_id,
1570-
"client_secret": CLEARTEXT_SECRET,
1571-
}
1572-
1573-
response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data)
1574-
self.assertEqual(response.status_code, 200)
1575-
1576-
content = json.loads(response.content.decode("utf-8"))
1577-
self.assertEqual(content["token_type"], "Bearer")
1578-
self.assertEqual(content["scope"], "read write")
1579-
self.assertEqual(content["expires_in"], self.oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS)
1580-
1581-
def test_oob_as_json(self):
1582-
"""
1583-
Test out-of-band authentication, with a JSON response.
1584-
"""
1585-
self.client.login(username="test_user", password="123456")
1586-
1587-
authcode_data = {
1588-
"client_id": self.application.client_id,
1589-
"state": "random_state_string",
1590-
"scope": "read write",
1591-
"redirect_uri": URI_OOB_AUTO,
1592-
"response_type": "code",
1593-
"allow": True,
1594-
}
1595-
1596-
response = self.client.post(reverse("oauth2_provider:authorize"), data=authcode_data)
1597-
self.assertEqual(response.status_code, 200)
1598-
self.assertRegex(response["Content-Type"], "^application/json")
1599-
1600-
parsed_response = json.loads(response.content.decode("utf-8"))
1601-
1602-
self.assertIn("access_token", parsed_response)
1603-
authorization_code = parsed_response["access_token"]
1604-
1605-
token_request_data = {
1606-
"grant_type": "authorization_code",
1607-
"code": authorization_code,
1608-
"redirect_uri": URI_OOB_AUTO,
1609-
"client_id": self.application.client_id,
1610-
"client_secret": CLEARTEXT_SECRET,
1611-
}
1612-
1613-
response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data)
1614-
self.assertEqual(response.status_code, 200)
1615-
1616-
content = json.loads(response.content.decode("utf-8"))
1617-
self.assertEqual(content["token_type"], "Bearer")
1618-
self.assertEqual(content["scope"], "read write")
1619-
self.assertEqual(content["expires_in"], self.oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS)
1620-
16211531

16221532
@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW)
16231533
class TestOIDCAuthorizationCodeTokenView(BaseAuthorizationCodeTokenView):

0 commit comments

Comments
 (0)