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
importjson
22
importlogging
3-
importurllib.parse
43

54
fromdjango.contrib.auth.mixinsimportLoginRequiredMixin
6-
fromdjango.httpimportHttpResponse, JsonResponse
7-
fromdjango.shortcutsimportrender
8-
fromdjango.urlsimportreverse
5+
fromdjango.httpimportHttpResponse
96
fromdjango.utilsimporttimezone
107
fromdjango.utils.decoratorsimportmethod_decorator
118
fromdjango.views.decorators.csrfimportcsrf_exempt
@@ -207,42 +204,13 @@ def get(self, request, *args, **kwargs):
207204
credentials=credentials,
208205
allow=True,
209206
)
210-
returnself.redirect(uri, application, token)
207+
returnself.redirect(uri, application)
211208

212209
exceptOAuthToolkitErroraserror:
213210
returnself.error_response(error, application)
214211

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

217-
defredirect(self, redirect_to, application, token=None):
218-
219-
ifnotredirect_to.startswith("urn:ietf:wg:oauth:2.0:oob"):
220-
returnsuper().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-
ifredirect_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-
returnJsonResponse(response)
236-
237-
else:
238-
returnrender(
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
classTokenView(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
importdatetime
33
importhashlib
44
importjson
5-
importre
65
fromurllib.parseimportparse_qs, urlparse
76

87
importpytest
@@ -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-
deftest_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-
deftest_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
classTestOIDCAuthorizationCodeTokenView(BaseAuthorizationCodeTokenView):

0 commit comments

Comments
(0)