Skip to content

Commit 51c39d7

Browse files
authored
Handle Oauth2 errors for SSO OIDC service (boto#3642)
1 parent 0e9fa7e commit 51c39d7

File tree

3 files changed

+92
-0
lines changed

3 files changed

+92
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "enhancement",
3+
"category": "``sso-oidc``",
4+
"description": "Fixed missing error messages in SSO OIDC error responses by mapping OAuth2 error_description field to the standard Message field. Issue was raised in `#2216 <https://github.com/boto/botocore/issues/2216>`__."
5+
}

botocore/handlers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,21 @@ def _should_handle_200_error(operation_model, response_dict):
13181318
return True
13191319

13201320

1321+
def _map_oauth2_errors(response_dict, **kwargs):
1322+
# SSO OIDC follows the OAuth2 standard, which returns error messages in
1323+
# 'error_description' instead of the 'Message' field botocore expects.
1324+
try:
1325+
if response_dict.get('status_code') < 400:
1326+
return
1327+
body = json.loads(response_dict.get('body', b'{}'))
1328+
if message := body.get('error_description'):
1329+
if not body.get('Message', body.get("message")):
1330+
body['Message'] = message
1331+
response_dict['body'] = json.dumps(body).encode('utf-8')
1332+
except (ValueError, AttributeError, TypeError):
1333+
pass
1334+
1335+
13211336
def _update_status_code(response, **kwargs):
13221337
# Update the http_response status code when the parsed response has been
13231338
# modified in a handler. This enables retries for cases like ``_handle_200_error``.
@@ -1503,6 +1518,7 @@ def get_bearer_auth_supported_services():
15031518
),
15041519
('before-parse.s3.*', handle_expires_header),
15051520
('before-parse.s3.*', _handle_200_error, REGISTER_FIRST),
1521+
('before-parse.sso-oidc.*', _map_oauth2_errors),
15061522
('before-parameter-build', generate_idempotent_uuid),
15071523
('before-parameter-build', _handle_request_validation_mode_member),
15081524
('before-parameter-build.s3', validate_bucket_name),

tests/unit/test_handlers.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2153,3 +2153,74 @@ def test_set_auth_scheme_preference_signer_with_bearer_token(
21532153
assert signature_version == expected_signature_version, (
21542154
f"Expected '{expected_signature_version}' but got '{signature_version}'"
21552155
)
2156+
2157+
2158+
def test_map_oauth2_errors_adds_message():
2159+
body = {'error': 'invalid_grant', 'error_description': 'Token is expired'}
2160+
response_dict = {
2161+
'status_code': 400,
2162+
'body': json.dumps(body).encode('utf-8'),
2163+
'headers': {},
2164+
}
2165+
handlers._map_oauth2_errors(response_dict)
2166+
parsed = json.loads(response_dict['body'])
2167+
assert parsed['Message'] == 'Token is expired'
2168+
assert parsed['error'] == body['error']
2169+
assert parsed['error_description'] == body['error_description']
2170+
2171+
2172+
@pytest.mark.parametrize(
2173+
"status_code, body",
2174+
[
2175+
# Success response
2176+
(200, {'access_token': 'foo'}),
2177+
# No error_description
2178+
(400, {'error': 'invalid_grant'}),
2179+
# Empty error description
2180+
(400, {'error': 'invalid_grant', 'error_description': ''}),
2181+
# Error response already contains Message or message
2182+
(
2183+
400,
2184+
{
2185+
'error': 'invalid_grant',
2186+
'error_description': 'foo',
2187+
'Message': 'something went wrong',
2188+
},
2189+
),
2190+
(
2191+
400,
2192+
{
2193+
'error': 'invalid_grant',
2194+
'error_description': 'bar',
2195+
'message': 'something went wrong',
2196+
},
2197+
),
2198+
],
2199+
)
2200+
def test_map_oauth2_errors_preserves_body(status_code, body):
2201+
response_dict = {
2202+
'status_code': status_code,
2203+
'body': json.dumps(body).encode('utf-8'),
2204+
'headers': {},
2205+
}
2206+
original_body = response_dict['body']
2207+
handlers._map_oauth2_errors(response_dict)
2208+
assert response_dict['body'] == original_body
2209+
2210+
2211+
@pytest.mark.parametrize(
2212+
"response_dict",
2213+
[
2214+
# Invalid JSON body
2215+
{'status_code': 400, 'body': b'not json', 'headers': {}},
2216+
# Empty body
2217+
{'status_code': 400, 'body': b'', 'headers': {}},
2218+
# No body
2219+
{'status_code': 400, 'headers': {}},
2220+
# Missing status_code
2221+
{'body': b'{}', 'headers': {}},
2222+
],
2223+
)
2224+
def test_map_oauth2_errors_does_not_raise(response_dict):
2225+
# Should silently pass on malformed inputs
2226+
handlers._map_oauth2_errors(response_dict)

0 commit comments

Comments
 (0)