Skip to content

Commit 709496e

Browse files
committed
Added capabilities for reading the claims challenge and throwing it with the exception
1 parent 5866dda commit 709496e

File tree

3 files changed

+46
-4
lines changed

3 files changed

+46
-4
lines changed

azure-devops/azure/devops/client.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import os
1010
import re
1111
import uuid
12+
import www_authenticate
1213

1314
from msrest import Deserializer, Serializer
1415
from msrest.exceptions import DeserializationError, SerializationError
@@ -285,10 +286,31 @@ def _handle_error(self, request, response):
285286
pass
286287
elif response.content is not None:
287288
error_message = response.content.decode("utf-8") + ' '
289+
288290
if response.status_code == 401:
289291
full_message_format = '{error_message}The requested resource requires user authentication: {url}'
290-
raise AzureDevOpsAuthenticationError(full_message_format.format(error_message=error_message,
291-
url=request.url))
292+
formatted_message = full_message_format.format(error_message=error_message, url=request.url)
293+
294+
# Check for WWW-Authenticate header and extract claims challenge if present
295+
claims_challenge = None
296+
if 'WWW-Authenticate' in response.headers:
297+
www_auth_header = response.headers['WWW-Authenticate']
298+
logger.debug('Received WWW-Authenticate header: %s', www_auth_header)
299+
300+
try:
301+
# Parse the WWW-Authenticate header
302+
parsed = www_authenticate.parse(www_auth_header)
303+
304+
# Extract the claims challenge from bearer params if present
305+
claims_challenge = parsed.get("bearer", {}).get("claims")
306+
if claims_challenge:
307+
logger.debug('Claims challenge extracted: %s', claims_challenge)
308+
except Exception as ex:
309+
# If parsing fails, log the error but continue without claims
310+
logger.debug('Failed to parse WWW-Authenticate header: %s', str(ex))
311+
312+
# Raise authentication error with claims challenge if found
313+
raise AzureDevOpsAuthenticationError(formatted_message, claims_challenge=claims_challenge)
292314
else:
293315
full_message_format = '{error_message}Operation returned a {status_code} status code.'
294316
raise AzureDevOpsClientRequestError(full_message_format.format(error_message=error_message,

azure-devops/azure/devops/exceptions.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,25 @@ class AzureDevOpsClientError(ClientException):
1515

1616

1717
class AzureDevOpsAuthenticationError(AuthenticationError):
18-
pass
18+
"""AzureDevOpsAuthenticationError.
19+
Extends the AuthenticationError to include support for claims challenges.
20+
"""
21+
22+
def __init__(self, message, claims_challenge=None, inner_exception=None):
23+
"""
24+
:param message: The error message.
25+
:param claims_challenge: Optional claims challenge string from the WWW-Authenticate header.
26+
:param inner_exception: Optional inner exception.
27+
"""
28+
super(AzureDevOpsAuthenticationError, self).__init__(message, inner_exception)
29+
self.claims_challenge = claims_challenge
30+
31+
def get_claims_challenge(self):
32+
"""Get the claims challenge value if one exists.
33+
34+
:return: The claims challenge string or None.
35+
"""
36+
return self.claims_challenge
1937

2038

2139
class AzureDevOpsClientRequestError(ClientRequestError):

azure-devops/setup.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
# http://pypi.python.org/pypi/setuptools
1717

1818
REQUIRES = [
19-
"msrest>=0.7.1,<0.8.0"
19+
"msrest>=0.7.1,<0.8.0",
20+
"www-authenticate>=0.9.1",
21+
"msal>=1.20.0"
2022
]
2123

2224
CLASSIFIERS = [

0 commit comments

Comments
 (0)