Skip to content

Commit 8ca2932

Browse files
committed
WL#16341: OpenID Connect (Oauth2 - JWT) Authentication Support
Change-Id: I34b986b5533a6f9aedf9ba8701498932d17d3048
1 parent 79a9d4c commit 8ca2932

28 files changed

+968
-97
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ v9.1.0
1515
- WL#16444: Drop build support for DEB packages
1616
- WL#16442: Upgrade gssapi version to 1.8.3
1717
- WL#16411: Improve wheel metadata information for Classic and XDevAPI connectors
18+
- WL#16341: OpenID Connect (Oauth2 - JWT) Authentication Support
1819
- WL#16307: Remove Python 3.8 support
1920
- WL#16306: Add support for Python 3.13
2021

mysql-connector-python/lib/mysql/connector/abstracts.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,18 @@
140140
"No supported TLS protocol version found in the 'tls-versions' list '{}'. "
141141
)
142142

143-
KRB_SERVICE_PINCIPAL_ERROR = (
143+
KRB_SERVICE_PRINCIPAL_ERROR = (
144144
'Option "krb_service_principal" {error}, must be a string in the form '
145145
'"primary/instance@realm" e.g "ldap/ldapauth@MYSQL.COM" where "@realm" '
146146
"is optional and if it is not given will be assumed to belong to the "
147147
"default realm, as configured in the krb5.conf file."
148148
)
149149

150+
OPENID_TOKEN_FILE_ERROR = (
151+
'Option "openid_token_file" {error}, it must be a string in the form '
152+
'"path/to/openid/token/file".'
153+
)
154+
150155
MYSQL_PY_TYPES = (
151156
Decimal,
152157
bytes,
@@ -215,6 +220,7 @@ def __init__(self) -> None:
215220
self._oci_config_profile: Optional[str] = None
216221
self._webauthn_callback: Optional[Union[str, Callable[[str], None]]] = None
217222
self._krb_service_principal: Optional[str] = None
223+
self._openid_token_file: Optional[str] = None
218224

219225
self._use_unicode: bool = True
220226
self._get_warnings: bool = False
@@ -724,10 +730,15 @@ def config(self, **kwargs: Any) -> None:
724730
if self._unix_socket and os.name == "posix":
725731
self._ssl_disabled = True
726732

727-
if self._ssl_disabled and self._auth_plugin == "mysql_clear_password":
728-
raise InterfaceError(
729-
"Clear password authentication is not supported over insecure channels"
730-
)
733+
if self._ssl_disabled:
734+
if self._auth_plugin == "mysql_clear_password":
735+
raise InterfaceError(
736+
"Clear password authentication is not supported over insecure channels"
737+
)
738+
if self._auth_plugin == "authentication_openid_connect_client":
739+
raise InterfaceError(
740+
"OpenID Connect authentication is not supported over insecure channels"
741+
)
731742

732743
if set_ssl_flag:
733744
if "verify_cert" not in self._ssl:
@@ -822,22 +833,38 @@ def config(self, **kwargs: Any) -> None:
822833
self._krb_service_principal = config["krb_service_principal"]
823834
if not isinstance(self._krb_service_principal, str):
824835
raise InterfaceError(
825-
KRB_SERVICE_PINCIPAL_ERROR.format(error="is not a string")
836+
KRB_SERVICE_PRINCIPAL_ERROR.format(error="is not a string")
826837
)
827838
if self._krb_service_principal == "":
828839
raise InterfaceError(
829-
KRB_SERVICE_PINCIPAL_ERROR.format(
840+
KRB_SERVICE_PRINCIPAL_ERROR.format(
830841
error="can not be an empty string"
831842
)
832843
)
833844
if "/" not in self._krb_service_principal:
834845
raise InterfaceError(
835-
KRB_SERVICE_PINCIPAL_ERROR.format(error="is incorrectly formatted")
846+
KRB_SERVICE_PRINCIPAL_ERROR.format(error="is incorrectly formatted")
836847
)
837848

838849
if self._webauthn_callback:
839850
self._validate_callable("webauth_callback", self._webauthn_callback, 1)
840851

852+
if config.get("openid_token_file") is not None:
853+
self._openid_token_file = config["openid_token_file"]
854+
if not isinstance(self._openid_token_file, str):
855+
raise InterfaceError(
856+
OPENID_TOKEN_FILE_ERROR.format(error="is not a string")
857+
)
858+
if self._openid_token_file == "":
859+
raise InterfaceError(
860+
OPENID_TOKEN_FILE_ERROR.format(error="cannot be an empty string")
861+
)
862+
if not os.path.exists(self._openid_token_file):
863+
raise InterfaceError(
864+
f"The path '{self._openid_token_file}' provided via 'openid_token_file' "
865+
"does not exist"
866+
)
867+
841868
def _add_default_conn_attrs(self) -> None:
842869
"""Adds the default connection attributes."""
843870

@@ -2036,6 +2063,7 @@ def cmd_change_user(
20362063
password3: str = "",
20372064
oci_config_file: str = "",
20382065
oci_config_profile: str = "",
2066+
openid_token_file: str = "",
20392067
) -> Optional[Dict[str, Any]]:
20402068
"""Changes the current logged in user.
20412069
@@ -2055,6 +2083,7 @@ def cmd_change_user(
20552083
password3: New account's password factor 3.
20562084
oci_config_file: OCI configuration file location (path-like string).
20572085
oci_config_profile: OCI configuration profile location (path-like string).
2086+
openid_token_file: OpenID Connect token file location (path-like string).
20582087
20592088
Returns:
20602089
ok_packet: Dictionary containing the OK packet information.

mysql-connector-python/lib/mysql/connector/aio/abstracts.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@
6565

6666
from ..abstracts import (
6767
DUPLICATED_IN_LIST_ERROR,
68-
KRB_SERVICE_PINCIPAL_ERROR,
68+
KRB_SERVICE_PRINCIPAL_ERROR,
6969
MYSQL_PY_TYPES,
70+
OPENID_TOKEN_FILE_ERROR,
7071
TLS_V1_3_SUPPORTED,
7172
TLS_VER_NO_SUPPORTED,
7273
TLS_VERSION_ERROR,
@@ -183,6 +184,7 @@ def __init__(
183184
raw: bool = False,
184185
kerberos_auth_mode: Optional[str] = None,
185186
krb_service_principal: Optional[str] = None,
187+
openid_token_file: Optional[str] = None,
186188
webauthn_callback: Optional[Union[str, Callable[[str], None]]] = None,
187189
allow_local_infile: bool = DEFAULT_CONFIGURATION["allow_local_infile"],
188190
allow_local_infile_in_path: Optional[str] = DEFAULT_CONFIGURATION[
@@ -260,6 +262,7 @@ def __init__(
260262
self._converter_str_fallback: bool = converter_str_fallback
261263
self._kerberos_auth_mode: Optional[str] = kerberos_auth_mode
262264
self._krb_service_principal: Optional[str] = krb_service_principal
265+
self._openid_token_file: Optional[str] = openid_token_file
263266
self._allow_local_infile: bool = allow_local_infile
264267
self._allow_local_infile_in_path: Optional[str] = allow_local_infile_in_path
265268
self._get_warnings: bool = get_warnings
@@ -329,11 +332,16 @@ def _validate_connection_options(self) -> None:
329332
if self._unix_socket and os.name == "posix":
330333
self._ssl_disabled = True
331334

332-
if self._ssl_disabled and self._auth_plugin == "mysql_clear_password":
333-
raise InterfaceError(
334-
"Clear password authentication is not supported over insecure "
335-
" channels"
336-
)
335+
if self._ssl_disabled:
336+
if self._auth_plugin == "mysql_clear_password":
337+
raise InterfaceError(
338+
"Clear password authentication is not supported over insecure "
339+
" channels"
340+
)
341+
if self._auth_plugin == "authentication_openid_connect_client":
342+
raise InterfaceError(
343+
"OpenID Connect authentication is not supported over insecure channels"
344+
)
337345

338346
if not isinstance(self._port, int):
339347
raise InterfaceError("TCP/IP port number should be an integer")
@@ -414,22 +422,37 @@ def _validate_connection_options(self) -> None:
414422
if self._krb_service_principal:
415423
if not isinstance(self._krb_service_principal, str):
416424
raise InterfaceError(
417-
KRB_SERVICE_PINCIPAL_ERROR.format(error="is not a string")
425+
KRB_SERVICE_PRINCIPAL_ERROR.format(error="is not a string")
418426
)
419427
if self._krb_service_principal == "":
420428
raise InterfaceError(
421-
KRB_SERVICE_PINCIPAL_ERROR.format(
429+
KRB_SERVICE_PRINCIPAL_ERROR.format(
422430
error="can not be an empty string"
423431
)
424432
)
425433
if "/" not in self._krb_service_principal:
426434
raise InterfaceError(
427-
KRB_SERVICE_PINCIPAL_ERROR.format(error="is incorrectly formatted")
435+
KRB_SERVICE_PRINCIPAL_ERROR.format(error="is incorrectly formatted")
428436
)
429437

430438
if self._webauthn_callback:
431439
self._validate_callable("webauth_callback", self._webauthn_callback, 1)
432440

441+
if self._openid_token_file:
442+
if not isinstance(self._openid_token_file, str):
443+
raise InterfaceError(
444+
OPENID_TOKEN_FILE_ERROR.format(error="is not a string")
445+
)
446+
if self._openid_token_file == "":
447+
raise InterfaceError(
448+
OPENID_TOKEN_FILE_ERROR.format(error="cannot be an empty string")
449+
)
450+
if not os.path.exists(self._openid_token_file):
451+
raise InterfaceError(
452+
f"The path '{self._openid_token_file}' provided via 'openid_token_file' "
453+
"does not exist"
454+
)
455+
433456
def _validate_tls_ciphersuites(self) -> None:
434457
"""Validates the tls_ciphersuites option."""
435458
tls_ciphersuites = []
@@ -1569,6 +1592,7 @@ async def cmd_change_user(
15691592
password3: str = "",
15701593
oci_config_file: str = "",
15711594
oci_config_profile: str = "",
1595+
openid_token_file: str = "",
15721596
) -> Optional[OkPacketType]:
15731597
"""Changes the current logged in user.
15741598
@@ -1588,6 +1612,7 @@ async def cmd_change_user(
15881612
password3: New account's password factor 3.
15891613
oci_config_file: OCI configuration file location (path-like string).
15901614
oci_config_profile: OCI configuration profile location (path-like string).
1615+
openid_token_file: OpenID Connect token file location (path-like string).
15911616
15921617
Returns:
15931618
ok_packet: Dictionary containing the OK packet information.

mysql-connector-python/lib/mysql/connector/aio/authentication.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@
3232

3333
__all__ = ["MySQLAuthenticator"]
3434

35-
import copy
36-
3735
from typing import TYPE_CHECKING, Any, Dict, Optional
3836

3937
from ..errors import InterfaceError, NotSupportedError, get_exception
@@ -89,6 +87,10 @@ def plugin_config(self) -> Dict[str, Any]:
8987
"""
9088
return self._plugin_config
9189

90+
def update_plugin_config(self, config: Dict[str, Any]) -> None:
91+
"""Update the 'plugin_config' instance variable"""
92+
self._plugin_config.update(config)
93+
9294
def _switch_auth_strategy(
9395
self,
9496
new_strategy_name: str,
@@ -243,12 +245,12 @@ async def authenticate(
243245
database: Optional[str] = None,
244246
charset: int = DEFAULT_CHARSET_ID,
245247
client_flags: int = 0,
248+
ssl_enabled: bool = False,
246249
max_allowed_packet: int = DEFAULT_MAX_ALLOWED_PACKET,
247250
auth_plugin: Optional[str] = None,
248251
auth_plugin_class: Optional[str] = None,
249252
conn_attrs: Optional[Dict[str, str]] = None,
250253
is_change_user_request: bool = False,
251-
**plugin_config: Any,
252254
) -> bytes:
253255
"""Perform the authentication phase.
254256
@@ -264,15 +266,13 @@ async def authenticate(
264266
database: Initial database name for the connection.
265267
charset: Client charset (see [1]), only the lower 8-bits.
266268
client_flags: Integer representing client capabilities flags.
269+
ssl_enabled: Boolean indicating whether SSL is enabled,
267270
max_allowed_packet: Maximum packet size.
268271
auth_plugin: Authorization plugin name.
269272
auth_plugin_class: Authorization plugin class (has higher precedence
270273
than the authorization plugin name).
271274
conn_attrs: Connection attributes.
272275
is_change_user_request: Whether is a `change user request` operation or not.
273-
plugin_config: Custom configuration to be passed to the auth plugin
274-
when invoked. The parameters defined here will override the
275-
ones defined in the auth plugin itself.
276276
277277
Returns:
278278
ok_packet: OK packet.
@@ -287,7 +287,7 @@ async def authenticate(
287287
# update credentials, plugin config and plugin class
288288
self._username = username
289289
self._passwords = {1: password1, 2: password2, 3: password3}
290-
self._plugin_config = copy.deepcopy(plugin_config)
290+
self._ssl_enabled = ssl_enabled
291291
self._auth_plugin_class = auth_plugin_class
292292

293293
# client's handshake response

mysql-connector-python/lib/mysql/connector/aio/connection.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,17 @@ async def _do_handshake(self) -> None:
242242
self._charset = charsets.get_by_collation(self._charset_collation)
243243

244244
if not self._handshake["capabilities"] & ClientFlag.SSL:
245-
if self._auth_plugin == "mysql_clear_password" and not self.is_secure:
246-
raise InterfaceError(
247-
"Clear password authentication is not supported over "
248-
"insecure channels"
249-
)
245+
if not self.is_secure:
246+
if self._auth_plugin == "mysql_clear_password":
247+
raise InterfaceError(
248+
"Clear password authentication is not supported over "
249+
"insecure channels"
250+
)
251+
if self._auth_plugin == "authentication_openid_connect_client":
252+
raise InterfaceError(
253+
"OpenID Connect authentication is not supported over "
254+
"insecure channels"
255+
)
250256
if self._ssl_verify_cert:
251257
raise InterfaceError(
252258
"SSL is required but the server doesn't support it",
@@ -316,6 +322,17 @@ async def _do_auth(self) -> None:
316322
await self._socket.switch_to_ssl(ssl_context)
317323
self._ssl_active = True
318324

325+
# Add the custom configurations required by specific auth plugins
326+
self._authenticator.update_plugin_config(
327+
config={
328+
"krb_service_principal": self._krb_service_principal,
329+
"oci_config_file": self._oci_config_file,
330+
"oci_config_profile": self._oci_config_profile,
331+
"webauthn_callback": self._webauthn_callback,
332+
"openid_token_file": self._openid_token_file,
333+
}
334+
)
335+
319336
ok_pkt = await self._authenticator.authenticate(
320337
sock=self._socket,
321338
handshake=self._handshake,
@@ -326,13 +343,10 @@ async def _do_auth(self) -> None:
326343
database=self._database,
327344
charset=self._charset.charset_id,
328345
client_flags=self._client_flags,
346+
ssl_enabled=self._ssl_active,
329347
auth_plugin=self._auth_plugin,
330348
auth_plugin_class=self._auth_plugin_class,
331349
conn_attrs=self._connection_attrs,
332-
krb_service_principal=self._krb_service_principal,
333-
oci_config_file=self._oci_config_file,
334-
oci_config_profile=self._oci_config_profile,
335-
webauthn_callback=self._webauthn_callback,
336350
)
337351
self._handle_ok(ok_pkt)
338352

@@ -1297,6 +1311,7 @@ async def cmd_change_user(
12971311
password3: str = "",
12981312
oci_config_file: str = "",
12991313
oci_config_profile: str = "",
1314+
openid_token_file: str = "",
13001315
) -> Optional[OkPacketType]:
13011316
"""Change the current logged in user.
13021317
@@ -1329,9 +1344,19 @@ async def cmd_change_user(
13291344

13301345
if oci_config_file:
13311346
self._oci_config_file = oci_config_file
1332-
1347+
if openid_token_file:
1348+
self._openid_token_file = openid_token_file
13331349
self._oci_config_profile = oci_config_profile
13341350

1351+
# Update the custom configurations needed by specific auth plugins
1352+
self._authenticator.update_plugin_config(
1353+
config={
1354+
"oci_config_file": self._oci_config_file,
1355+
"oci_config_profile": self._oci_config_profile,
1356+
"openid_token_file": self._openid_token_file,
1357+
}
1358+
)
1359+
13351360
ok_packet: bytes = await self._authenticator.authenticate(
13361361
sock=self._socket,
13371362
handshake=self._handshake,
@@ -1345,9 +1370,6 @@ async def cmd_change_user(
13451370
ssl_enabled=self._ssl_active,
13461371
auth_plugin=self._auth_plugin,
13471372
conn_attrs=self._connection_attrs,
1348-
auth_plugin_class=self._auth_plugin_class,
1349-
oci_config_file=self._oci_config_file,
1350-
oci_config_profile=self._oci_config_profile,
13511373
is_change_user_request=True,
13521374
)
13531375

0 commit comments

Comments
 (0)