Skip to content

Commit 0374aa2

Browse files
FEAT: Adding authentication module and adding new auth types (#135)
### ADO Work Item Reference <!-- Insert your ADO Work Item ID below (e.g. AB#37452) --> > [AB#37905](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/37905) > [AB#37927](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/37927) > [AB#37926](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/37926) ------------------------------------------------------------------- ### Summary This pull request introduces significant enhancements to the `mssql-python` package, focusing on expanding authentication support and improving connection string handling. The most notable changes include adding support for new Azure Active Directory (AAD) authentication methods, implementing a dedicated authentication module, and integrating these updates into the connection handling logic. ### Authentication Enhancements: * **Expanded AAD Authentication Methods**: The documentation (`README.md`) now reflects support for additional authentication methods, including `ActiveDirectoryInteractive` (via browser), `ActiveDirectoryDeviceCode` (for environments without browser access), and `ActiveDirectoryDefault` (which selects the best method based on the environment). Notes were added to clarify usage and constraints for these methods. * **New `auth.py` Module**: Introduced a dedicated module (`mssql_python/auth.py`) to handle AAD authentication. This module includes: - Support for `DefaultAzureCredential`, `DeviceCodeCredential`, and `InteractiveBrowserCredential` for token retrieval. - Utility functions for processing connection string parameters, removing sensitive data, and generating SQL Server-compatible token structures. ### Connection Handling Updates: * **Integration of Authentication Logic**: The `process_connection_string` function from the new `auth.py` module was integrated into the connection initialization process in `mssql_python/connection.py`. If the connection string specifies an AAD authentication type, it is processed to remove sensitive parameters and include the appropriate authentication token in `attrs_before`. * **Import Update**: The `process_connection_string` function was imported into `mssql_python/connection.py` to enable the integration of the new authentication logic. --------- Co-authored-by: Jahnvi Thakkar <jathakkar@microsoft.com> Co-authored-by: Gaurav Sharma <sharmag@microsoft.com>
1 parent 6e11847 commit 0374aa2

File tree

6 files changed

+417
-2
lines changed

6 files changed

+417
-2
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,23 @@ By adhering to the DB API 2.0 specification, the mssql-python module ensures com
4848

4949
### Support for Microsoft Entra ID Authentication
5050

51-
The Microsoft mssql-python driver enables Python applications to connect to Microsoft SQL Server, Azure SQL Database, or Azure SQL Managed Instance using Microsoft Entra ID identities. It supports various authentication methods, including username and password, Microsoft Entra managed identity, and Integrated Windows Authentication in a federated, domain-joined environment. Additionally, the driver supports Microsoft Entra interactive authentication and Microsoft Entra managed identity authentication for both system-assigned and user-assigned managed identities.
51+
The Microsoft mssql-python driver enables Python applications to connect to Microsoft SQL Server, Azure SQL Database, or Azure SQL Managed Instance using Microsoft Entra ID identities. It supports a variety of authentication methods, including username and password, Microsoft Entra managed identity (system-assigned and user-assigned), Integrated Windows Authentication in a federated, domain-joined environment, interactive authentication via browser, device code flow for environments without browser access, and the default authentication method based on environment and configuration. This flexibility allows developers to choose the most suitable authentication approach for their deployment scenario.
5252

5353
EntraID authentication is now fully supported on MacOS and Linux but with certain limitations as mentioned in the table:
5454

5555
| Authentication Method | Windows Support | macOS/Linux Support | Notes |
5656
|----------------------|----------------|---------------------|-------|
5757
| ActiveDirectoryPassword | ✅ Yes | ✅ Yes | Username/password-based authentication |
58-
| ActiveDirectoryInteractive | ✅ Yes | ❌ No | Only works on Windows |
58+
| ActiveDirectoryInteractive | ✅ Yes | ✅ Yes | Interactive login via browser; requires user interaction |
5959
| ActiveDirectoryMSI (Managed Identity) | ✅ Yes | ✅ Yes | For Azure VMs/containers with managed identity |
6060
| ActiveDirectoryServicePrincipal | ✅ Yes | ✅ Yes | Use client ID and secret or certificate |
6161
| ActiveDirectoryIntegrated | ✅ Yes | ❌ No | Only works on Windows (requires Kerberos/SSPI) |
62+
| ActiveDirectoryDeviceCode | ✅ Yes | ✅ Yes | Device code flow for authentication; suitable for environments without browser access |
63+
| ActiveDirectoryDefault | ✅ Yes | ✅ Yes | Uses default authentication method based on environment and configuration |
64+
65+
**NOTE**: For using Access Token, the connection string *must not* contain `UID`, `PWD`, `Authentication`, or `Trusted_Connection` keywords.
66+
67+
**NOTE**: For using ActiveDirectoryDeviceCode, make sure to specify a `Connect Timeout` that provides enough time to go through the device code flow authentication process.
6268

6369
### Enhanced Pythonic Features
6470

mssql_python/auth.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""
2+
Copyright (c) Microsoft Corporation.
3+
Licensed under the MIT license.
4+
This module handles authentication for the mssql_python package.
5+
"""
6+
7+
import platform
8+
import struct
9+
from typing import Tuple, Dict, Optional, Union
10+
from mssql_python.logging_config import get_logger, ENABLE_LOGGING
11+
from mssql_python.constants import AuthType
12+
13+
logger = get_logger()
14+
15+
class AADAuth:
16+
"""Handles Azure Active Directory authentication"""
17+
18+
@staticmethod
19+
def get_token_struct(token: str) -> bytes:
20+
"""Convert token to SQL Server compatible format"""
21+
token_bytes = token.encode("UTF-16-LE")
22+
return struct.pack(f"<I{len(token_bytes)}s", len(token_bytes), token_bytes)
23+
24+
@staticmethod
25+
def get_token(auth_type: str) -> bytes:
26+
"""Get token using the specified authentication type"""
27+
from azure.identity import (
28+
DefaultAzureCredential,
29+
DeviceCodeCredential,
30+
InteractiveBrowserCredential
31+
)
32+
from azure.core.exceptions import ClientAuthenticationError
33+
34+
# Mapping of auth types to credential classes
35+
credential_map = {
36+
"default": DefaultAzureCredential,
37+
"devicecode": DeviceCodeCredential,
38+
"interactive": InteractiveBrowserCredential,
39+
}
40+
41+
credential_class = credential_map[auth_type]
42+
43+
try:
44+
credential = credential_class()
45+
token = credential.get_token("https://database.windows.net/.default").token
46+
return AADAuth.get_token_struct(token)
47+
except ClientAuthenticationError as e:
48+
# Re-raise with more specific context about Azure AD authentication failure
49+
raise RuntimeError(
50+
f"Azure AD authentication failed for {credential_class.__name__}: {e}. "
51+
f"This could be due to invalid credentials, missing environment variables, "
52+
f"user cancellation, network issues, or unsupported configuration."
53+
) from e
54+
except Exception as e:
55+
# Catch any other unexpected exceptions
56+
raise RuntimeError(f"Failed to create {credential_class.__name__}: {e}") from e
57+
58+
def process_auth_parameters(parameters: list) -> Tuple[list, Optional[str]]:
59+
"""
60+
Process connection parameters and extract authentication type.
61+
62+
Args:
63+
parameters: List of connection string parameters
64+
65+
Returns:
66+
Tuple[list, Optional[str]]: Modified parameters and authentication type
67+
68+
Raises:
69+
ValueError: If an invalid authentication type is provided
70+
"""
71+
modified_parameters = []
72+
auth_type = None
73+
74+
for param in parameters:
75+
param = param.strip()
76+
if not param:
77+
continue
78+
79+
if "=" not in param:
80+
modified_parameters.append(param)
81+
continue
82+
83+
key, value = param.split("=", 1)
84+
key_lower = key.lower()
85+
value_lower = value.lower()
86+
87+
if key_lower == "authentication":
88+
# Check for supported authentication types and set auth_type accordingly
89+
if value_lower == AuthType.INTERACTIVE.value:
90+
# Interactive authentication (browser-based); only append parameter for non-Windows
91+
if platform.system().lower() == "windows":
92+
continue # Skip adding this parameter for Windows
93+
auth_type = "interactive"
94+
elif value_lower == AuthType.DEVICE_CODE.value:
95+
# Device code authentication (for devices without browser)
96+
auth_type = "devicecode"
97+
elif value_lower == AuthType.DEFAULT.value:
98+
# Default authentication (uses DefaultAzureCredential)
99+
auth_type = "default"
100+
modified_parameters.append(param)
101+
102+
return modified_parameters, auth_type
103+
104+
def remove_sensitive_params(parameters: list) -> list:
105+
"""Remove sensitive parameters from connection string"""
106+
exclude_keys = [
107+
"uid=", "pwd=", "encrypt=", "trustservercertificate=", "authentication="
108+
]
109+
return [
110+
param for param in parameters
111+
if not any(param.lower().startswith(exclude) for exclude in exclude_keys)
112+
]
113+
114+
def get_auth_token(auth_type: str) -> Optional[bytes]:
115+
"""Get authentication token based on auth type"""
116+
if not auth_type:
117+
return None
118+
119+
# Handle platform-specific logic for interactive auth
120+
if auth_type == "interactive" and platform.system().lower() == "windows":
121+
return None # Let Windows handle AADInteractive natively
122+
123+
try:
124+
return AADAuth.get_token(auth_type)
125+
except (ValueError, RuntimeError):
126+
return None
127+
128+
def process_connection_string(connection_string: str) -> Tuple[str, Optional[Dict]]:
129+
"""
130+
Process connection string and handle authentication.
131+
132+
Args:
133+
connection_string: The connection string to process
134+
135+
Returns:
136+
Tuple[str, Optional[Dict]]: Processed connection string and attrs_before dict if needed
137+
138+
Raises:
139+
ValueError: If the connection string is invalid or empty
140+
"""
141+
# Check type first
142+
if not isinstance(connection_string, str):
143+
raise ValueError("Connection string must be a string")
144+
145+
# Then check if empty
146+
if not connection_string:
147+
raise ValueError("Connection string cannot be empty")
148+
149+
parameters = connection_string.split(";")
150+
151+
# Validate that there's at least one valid parameter
152+
if not any('=' in param for param in parameters):
153+
raise ValueError("Invalid connection string format")
154+
155+
modified_parameters, auth_type = process_auth_parameters(parameters)
156+
157+
if auth_type:
158+
modified_parameters = remove_sensitive_params(modified_parameters)
159+
token_struct = get_auth_token(auth_type)
160+
if token_struct:
161+
return ";".join(modified_parameters) + ";", {1256: token_struct}
162+
163+
return ";".join(modified_parameters) + ";", None

mssql_python/connection.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
- Cursors are also cleaned up automatically when no longer referenced, to prevent memory leaks.
1212
"""
1313
import weakref
14+
import re
1415
from mssql_python.cursor import Cursor
1516
from mssql_python.logging_config import get_logger, ENABLE_LOGGING
1617
from mssql_python.constants import ConstantsDDBC as ddbc_sql_const
1718
from mssql_python.helpers import add_driver_to_connection_str, check_error
1819
from mssql_python import ddbc_bindings
1920
from mssql_python.pooling import PoolingManager
2021
from mssql_python.exceptions import DatabaseError, InterfaceError
22+
from mssql_python.auth import process_connection_string
2123

2224
logger = get_logger()
2325

@@ -64,6 +66,17 @@ def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_bef
6466
connection_str, **kwargs
6567
)
6668
self._attrs_before = attrs_before or {}
69+
70+
# Check if the connection string contains authentication parameters
71+
# This is important for processing the connection string correctly.
72+
# If authentication is specified, it will be processed to handle
73+
# different authentication types like interactive, device code, etc.
74+
if re.search(r"authentication", self.connection_str, re.IGNORECASE):
75+
connection_result = process_connection_string(self.connection_str)
76+
self.connection_str = connection_result[0]
77+
if connection_result[1]:
78+
self._attrs_before.update(connection_result[1])
79+
6780
self._closed = False
6881

6982
# Using WeakSet which automatically removes cursors when they are no longer in use

mssql_python/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,9 @@ class ConstantsDDBC(Enum):
116116
SQL_C_WCHAR = -8
117117
SQL_NULLABLE = 1
118118
SQL_MAX_NUMERIC_LEN = 16
119+
120+
class AuthType(Enum):
121+
"""Constants for authentication types"""
122+
INTERACTIVE = "activedirectoryinteractive"
123+
DEVICE_CODE = "activedirectorydevicecode"
124+
DEFAULT = "activedirectorydefault"

setup.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ def finalize_options(self):
100100
include_package_data=True,
101101
# Requires >= Python 3.10
102102
python_requires='>=3.10',
103+
# Add dependencies
104+
install_requires=[
105+
'azure-identity>=1.12.0', # Azure authentication library
106+
],
103107
classifiers=[
104108
'Operating System :: Microsoft :: Windows',
105109
'Operating System :: MacOS',

0 commit comments

Comments
 (0)