From 084f9cb6710744f627cb3114d692f5ca26fecdda Mon Sep 17 00:00:00 2001 From: Manfred Riem Date: Thu, 17 Mar 2022 08:42:48 -0600 Subject: [PATCH] Initial import --- mysql/README.md | 32 ++++ mysql/pom.xml | 45 ++++++ .../AzureMySqlMSIAuthenticationPlugin.java | 150 ++++++++++++++++++ mysql/src/test/java/test/JDBCTest.java | 41 +++++ 4 files changed, 268 insertions(+) create mode 100644 mysql/README.md create mode 100644 mysql/pom.xml create mode 100644 mysql/src/main/java/com/azure/mysql/auth/plugin/AzureMySqlMSIAuthenticationPlugin.java create mode 100644 mysql/src/test/java/test/JDBCTest.java diff --git a/mysql/README.md b/mysql/README.md new file mode 100644 index 0000000..8a73959 --- /dev/null +++ b/mysql/README.md @@ -0,0 +1,32 @@ +# README + +## How to run the test? + +You can either run the test using its main method and then you pass in the JDBC URL directly, or if you are running the test using JUnit integration you will have to pass the JDBC URL using the -Durl=xxx syntax. + +## What does the JDBC URL look like? + +``` +jdbc:mysql://hostname:portNumber/databaseName?sslMode=REQUIRED&defaultAuthenticationPlugin=com.azure.mysql.auth.plugin.AzureMySqlMSIAuthenticationPlugin&authenticationPlugins=com.azure.mysql.auth.plugin.AzureMySqlMSIAuthenticationPlugin&user=username +``` + +### Required modifiable properties + +1. `hostname` is the hostname of the MySQL instance +2. `portNumber` is the port number of the MySQL instance +3. `databaseName` is the name of the MySQL database to connect to +4. `username` is the username to connect with (format should be `user@tenant@hostname-only`) + +Note `hostname-only` is the first part of the FQDN hostname. + +### Required fixed properties + +* `sslMode` needs to be set to REQUIRED. +* `defaultAuthenticationPlugin` needs to be set to the implementing class name in this case `com.azure.mysql.auth.plugin.AzureMySqlMSIAuthenticationPlugin` +* `authenticationPlugins` needs to be set to `com.azure.mysql.auth.plugin.AzureMySqlMSIAuthenticationPlugin` + +## Background information + +* https://docs.microsoft.com/en-us/azure/mysql/howto-connect-with-managed-identity +* https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-authentication.html +* https://techcommunity.microsoft.com/t5/azure-database-for-mysql-blog/how-to-connect-to-azure-database-for-mysql-using-managed/ba-p/1518196#:~:text=%20How%20to%20connect%20to%20Azure%20Database%20for,the%20Managed%20Identity%20GUID%20and%20then...%20More%20 diff --git a/mysql/pom.xml b/mysql/pom.xml new file mode 100644 index 0000000..dd6b96a --- /dev/null +++ b/mysql/pom.xml @@ -0,0 +1,45 @@ + + + + 4.0.0 + com.azure.mysql + azure-mysql-auth-plugin + 0.0.1-SNAPSHOT + jar + azure-mysql-auth-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.0 + + 8 + + + + + + + com.azure + azure-identity + 1.4.5 + compile + + + mysql + mysql-connector-java + 8.0.28 + compile + + + junit + junit + 4.13.2 + test + + + + UTF-8 + + diff --git a/mysql/src/main/java/com/azure/mysql/auth/plugin/AzureMySqlMSIAuthenticationPlugin.java b/mysql/src/main/java/com/azure/mysql/auth/plugin/AzureMySqlMSIAuthenticationPlugin.java new file mode 100644 index 0000000..d4f96ef --- /dev/null +++ b/mysql/src/main/java/com/azure/mysql/auth/plugin/AzureMySqlMSIAuthenticationPlugin.java @@ -0,0 +1,150 @@ +package com.azure.mysql.auth.plugin; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; +import com.azure.identity.AzureCliCredentialBuilder; +import com.azure.identity.ChainedTokenCredentialBuilder; +import com.azure.identity.ManagedIdentityCredential; +import com.azure.identity.ManagedIdentityCredentialBuilder; +import com.mysql.cj.callback.MysqlCallbackHandler; +import com.mysql.cj.callback.UsernameCallback; +import com.mysql.cj.protocol.AuthenticationPlugin; +import com.mysql.cj.protocol.Protocol; +import com.mysql.cj.protocol.a.NativeConstants; +import com.mysql.cj.protocol.a.NativePacketPayload; +import java.io.UnsupportedEncodingException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +/** + * The Authentication plugin that enables Azure AD managed identity support. + */ +public class AzureMySqlMSIAuthenticationPlugin implements AuthenticationPlugin { + + /** + * Stores the access token. + */ + private AccessToken accessToken; + + /** + * Stores the callback handler. + */ + private MysqlCallbackHandler callbackHandler; + + /** + * Stores the protocol. + */ + private Protocol protocol; + + @Override + public void destroy() { + } + + @Override + public String getProtocolPluginName() { + return "azure_mysql_msi"; + } + + @Override + public void init(Protocol protocol) { + this.protocol = protocol; + } + + @Override + public void init(Protocol protocol, MysqlCallbackHandler callbackHandler) { + this.init(protocol); + this.callbackHandler = callbackHandler; + } + + @Override + public boolean isReusable() { + return true; + } + + @Override + public boolean nextAuthenticationStep(NativePacketPayload fromServer, + List toServer) { + + /* + * See com.mysql.cj.protocol.a.authentication.MysqlClearPasswordPlugin + */ + toServer.clear(); + NativePacketPayload response; + + if (fromServer == null || accessToken == null || accessToken.isExpired()) { + response = new NativePacketPayload(new byte[0]); + } else if (protocol.getSocketConnection().isSSLEstablished()) { + try { + response = new NativePacketPayload( + accessToken.getToken().getBytes( + protocol.getServerSession() + .getCharsetSettings() + .getPasswordCharacterEncoding())); + response.setPosition(response.getPayloadLength()); + response.writeInteger(NativeConstants.IntegerDataType.INT1, 0); + response.setPosition(0); + } catch (UnsupportedEncodingException uee) { + response = new NativePacketPayload(new byte[0]); + } + } else { + response = new NativePacketPayload(new byte[0]); + } + + toServer.add(response); + return true; + } + + @Override + public boolean requiresConfidentiality() { + return true; + } + + @Override + public void reset() { + accessToken = null; + } + + @Override + public void setAuthenticationParameters(String username, String password) { + + /* + * If username is specified use it as a managed identity (and if it + * fails let the AzureCliCredential have a chance), otherwise assume + * system assigned managed identity. + */ + TokenCredential credential; + + if (username != null) { + ArrayList credentials = new ArrayList<>(); + credentials.add(new ManagedIdentityCredentialBuilder() + .clientId(username).build()); + credentials.add(new AzureCliCredentialBuilder().build()); + credential = new ChainedTokenCredentialBuilder().addAll(credentials).build(); + } else { + credential = new ManagedIdentityCredentialBuilder().build(); + username = ((ManagedIdentityCredential) credential).getClientId(); + } + + /** + * Setup the username callback. + */ + callbackHandler.handle(new UsernameCallback(username)); + + /* + * Setup the access token. + */ + if (username != null) { + TokenRequestContext request = new TokenRequestContext(); + ArrayList scopes = new ArrayList<>(); + scopes.add("https://ossrdbms-aad.database.windows.net"); + request.setScopes(scopes); + accessToken = credential.getToken(request).block(Duration.ofSeconds(30)); + } + } + + @Override + public void setSourceOfAuthData(String sourceOfAuthData) { + } +} diff --git a/mysql/src/test/java/test/JDBCTest.java b/mysql/src/test/java/test/JDBCTest.java new file mode 100644 index 0000000..09c9835 --- /dev/null +++ b/mysql/src/test/java/test/JDBCTest.java @@ -0,0 +1,41 @@ +package test; + +import java.sql.Connection; +import java.sql.DriverManager; +import org.junit.Test; + +/** + * A test that can be used to verify that managed identity is working with your + * MySQL JDBC connection. + */ +public class JDBCTest { + + /** + * Test connection. + */ + @Test + public void testConnection() { + main(new String[]{System.getProperty("url")}); + } + + /** + * Main method. + * + * @param arguments the arguments. + */ + public static void main(String[] arguments) { + Connection connection; + try { + connection = DriverManager.getConnection(arguments[0]); + + if (connection != null) { + System.out.println("Successfully connected."); + System.out.println(connection.isValid(10)); + } else { + System.out.println("Failed to connect."); + } + } catch (Exception e) { + e.printStackTrace(System.err); + } + } +}