Skip to content

Commit 8f4033a

Browse files
committed
Add HTTP status check before parsing token responses
- Prevents misleading 'Missing access_token parameter' errors when server returns HTTP errors. - Properly raises HTTPError with the actual error message received from the server for 4xx/5xx responses.
1 parent f33dac3 commit 8f4033a

File tree

2 files changed

+72
-0
lines changed

2 files changed

+72
-0
lines changed

requests_oauthlib/oauth2_session.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
55
from oauthlib.oauth2 import LegacyApplicationClient
66
from oauthlib.oauth2 import TokenExpiredError, is_secure_transport
7+
from oauthlib.oauth2.rfc6749.errors import CustomOAuth2Error
78
import requests
89

910
log = logging.getLogger(__name__)
@@ -199,6 +200,17 @@ def authorization_url(self, url, state=None, **kwargs):
199200
state,
200201
)
201202

203+
def validate_token_response(self, r):
204+
message = ""
205+
try:
206+
r.raise_for_status()
207+
except requests.HTTPError as e:
208+
message = str(e)
209+
if r.text:
210+
message += f"\nBody: {r.text}"
211+
if message:
212+
raise CustomOAuth2Error('Response error', message, uri=r.request.url, status_code=r.status_code)
213+
202214
def fetch_token(
203215
self,
204216
token_url,
@@ -403,6 +415,7 @@ def fetch_token(
403415
log.debug("Invoking hook %s.", hook)
404416
r = hook(r)
405417

418+
self.validate_token_response(r)
406419
self._client.parse_request_body_response(r.text, scope=self.scope)
407420
self.token = self._client.token
408421
log.debug("Obtained token %s.", self.token)
@@ -493,6 +506,7 @@ def refresh_token(
493506
log.debug("Invoking hook %s.", hook)
494507
r = hook(r)
495508

509+
self.validate_token_response(r)
496510
self.token = self._client.parse_request_body_response(r.text, scope=self.scope)
497511
if "refresh_token" not in self.token:
498512
log.debug("No new refresh token given. Re-using old.")

tests/test_oauth2_session.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from oauthlib.common import urlencode
1313
from oauthlib.oauth2 import TokenExpiredError, OAuth2Error
1414
from oauthlib.oauth2 import MismatchingStateError
15+
from oauthlib.oauth2.rfc6749.errors import CustomOAuth2Error
1516
from oauthlib.oauth2 import WebApplicationClient, MobileApplicationClient
1617
from oauthlib.oauth2 import LegacyApplicationClient, BackendApplicationClient
1718
from requests_oauthlib import OAuth2Session, TokenUpdated
@@ -524,6 +525,63 @@ def fake_send(r, **kwargs):
524525
sess.fetch_token(url)
525526
self.assertTrue(sess.authorized)
526527

528+
def test_fetch_token_http_error_handling(self):
529+
"""Test that HTTP errors are properly raised instead of parsing error responses as tokens."""
530+
url = "https://example.com/token"
531+
532+
# Test 400 error response (like the original issue)
533+
error_400 = {"messages": [{"logLevel": "Error", "text": "User not Found"}], "status": "Forbidden"}
534+
535+
def fake_400_error(r, **kwargs):
536+
resp = mock.MagicMock()
537+
resp.text = json.dumps(error_400)
538+
resp.status_code = 400
539+
resp.request = mock.MagicMock()
540+
resp.request.url = url
541+
resp.raise_for_status.side_effect = requests.exceptions.HTTPError(
542+
"400 Client Error", response=resp
543+
)
544+
return resp
545+
546+
for client in self.clients:
547+
sess = OAuth2Session(client=client)
548+
sess.send = fake_400_error
549+
550+
if isinstance(client, LegacyApplicationClient):
551+
self.assertRaises(
552+
CustomOAuth2Error,
553+
sess.fetch_token,
554+
url,
555+
username="username1",
556+
password="password1",
557+
)
558+
else:
559+
self.assertRaises(CustomOAuth2Error, sess.fetch_token, url)
560+
561+
def test_refresh_token_http_error_handling(self):
562+
"""Test that HTTP errors are properly raised instead of parsing error responses as tokens."""
563+
url = "https://example.com/refresh"
564+
565+
# Test 400 error response
566+
error_400 = {"error": "invalid_grant",
567+
"error_description": "Refresh token is invalid"}
568+
569+
def fake_400_error(r, **kwargs):
570+
resp = mock.MagicMock()
571+
resp.text = json.dumps(error_400)
572+
resp.status_code = 400
573+
resp.request = mock.MagicMock()
574+
resp.request.url = url
575+
resp.raise_for_status.side_effect = requests.exceptions.HTTPError(
576+
"400 Client Error", response=resp
577+
)
578+
return resp
579+
580+
for client in self.clients:
581+
sess = OAuth2Session(client=client, token=self.token)
582+
sess.send = fake_400_error
583+
self.assertRaises(CustomOAuth2Error, sess.refresh_token, url)
584+
527585

528586
class OAuth2SessionNetrcTest(OAuth2SessionTest):
529587
"""Ensure that there is no magic auth handling.

0 commit comments

Comments
 (0)