Skip to content

Commit 2cc2b60

Browse files
authored
fix: token request throws an error when client is provided in body (#1252)
It ends up overwriting request.client which should be the application. This fix ensures request client is set to the client_id
1 parent 87fef47 commit 2cc2b60

File tree

5 files changed

+95
-12
lines changed

5 files changed

+95
-12
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Peter Karman
102102
Peter McDonald
103103
Petr Dlouhý
104104
pySilver
105+
@realsuayip
105106
Rodney Richardson
106107
Rustem Saiargaliev
107108
Rustem Saiargaliev

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
* Support for Django 5.2
1111
* Support for Python 3.14 (Django >= 5.2.8)
12+
* #1539 Add device authorization grant support
13+
1214

1315
<!--
1416
### Changed
1517
### Deprecated
1618
### Removed
19+
-->
1720
### Fixed
21+
* #1252 Fix crash when 'client' is in token request body
22+
<!--
1823
### Security
1924
-->
2025

@@ -27,7 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2732
* #1506 Support for Wildcard Origin and Redirect URIs - Adds a new setting [ALLOW_URL_WILDCARDS](https://django-oauth-toolkit.readthedocs.io/en/latest/settings.html#allow-uri-wildcards). This feature is useful for working with CI service such as cloudflare, netlify, and vercel that offer branch
2833
deployments for development previews and user acceptance testing.
2934
* #1586 Turkish language support added
30-
* #1539 Add device authorization grant support
3135

3236
### Changed
3337
The project is now hosted in the django-oauth organization.

oauth2_provider/oauth2_validators.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -214,19 +214,31 @@ def _load_application(self, client_id, request):
214214
If request.client was not set, load application instance for given
215215
client_id and store it in request.client
216216
"""
217-
218-
# we want to be sure that request has the client attribute!
219-
assert hasattr(request, "client"), '"request" instance has no "client" attribute'
220-
217+
if request.client:
218+
# check for cached client, to save the db hit if this has already been loaded
219+
if not isinstance(request.client, Application):
220+
# resetting request.client (client_id=%r): not an Application, something else set request.client erroneously
221+
request.client = None
222+
elif request.client.client_id != client_id:
223+
# resetting request.client (client_id=%r): request.client.client_id does not match the given client_id
224+
request.client = None
225+
elif not request.client.is_usable(request):
226+
# resetting request.client (client_id=%r): request.client is a valid Application, but is not usable
227+
request.client = None
228+
else:
229+
# request.client is a valid Application, reusing it
230+
return request.client
221231
try:
222-
request.client = request.client or Application.objects.get(client_id=client_id)
223-
# Check that the application can be used (defaults to always True)
224-
if not request.client.is_usable(request):
225-
log.debug("Failed body authentication: Application %r is disabled" % (client_id))
232+
# cache not hit, loading application from database for client_id %r
233+
client = Application.objects.get(client_id=client_id)
234+
if not client.is_usable(request):
235+
# Failed to load application: Application %r is not usable
226236
return None
237+
request.client = client
238+
# Loaded application with client_id %r from database
227239
return request.client
228240
except Application.DoesNotExist:
229-
log.debug("Failed body authentication: Application %r does not exist" % (client_id))
241+
# Failed to load application: Application with client_id %r does not exist
230242
return None
231243

232244
def _set_oauth2_error_on_request(self, request, access_token, scopes):
@@ -289,6 +301,7 @@ def client_authentication_required(self, request, *args, **kwargs):
289301
pass
290302

291303
self._load_application(request.client_id, request)
304+
log.debug("Determining if client authentication is required for client %r", request.client)
292305
if request.client:
293306
return request.client.client_type == AbstractApplication.CLIENT_CONFIDENTIAL
294307

tests/test_authorization_code.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,27 @@ def test_request_body_params(self):
13081308
self.assertEqual(content["scope"], "read write")
13091309
self.assertEqual(content["expires_in"], self.oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS)
13101310

1311+
def test_request_body_params_client_typo(self):
1312+
"""
1313+
Verify that using incorrect parameter name (client instead of client_id) returns invalid_client error
1314+
"""
1315+
self.client.login(username="test_user", password="123456")
1316+
authorization_code = self.get_auth()
1317+
1318+
token_request_data = {
1319+
"grant_type": "authorization_code",
1320+
"code": authorization_code,
1321+
"redirect_uri": "http://example.org",
1322+
"client": self.application.client_id,
1323+
"client_secret": CLEARTEXT_SECRET,
1324+
}
1325+
1326+
response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data)
1327+
self.assertEqual(response.status_code, 401)
1328+
1329+
content = json.loads(response.content.decode("utf-8"))
1330+
self.assertEqual(content["error"], "invalid_client")
1331+
13111332
def test_public(self):
13121333
"""
13131334
Request an access token using client_type: public

tests/test_oauth2_validators.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,52 @@ def test_client_authentication_required(self):
216216
self.request.client = ""
217217
self.assertTrue(self.validator.client_authentication_required(self.request))
218218

219-
def test_load_application_fails_when_request_has_no_client(self):
220-
self.assertRaises(AssertionError, self.validator.authenticate_client_id, "client_id", {})
219+
def test_load_application_loads_client_id_when_request_has_no_client(self):
220+
self.request.client = None
221+
application = self.validator._load_application("client_id", self.request)
222+
self.assertEqual(application, self.application)
223+
224+
def test_load_application_uses_cached_when_request_has_valid_client_matching_client_id(self):
225+
self.request.client = self.application
226+
application = self.validator._load_application("client_id", self.request)
227+
self.assertIs(application, self.application)
228+
self.assertIs(self.request.client, self.application)
229+
230+
def test_load_application_succeeds_when_request_has_invalid_client_valid_client_id(self):
231+
self.request.client = 'invalid_client'
232+
application = self.validator._load_application("client_id", self.request)
233+
self.assertEqual(application, self.application)
234+
self.assertEqual(self.request.client, self.application)
235+
236+
def test_load_application_overwrites_client_on_client_id_mismatch(self):
237+
another_application = Application.objects.create(
238+
client_id="another_client_id",
239+
client_secret=CLEARTEXT_SECRET,
240+
user=self.user,
241+
client_type=Application.CLIENT_PUBLIC,
242+
authorization_grant_type=Application.GRANT_PASSWORD,
243+
)
244+
self.request.client = another_application
245+
application = self.validator._load_application("client_id", self.request)
246+
self.assertEqual(application, self.application)
247+
self.assertEqual(self.request.client, self.application)
248+
another_application.delete()
249+
250+
@mock.patch.object(Application, "is_usable")
251+
def test_load_application_returns_none_when_client_not_usable_cached(self, mock_is_usable):
252+
mock_is_usable.return_value = False
253+
self.request.client = self.application
254+
application = self.validator._load_application("client_id", self.request)
255+
self.assertIsNone(application)
256+
self.assertIsNone(self.request.client)
257+
258+
@mock.patch.object(Application, "is_usable")
259+
def test_load_application_returns_none_when_client_not_usable_db_lookup(self, mock_is_usable):
260+
mock_is_usable.return_value = False
261+
self.request.client = None
262+
application = self.validator._load_application("client_id", self.request)
263+
self.assertIsNone(application)
264+
self.assertIsNone(self.request.client)
221265

222266
def test_rotate_refresh_token__is_true(self):
223267
self.assertTrue(self.validator.rotate_refresh_token(mock.MagicMock()))

0 commit comments

Comments
 (0)