Skip to content

Commit 5b1f433

Browse files
committed
WL#14665:SSPI Kerberos authentication for Windows (pure-python)
The purpose of this Worklog is to provide alternative implementation for Kerberos authentication, based on SSPI infrastructure provided natively by Windows OS. This implementation requires win32api python module.
1 parent 8bfdaa6 commit 5b1f433

File tree

4 files changed

+141
-10
lines changed

4 files changed

+141
-10
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ v8.0.29
1515
- WL#14852: Align TLS option checking across connectors
1616
- WL#14824: Remove Python 3.6 support
1717
- WL#14679: Allow custom class for data type conversion in Django backend
18+
- WL#14665: SSPI Kerberos authentication for Windows (pure-python)
1819
- BUG#33729842: Character set 'utf8mb3' support
1920

2021
v8.0.28

lib/mysql/connector/authentication.py

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,18 @@
5050

5151
try:
5252
import gssapi
53-
except:
53+
except ImportError:
5454
gssapi = None
5555

56+
try:
57+
import sspi
58+
import sspicon
59+
import win32api
60+
except ImportError:
61+
sspi = None
62+
sspicon = None
63+
win32api = None
64+
5665
from . import errors
5766
from .utils import (normalize_unicode_string as norm_ustr,
5867
validate_normalized_unicode_string as valid_norm)
@@ -1017,6 +1026,125 @@ def auth_response(self, oci_path=None):
10171026
return auth_response.encode()
10181027

10191028

1029+
class MySQLSSPIKerberosAuthPlugin(BaseAuthPlugin):
1030+
"""Implement the MySQL Kerberos authentication plugin with Windows SSPI"""
1031+
1032+
plugin_name = "authentication_kerberos_client"
1033+
requires_ssl = False
1034+
context = None
1035+
1036+
def _parse_auth_data(self, packet):
1037+
"""Parse authentication data.
1038+
1039+
Get the SPN and REALM from the authentication data packet.
1040+
1041+
Format:
1042+
SPN string length two bytes <B1> <B2> +
1043+
SPN string +
1044+
UPN realm string length two bytes <B1> <B2> +
1045+
UPN realm string
1046+
1047+
Returns:
1048+
tuple: With 'spn' and 'realm'.
1049+
"""
1050+
spn_len = struct.unpack("<H", packet[:2])[0]
1051+
packet = packet[2:]
1052+
1053+
spn = struct.unpack(f"<{spn_len}s", packet[:spn_len])[0]
1054+
packet = packet[spn_len:]
1055+
1056+
realm_len = struct.unpack("<H", packet[:2])[0]
1057+
realm = struct.unpack(f"<{realm_len}s", packet[2:])[0]
1058+
1059+
return spn.decode(), realm.decode()
1060+
1061+
def auth_response(self, auth_data=None):
1062+
"""Prepare the first message to the server."""
1063+
_LOGGER.debug("auth_response for sspi")
1064+
spn = None
1065+
realm = None
1066+
1067+
if auth_data:
1068+
try:
1069+
spn, realm = self._parse_auth_data(auth_data)
1070+
except struct.error as err:
1071+
raise InterruptedError(f"Invalid authentication data: {err}")
1072+
1073+
_LOGGER.debug("Service Principal: %s", spn)
1074+
_LOGGER.debug("Realm: %s", realm)
1075+
_LOGGER.debug("Username: %s", self._username)
1076+
1077+
if sspicon is None or sspi is None:
1078+
raise errors.ProgrammingError(
1079+
'Package "pywin32" (Python for Win32 (pywin32) extensions)'
1080+
' is not installed.')
1081+
1082+
flags = (
1083+
sspicon.ISC_REQ_MUTUAL_AUTH,
1084+
sspicon.ISC_REQ_DELEGATE
1085+
)
1086+
1087+
if self._username and self._password:
1088+
_auth_info = (self._username, realm, self._password)
1089+
else:
1090+
_auth_info = None
1091+
1092+
targetspn = spn
1093+
_LOGGER.debug("targetspn: %s", targetspn)
1094+
_LOGGER.debug("_auth_info is None: %s", _auth_info is None)
1095+
1096+
self.clientauth = sspi.ClientAuth(
1097+
'Kerberos', targetspn=targetspn, auth_info=_auth_info,
1098+
scflags=sum(flags), datarep=sspicon.SECURITY_NETWORK_DREP)
1099+
1100+
try:
1101+
data = None
1102+
err, out_buf = self.clientauth.authorize(data)
1103+
_LOGGER.debug("Context step err: %s", err)
1104+
_LOGGER.debug("Context step out_buf: %s", out_buf)
1105+
_LOGGER.debug("Context completed?: %s", self.clientauth.authenticated)
1106+
initial_client_token = out_buf[0].Buffer
1107+
_LOGGER.debug("pkg_info: %s", self.clientauth.pkg_info)
1108+
except Exception as err:
1109+
raise errors.InterfaceError(
1110+
f"Unable to initiate security context: {err}"
1111+
)
1112+
1113+
_LOGGER.debug("Initial client token: %s", initial_client_token)
1114+
return initial_client_token
1115+
1116+
def auth_continue(self, tgt_auth_challenge):
1117+
"""Continue with the Kerberos TGT service request.
1118+
1119+
With the TGT authentication service given response generate a TGT
1120+
service request. This method must be invoked sequentially (in a loop)
1121+
until the security context is completed and an empty response needs to
1122+
be send to acknowledge the server.
1123+
1124+
Args:
1125+
tgt_auth_challenge: the challenge for the negotiation.
1126+
1127+
Returns:
1128+
tuple (bytearray TGS service request,
1129+
bool True if context is completed otherwise False).
1130+
"""
1131+
_LOGGER.debug("tgt_auth challenge: %s", tgt_auth_challenge)
1132+
1133+
err, out_buf = self.clientauth.authorize(tgt_auth_challenge)
1134+
1135+
_LOGGER.debug("Context step err: %s", err)
1136+
_LOGGER.debug("Context step out_buf: %s", out_buf)
1137+
resp = out_buf[0].Buffer
1138+
_LOGGER.debug("Context step resp: %s", resp)
1139+
_LOGGER.debug("Context completed?: %s", self.clientauth.authenticated)
1140+
1141+
return resp, self.clientauth.authenticated
1142+
1143+
1144+
if os.name == 'nt':
1145+
MySQLKerberosAuthPlugin = MySQLSSPIKerberosAuthPlugin
1146+
1147+
10201148
def get_auth_plugin(plugin_name):
10211149
"""Return authentication class based on plugin name
10221150

lib/mysql/connector/connection.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
from .utils import int1store, int4store, lc_int, get_platform
5959
from .abstracts import MySQLConnectionAbstract
6060

61+
6162
logging.getLogger(__name__).addHandler(logging.NullHandler())
6263

6364
_LOGGER = logging.getLogger(__name__)
@@ -219,11 +220,16 @@ def _do_auth(self, username=None, password=None, database=None,
219220
"# _do_auth(): user: %s", username)
220221
_LOGGER.debug(
221222
"# _do_auth(): self._auth_plugin: %s", self._auth_plugin)
222-
if self._auth_plugin.startswith("authentication_oci") and not username:
223+
if (self._auth_plugin.startswith("authentication_oci") or
224+
(self._auth_plugin.startswith("authentication_kerberos") and
225+
os.name == 'nt')) and not username:
223226
username = getpass.getuser()
224227
_LOGGER.debug(
225-
"MySQL user is empty, OS user: %s will be used for "
226-
"authentication_oci_client", username)
228+
"MySQL user is empty, OS user: %s will be used for %s",
229+
username, self._auth_plugin)
230+
231+
_LOGGER.debug("# _do_auth(): user: %s", username)
232+
_LOGGER.debug("# _do_auth(): password: %s", password)
227233

228234
packet = self._protocol.make_auth(
229235
handshake=self._handshake,
@@ -477,11 +483,7 @@ def _open_connection(self):
477483
478484
Raises on errors.
479485
"""
480-
if self._auth_plugin == "authentication_kerberos_client":
481-
if os.name == "nt":
482-
raise errors.ProgrammingError(
483-
"The Kerberos authentication is not available on Windows"
484-
)
486+
if self._auth_plugin == "authentication_kerberos_client" and os.name != 'nt':
485487
if not self._user:
486488
cls = get_auth_plugin(self._auth_plugin)
487489
self._user = cls.get_user_from_credentials()

tests/test_authentication.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def test_get_auth_plugin(self):
8989
plugin_classes = {}
9090
for name, obj in inspect.getmembers(authentication):
9191
if inspect.isclass(obj) and hasattr(obj, 'plugin_name'):
92-
if obj.plugin_name:
92+
if obj.plugin_name and name != "MySQLSSPIKerberosAuthPlugin":
9393
plugin_classes[obj.plugin_name] = obj
9494
for plugin_name in _STANDARD_PLUGINS:
9595
self.assertEqual(plugin_classes[plugin_name],

0 commit comments

Comments
 (0)