Token federation: `_exchange_token` raises KeyError('access_token') on error responses, discarding the real failure reason
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 76/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- python
- Domain
- authentication
Research direction
Start in src/databricks/sql/auth/token_federation.py at _exchange_token around lines 191-194, then trace the handler around line 148 and the wrapping entry point in src/databricks/sql/auth/auth.py:68. Reproduce an exchange that returns an OAuth error body and verify that the warning exposes the endpoint's error reason without logging tokens, while fallback to the external token remains unchanged.
Written by the indexing model from the issue text.
Description
Summary
When token exchange fails, _exchange_token raises KeyError: 'access_token' instead of surfacing the error the endpoint actually returned. The caller catches it and logs the KeyError, so the log line reports the name of a missing dict key rather than why the exchange was rejected:
Token exchange failed, using external token: 'access_token'
There is no way to tell from that whether the exchange was misconfigured, unauthorized, or unsupported.
Versions
Reproduced on databricks-sql-connector==4.3.0. The relevant code is byte-identical in v4.4.0 and on main today, so this is not fixed in a later release.
Where
src/databricks/sql/auth/token_federation.py on main:
191: token_response = json.loads(response.data.decode())
192:
193: return Token(
194: token_response["access_token"], token_response.get("token_type", "Bearer")
195: )
token_type is read defensively with .get(); access_token is not. When the endpoint returns an OAuth error body ({"error": ..., "error_description": ...}) rather than a token, line 194 raises KeyError.
That propagates to the handler at line 148:
147: except Exception as e:
148: logger.warning("Token exchange failed, using external token: %s", e)
str(KeyError("access_token")) renders as 'access_token', which is what reaches the log. The error and error_description from the response body are never read and are lost.
Reproduction
- Connect using an OAuth access token issued by an identity provider whose
isshost differs from the workspace host — for example an Microsoft Entra ID token for the Azure Databricks resource (2ff814a6-3304-4ab8-85cb-cd0e6f879c1d). TokenFederationProviderwraps every provider unconditionally (src/databricks/sql/auth/auth.py:68, "Always wrap with token federation"), and_should_exchange_tokenreturnsTruebecause the issuer host does not match the workspace host, so an exchange is always attempted.- Against a workspace where that exchange is not accepted, every connection logs the message above.
Functionally this is harmless — the fallback to the external token works correctly and queries succeed. The problem is purely diagnostic: the warning fires on every connection and gives no actionable information.
Suggested fix
Read the response defensively and raise something that names the actual failure, without logging the token itself:
token_response = json.loads(response.data.decode())
if "access_token" not in token_response:
error = token_response.get("error", "unknown_error")
description = token_response.get("error_description", "")
raise RuntimeError(f"Token exchange rejected by {token_url}: {error} {description}".strip())
return Token(token_response["access_token"], token_response.get("token_type", "Bearer"))
The existing except Exception at line 147 would then log the endpoint's own reason, and the graceful fallback behaviour is unchanged.
A non-JSON or non-2xx response would also currently surface as a confusing JSONDecodeError; checking the status code before parsing would cover that case too.
- Dominant language
- Python
- Stars
- 236
- Forks
- 162
- Avg merge
- 13h 13m
- Merged PRs (30d)
- 23
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from databricks/databricks-sql-python
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
databricks/databricks-sql-python#1034 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
databricks/databricks-sql-python#1012 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
databricks/databricks-sql-python#865 ·
Maintainers usually reply within 1 day
-
engineer-bot
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
databricks/databricks-sql-python#860 · 3 comments ·
Maintainers usually reply within 1 day
-
Cursor.close() leaks server-side handle for async commands that were never fetchedPossibly taken @nanookclaw claimed this 176 days ago. Openbug engineer-bot
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
databricks/databricks-sql-python#791 · 5 comments ·
Maintainers usually reply within 1 day
All issues in databricks/databricks-sql-python
Similar issues
-
component/fhir type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
OpenConceptLab/ocl_issues#3026 ·
-
ODS datetime imports reject fractional seconds and exports drop themPossibly taken @IlyaKhalafi claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
TauricResearch/TradingAgents#1693 ·
Maintainers usually reply within 2 days
-
requests support
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
UKGovernmentBEIS/inspect_ai#6537 ·
Maintainers usually reply within 1 day