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
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
1import logging
3import requests
4from requests.auth import HTTPBasicAuth
5from django.conf import settings
6from rest_framework.exceptions import APIException
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__)
14def exchange_token_for_excel_delegate(access_token: str) -> str:
15 """Exchange a user access token for an Excel delegate token.
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.
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", "")
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.")
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 }
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
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}.")
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.")
72 return exchanged