Skip to content

Commit f9c9c33

Browse files
realsuayipdopry
authored andcommitted
fix: token request throws an error when client is provided in body
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 f9c9c33

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)