Skip to content

FEAT: Add "token_provider= " parameter for custom Azure Identity credential support - #603

Merged
Jahnvi Thakkar (jahnvi480) merged 36 commits into
mainfrom
jahnvi/custom-credential-support
Aug 5, 2026
Merged

FEAT: Add "token_provider= " parameter for custom Azure Identity credential support#603
Jahnvi Thakkar (jahnvi480) merged 36 commits into
mainfrom
jahnvi/custom-credential-support

Conversation

@jahnvi480

@jahnvi480Jahnvi Thakkar (jahnvi480) commented May 27, 2026

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

AB#45121

GitHub Issue: #577


Summary

This pull request introduces a new token_provider parameter for Microsoft Entra ID (Azure AD) authentication, enabling the use of any credential object with a .get_token(scope) method (such as those from azure-identity). It also adds robust support for custom credential objects, improves token acquisition error handling, and refines identity key computation for connection pooling. Several internal utilities and documentation comments have been added to clarify and harden the authentication process.

Authentication and Token Provider Support:

  • Added a token_provider parameter to connect() and the Connection class, accepting any object with a get_token(scope) method for Microsoft Entra ID authentication (e.g., DefaultAzureCredential, AzureCliCredential, ManagedIdentityCredential). This is mutually exclusive with Authentication= in the connection string and with pre-acquired tokens in attrs_before. Bulk copy operations now re-acquire a fresh token from the provider for each operation.
  • Introduced the TokenProvider protocol to define the expected interface for custom credential objects, ensuring compatibility with both minimal implementations and all azure-identity credentials.

Token Acquisition and Error Handling:

  • Added _get_token_from_credential, acquire_token_from_credential, and acquire_raw_token_from_credential utility functions to centralize token acquisition, handle errors, and log token details, including expiry. These functions provide clear error messages for common mistakes (e.g., async credentials, missing .token attribute) and warn if an already-expired token is returned.
  • Improved documentation and logging for the Azure SQL Database OAuth scope, clarifying that only the Azure commercial cloud is supported via the token_provider path.

Connection Pooling and Identity Key Computation:

  • Refined the logic for computing connection pool identity keys: managed identities now use distinct prefixes (msi:client:<client_id> vs. msi:system), and token-based identities use a SHA-256 hash of the token. The computation is now expiry-aware and avoids collisions between system- and user-assigned identities.
  • Added compute_token_identity to encapsulate the hashing logic for token-based identity keys.

Type Checking and Coverage:

  • Updated .coveragerc to exclude type-checking-only imports from coverage, improving test accuracy.

These changes significantly improve the flexibility, reliability, and clarity of Microsoft Entra ID authentication support in the codebase.

Add a new 'credential' parameter to connect() that accepts any object
following the Azure TokenCredential protocol (.get_token() method).
This allows users to authenticate with any azure-identity credential
class without being limited to the driver's hardcoded credential map.
Changes:
- auth.py: Add _get_token_from_credential() shared helper,
acquire_token_from_credential(), acquire_raw_token_from_credential()
- db_connection.py: Add credential=None parameter to connect()
- connection.py: Validate credential, acquire token, store for
bulk copy token refresh. Mutually exclusive with Authentication=
- cursor.py: Check _custom_credential before _auth_type in bulk copy
- constants.py: Unify _KEY_* constants with _ALLOWED_CONNECTION_STRING_PARAMS
to use single source of truth (_CONNECTION_STRING_*_KEY pattern)
- test_008_auth.py: Add 12 new tests for custom credential flow

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new credential= parameter to the public connection API to support custom Azure Identity (Entra ID) credential objects for token acquisition, and wires that credential through to bulk copy so fresh tokens can be acquired when needed.

Changes:

  • Added credential parameter to connect() / Connection to accept objects implementing .get_token(scope).
  • Implemented credential-based token acquisition helpers in auth.py and integrated credential token usage into Cursor.bulkcopy().
  • Refactored connection-string key constants and added test coverage for the new credential flows.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
tests/test_008_auth.pyAdds unit tests for the new credential token helpers and connect(..., credential=...) behaviors.
mssql_python/db_connection.pyExtends the connect() API to accept and forward the new credential parameter.
mssql_python/cursor.pyUpdates bulk copy to acquire a fresh token from a user-supplied custom credential when present.
mssql_python/constants.pyRefactors connection-string key constants/aliases used by auth/connection code.
mssql_python/connection.pyImplements the new credential parameter behavior (validation, token acquisition, mutual exclusivity with Authentication=).
mssql_python/auth.pyAdds centralized helpers for acquiring raw tokens / ODBC token structs from custom credentials.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadmssql_python/connection.py Outdated
Comment threadmssql_python/db_connection.py
- Strip UID/PWD/Trusted_Connection from connection_str when credential=
is used (same as Authentication= path) to avoid leaking unused secrets
- Add credential= parameter to Connection.__init__ and connect() in
mssql_python.pyi type stubs
The _make_cursor helper uses MagicMock for the connection, which
auto-creates truthy attributes. Without explicitly setting
_custom_credential = None, the bulk copy code takes the custom
credential path instead of the expected _auth_type path.
@github-actions

github-actionsBot commented May 27, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

98%


🎯 Overall Coverage

81%


📈 Total Lines Covered:7357 out of 8973
📁 Project:mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/init.py (100%)
  • mssql_python/auth.py (97.9%): Missing lines 727
  • mssql_python/connection.py (98.1%): Missing lines 84
  • mssql_python/constants.py (100%)
  • mssql_python/cursor.py (100%)
  • mssql_python/db_connection.py (100%)

Summary

  • Total: 117 lines
  • Missing: 2 lines
  • Coverage: 98%

mssql_python/auth.py

Lines 723-731

723frame=frame.f_back724level+=1725# Every frame was internal (not expected in practice); fall back to the726# outermost real frame rather than blaming this helper.
! 727returnmax(level-1, 1)
728729730def_get_token_from_credential(
731credential: "TokenCredential",

mssql_python/connection.py

Lines 80-88

80""" 81  82 def get_token(self, scope: str) -> Any: 83 """Returnanobjectwitha``.token``attributefor``scope``."""
! 84 ...
858687# Add SQL_WMETADATA constant for metadata decoding configuration88SQL_WMETADATA: int=-99# Special flag for column name decoding


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.pybind.ddbc_bindings.h: 59.9%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 76.2%
mssql_python.__init__.py: 77.6%
mssql_python.row.py: 77.6%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection_pool.cpp: 81.4%
mssql_python.pybind.connection.connection.cpp: 83.7%
mssql_python.logging.py: 85.5%

🔗 Quick Links

⚙️ Build Summary📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Rename the public API parameter from 'credential' to 'token_provider'
to reduce ambiguity in our multi-auth-path context. 'credential' could
be confused with SQL auth username/password; 'token_provider' clearly
signals token-based Entra ID auth.
- Rename parameter: credential -> token_provider (connect, Connection)
- Rename internal attr: _custom_credential -> _token_provider
- Update error messages, docstrings, comments, .pyi stubs
- Improve docstring with usage example and explicit guidance
- All 97 tests pass
@jahnvi480Jahnvi Thakkar (jahnvi480) changed the title FEAT: Add "credential= " parameter for custom Azure Identity credential supportFEAT: Add "token_provider= " parameter for custom Azure Identity credential supportMay 29, 2026
@jahnvi480
Jahnvi Thakkar (jahnvi480) marked this pull request as draft June 2, 2026 03:17
…ial-support
# Conflicts:
#	mssql_python/cursor.py
#	tests/test_008_auth.py
…lk-copy token branch
C2: capture token expires_on from custom credential and store on connection. C3: raise DB-API InterfaceError/OperationalError instead of ValueError/TypeError for token_provider misuse and acquisition failures. Add unit tests covering the cursor bulk-copy token_provider branch (success, get_token failure, invalid token).
@github-actionsgithub-actionsBot added pr-size: large Substantial code update and removed pr-size: medium Moderate update size labels Jun 25, 2026
…ol typing; fix docstring error type
- auth.py: type credential params as TokenProvider Protocol; hard-code commercial-cloud scope
- connection.py: warn on ignored UID/PWD/Trusted_Connection when token_provider set; validate get_token arity; document token lifecycle limitations
- db_connection.py: note sovereign clouds out of scope
- test_008_auth.py: cover arity validation and dropped-credential warning
…al collision
The native connection pool keys on the sanitized connection string only, and
the access token lives in attrs_before (applied once on a new physical
connection, never re-applied on reuse). Two different principals sharing the
same Server/Database collapsed into one pool bucket, so one caller could be
handed another's authenticated connection (silent identity confusion).
Fix: Connection.__init__ disables pooling whenever SQL_COPT_SS_ACCESS_TOKEN is
present in attrs_before. One condition covers all access-token paths: raw
attrs_before token, built-in Authentication=ActiveDirectory* (token-injecting),
and token_provider=. Driver-native paths (e.g. ServicePrincipal) keep creds in
the connection string and remain poolable.
Adds regression tests in TestTokenProviderPooling.
- Remove unnecessary f-prefix from two non-interpolated SQL_WCHAR error
strings in connection.py (flagged by flake8-no-fstring-u style linters).
- Type token_provider as Optional[TokenProvider] (was Optional[object]) in
the .pyi stubs for Connection.__init__ and connect(), matching the runtime.
- Drop the 'See docs/DESIGN_TOKEN_PROVIDER_SUPPORT.md' comment in connection.py
(that file is not part of the PR).
- The expired-token warning in _get_token_from_credential is reached via two
call chains at different depths (connect vs bulk-copy), so a fixed stacklevel
cannot point at user code for both. Compute the stacklevel dynamically via
_stacklevel_to_caller(), which walks out of the package to the first external
frame. Works across all supported Python versions.
The .pyi stub is not shipped (not in setup.py package_data) and is misnamed
for a package stub, so type checkers never read it. Real type info for
token_provider comes from inline hints in db_connection.py plus the shipped
py.typed marker. Reverting keeps the PR surgical; ship-vs-delete of the stub
file is tracked as a separate backlog item.
- Re-export TokenProvider from the package root so rom mssql_python import
TokenProvider works for type annotations (added to __all__).
- Normalize MSI client_id (strip {braces}, lowercase) in compute_identity_key
so the same managed identity keys one pool regardless of GUID casing/bracing.
- Downgrade the already-expired-token diagnostic from warnings.warn to
logger.warning so it is never promoted to an exception under -W error.
- Type _token_expires_on as Optional[float] (a custom provider may report a
float POSIX timestamp).
- CHANGELOG: note that NUL characters in connection strings/params are now
rejected with InterfaceError instead of being silently truncated.
Tests updated accordingly (log assertion instead of pytest.warns) plus new
cases for the re-export and client_id normalization.
After merging the Arrow bulk copy feature (#665), _build_pycore_context now
reads self.connection._token_provider before the _auth_type dispatch. The
test_024 _cursor_with_conn helper builds a bare MagicMock connection, which
auto-vivifies a truthy _token_provider and routed every case through the
custom-credential token path -> InterfaceError (12 failures in CI). Real
Connection objects always initialize _token_provider to None; set it
explicitly on the mock so it is faithful.
@jahnvi480
Jahnvi Thakkar (jahnvi480) merged commit e1856e4 into mainAug 5, 2026
29 checks passed
Jahnvi Thakkar (jahnvi480) added a commit that referenced this pull request Aug 6, 2026
[AB#46836](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/46836)
### Summary
`tests/test_008_auth.py` added a top-level `from azure.core.credentials
import TokenCredential` (introduced with the `token_provider` work in
#603). That import runs at module-collection time — before the autouse
`setup_azure_identity` fixture injects its mock `azure.*` modules into
`sys.modules` — so in build/validation stages that install a minimal
dependency set (no `azure-identity`/`azure-core`), pytest aborts the
whole module with:
```
ModuleNotFoundError: No module named 'azure'
tests/test_008_auth.py:32: in <module>
from azure.core.credentials import TokenCredential
```
This wraps only that one top-level import in a `try/except ImportError`
guard (falling back to `TokenCredential = None`) so the module collects
even when `azure-core` is absent. The two tests in
`TestTokenProviderProtocol` that genuinely need the real
`runtime_checkable` Protocol are marked
`@pytest.mark.skipif(TokenCredential is None, ...)`; the third
scope-constant test does not use `TokenCredential` and continues to run.
All other `azure.*` imports in this file are function-local and already
resolve to the fixture's `sys.modules` mocks, so no other changes are
needed. When `azure-core` is installed (the normal path), behavior is
unchanged and both protocol tests run.
No production code is touched — test-only hardening.
@subrata-msSubrata (subrata-ms) mentioned this pull request Aug 6, 2026
Subrata (subrata-ms) added a commit that referenced this pull request Aug 7, 2026
### Work Item / Issue Reference
>
[AB#47087](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/47087)
-------------------------------------------------------------------
### Summary
Release mssql-python v1.13.0.
Version bump to 1.13.0. Updates `mssql_python/__init__.py`, `setup.py`,
`PyPI_Description.md`, and the README "Important Note" section. Bundled
`mssql_py_core` bumped from 0.1.7 to 0.1.8 (no source/API changes —
dev-nightly to stable pin).
#### Enhancements
- **ODBC driver ships exclusively via `mssql-python-odbc` (Phase 2)** —
The `libs/` fallback introduced in v1.12.0 has been removed.
`mssql-python` now hard-depends on `mssql-python-odbc==18.6.2.1`; `pip
install mssql-python` still pulls the driver package transparently.
Smaller wheels; driver binaries managed independently (#693).
- **Apache Arrow bulk copy** — New `Cursor.bulkcopy_arrow(table_name,
source)` method for high-performance bulk loading from `pyarrow.Table` /
`RecordBatch` / Arrow C Data Interface sources; classic `bulkcopy()` now
raises `TypeError` for Arrow inputs and steers users to the new method
(#665).
- **`token_provider=` parameter for Azure Identity credentials** —
`connect()` accepts any credential with a `.get_token(scope)` method
(`DefaultAzureCredential`, `AzureCliCredential`,
`ManagedIdentityCredential`, …). Mutually exclusive with
`Authentication=` in the connection string (#603, issue #577).
- **Identity-aware connection pooling with token-expiry refresh** — Pool
now keys on security context, preventing cross-identity connection
leaks; token acquisition deferred to pool-misses; connections with
near-expiry tokens refreshed automatically (#660, issues #651, #659).
#### Bug Fixes
- **Silent zero-row `executemany` batches on late NULLs** — Fixed
numeric array parameter binding paths
(`TINYINT`/`SMALLINT`/`INT`/`FLOAT`) that left indicator slots
uninitialized when a NULL appeared partway through the batch (#702,
issue #670).
- **`SQL_WVARCHAR` output converter applied as catch-all to non-string
columns** — Fallback now gated on `str`/`bytes` mapped types (#692,
issue #691).
- **Integer-keyed output converters silently never fired** —
`add_output_converter(SQL_DECIMAL, ...)` and other integer SQL type code
keys now dispatch correctly (pyodbc parity) (#690, issue #684).
- **`RecordBatchReader.Close()` for Arrow result sets** —
`Cursor.arrow_reader()` now returns a wrapper whose `.close()` releases
server-side resources and leaves the parent cursor usable (#644, issue
#643).
- **`AttributeError` in `Cursor.__del__` on partially-initialized
cursor** — `__init__` sets `closed`/`hstmt` before any raise; `__del__`
uses correct `sys.is_finalizing()` guard (#646, issue #642).
#### Version Bump
- `mssql_python/__init__.py`: `__version__ = "1.13.0"`
- `setup.py`: `version="1.13.0"`
- `PyPI_Description.md`: `## What's new in v1.13.0` section refreshed
- `README.md`: "Important Note" updated for Phase 2 (no more `libs/`
fallback, `mssql-python-odbc==18.6.2.1`)
Gaurav Sharma (bewithgaurav) pushed a commit that referenced this pull request Aug 11, 2026
resolves the cursor.py conflict from #665, which moved the shared bulkcopy
validation into _bulkcopy_core_and_validate. the timeout fix now lives in that
single helper, so it covers bulkcopy and bulkcopy_arrow at once.
also updates test_024's timeout_non_positive, which asserted the old behaviour,
and adds _token_provider to the new mock cursor for the #603 token_provider path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: largeSubstantial code update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@jahnvi480@sumitmsft@sdebruyn@bewithgaurav@gargsaumya