Coverage for backend/django/authentication/token_exchange.py: 84%

31 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-05-13 02:47 +0000

1import logging 

2 

3import requests 

4from requests.auth import HTTPBasicAuth 

5from django.conf import settings 

6from rest_framework.exceptions import APIException 

7 

8 

9TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange" 

10ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token" 

11logger = logging.getLogger(__name__) 

12 

13 

14def exchange_token_for_excel_delegate(access_token: str) -> str: 

15 """Exchange a user access token for an Excel delegate token. 

16 

17 The token endpoint is called with OAuth token-exchange form fields while 

18 authenticating the client via HTTP Basic auth using the configured client 

19 ID and secret. The requested token must also be limited to the configured 

20 Excel scope so the delegated token can be used by the Excel connection 

21 download flow. 

22 

23 Raises: 

24 APIException: If token exchange settings are missing, the HTTP request 

25 fails, the identity provider returns a non-200 response, or the 

26 response does not contain a usable access token. 

27 """ 

28 token_endpoint = settings.KEYCLOAK_TOKEN_EXCHANGE_ENDPOINT 

29 client_id = settings.KEYCLOAK_TOKEN_EXCHANGE_CLIENT_ID 

30 client_secret = settings.KEYCLOAK_TOKEN_EXCHANGE_CLIENT_SECRET 

31 audience = getattr(settings, "KEYCLOAK_TOKEN_EXCHANGE_AUDIENCE", "") or client_id 

32 excel_scope = getattr(settings, "AUTH_EXCEL_SCOPE_KEY", "") 

33 

34 if not token_endpoint or not client_id or not client_secret or not excel_scope: 

35 raise APIException("Token exchange is not configured.") 

36 

37 payload = { 

38 "audience": audience, 

39 "grant_type": TOKEN_EXCHANGE_GRANT_TYPE, 

40 "subject_token": access_token, 

41 "subject_token_type": ACCESS_TOKEN_TYPE, 

42 "requested_token_type": ACCESS_TOKEN_TYPE, 

43 "scope": excel_scope, 

44 } 

45 

46 try: 

47 response = requests.post( 

48 token_endpoint, 

49 auth=HTTPBasicAuth(client_id, client_secret), 

50 data=payload, 

51 headers={"Content-Type": "application/x-www-form-urlencoded"}, 

52 timeout=settings.KEYCLOAK_TOKEN_EXCHANGE_TIMEOUT_SECONDS, 

53 ) 

54 except requests.RequestException as exc: 

55 raise APIException(f"Token exchange request failed: {exc}") from exc 

56 

57 if response.status_code != 200: 

58 logger.error( 

59 "Excel token exchange failed with status=%s body=%s", 

60 response.status_code, 

61 response.text, 

62 ) 

63 raise APIException(f"Token exchange request failed with status {response.status_code}.") 

64 

65 try: 

66 exchanged = response.json().get("access_token") 

67 except ValueError as exc: 

68 raise APIException("Token exchange returned invalid JSON.") from exc 

69 if not exchanged: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true

70 raise APIException("Token exchange returned no access token.") 

71 

72 return exchanged