MySQL Blog Archive
For the latest blogs go to blogs.oracle.com/mysql
MySQL Connector/J 2FA and FIDO (WebAuthn)

Support for MySQL FIDO authentication in Java was introduced in MySQL Connector/J 8.0.28 with the implementation of the client-side authentication plugin authentication_fido_client. This implementation is FIDO compliant but we quickly realized we were also interested in supporting some newer FIDO2 features as well. Unfortunately, the original implementation couldn't be extended and we needed to come up with a new authentication plugin—our FIDO2 Web Authentication (WebAuthn) authentication_webauthn and authentication_webauthn_client authentication plugin.

MySQL Connector/J supports both, the now deprecated authentication_fido_client and the new authentication_webauthn_client. The way the driver supports both is almost identical in all aspects with just a few minor differences. This article explains how to use the WebAuthn authentication.

MySQL Connector/J is a JDBC Type 4 driver, where one of the main requirements is to be a 100% pure Java implementation, and there isn't any pure Java library supporting the authentication devices we could use. Sadly, the result is that developers will have to implement the code that handles the interaction with the authentication devices on their own. Don't be discouraged though, as I will show you how to do that.

Creating a MySQL User

Let's assume there is a MySQL server running and configured to support WebAuthn authentication, with the authentication plugin authentication_webauthn loaded and the system variable authentication_webauthn_rp_id properly configured.

Although not always the case, FIDO authentication often works with multifactor authentication, so additional configuration might therefore be necessary but, typically, a default MySQL installation is multifactor authentication ready.

So, let's start by creating the MySQL user we want to link to our FIDO device. We can use the mysql client with a root user.

mysql > CREATE USER 'johndoe'@'%'
          IDENTIFIED WITH caching_sha2_password BY 's3cr3t' AND
          IDENTIFIED WITH authentication_webauthn;
Query OK, 0 rows affected (0,02 sec)

Then we need perform the registration of the FIDO device by the user we just created. This is accomplished by running the mysql client on the same system the device is installed, which might require installing the mysql client in our working machine or moving the FIDO device to the system where the MySQL Server is running. In either case, issue the following command (additional command options to connect to the right server might be needed).

$ mysql --user=johndoe --password1 --register-factor=2
Enter password: <type "s3cr3t">
Please insert FIDO device and follow the instruction. Depending on the device, you may have to perform gesture action multiple times.
1. Perform gesture action (Skip this step if you are prompted to enter device PIN).
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.2.0-commercial MySQL Enterprise Server - Commercial

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql >

And we are done.

Getting 3rd Party Dependencies

As explained above, Connector/J will not be able to interact with authentication devices directly. So we will need a native system library and some Java bindings. JNI or JNA can be used here. I will use JNA for simplicity.

We will need the libfido2 native library, that must be installed in the system where we will be running our application.

We will also use JNA to implement our minimal Java bindings over the libfido2 library.

And, obviously, the MySQL Connector/J driver.

Implementing the Native Bindings

We can start our project by creating a simple class that implements the minimal set of bindings between Java and the libfido2 native library we will need later. As long as the native library is available in your system, you can choose whatever method you prefer to build and resolve all the above Java dependencies. I will just grab the Jar files myself and run all commands manually.

So, here is our bindings class. I named it FidoAssertion.java.

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.PointerType;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;

public class FidoAssertion {

    private interface LibFido2 extends Library {
        public static int FIDO_OK = 0;
        static class FidoAssertT extends PointerType {}
        static class FidoDevInfoT extends PointerType {}
        static class FidoDevT extends PointerType {}
        LibFido2 INSTANCE = Native.load("fido2", LibFido2.class);
        int fido_assert_allow_cred(FidoAssertT assrt, byte[] ptr, int len);
        int fido_assert_authdata_len(FidoAssertT assrt, int idx);
        Pointer fido_assert_authdata_ptr(FidoAssertT assrt, int idx);
        void fido_assert_free(PointerByReference assrt);
        FidoAssertT fido_assert_new();
        int fido_assert_count(FidoAssertT assrt);
        int fido_assert_set_clientdata_hash(FidoAssertT assrt, byte[] ptr, int len);
        int fido_assert_set_rp(FidoAssertT assrt, String id);
        int fido_assert_sig_len(FidoAssertT assrt, int idx);
        Pointer fido_assert_sig_ptr(FidoAssertT assrt, int idx);
        int fido_dev_close(FidoDevT dev);
        void fido_dev_free(PointerByReference dev);
        int fido_dev_get_assert(FidoDevT dev, FidoAssertT assrt, String pin);
        void fido_dev_info_free(PointerByReference devlist, int n);
        int fido_dev_info_manifest(FidoDevInfoT devlist, int ilen, IntByReference olen);
        FidoDevInfoT fido_dev_info_new(int n);
        String fido_dev_info_path(FidoDevInfoT di);
        FidoDevInfoT fido_dev_info_ptr(FidoDevInfoT devList, int size);
        FidoDevT fido_dev_new();
        int fido_dev_open(FidoDevT dev, String path);
        boolean fido_dev_supports_credman(FidoDevT dev);
        void fido_init(int flags);
    }

    private LibFido2.FidoAssertT fidoAssert;
    private LibFido2.FidoDevT fidoDev;
    private byte[] clientDataHash;
    private String relyingPartyId;
    private byte[] credentialId;
    private boolean supportsCredMan = false;

    public FidoAssertion() {
        LibFido2.INSTANCE.fido_init(0);
        initializeFidoDevice();
    }

    private void initializeFidoDevice() {
        LibFido2.FidoDevInfoT fidoDevInfo = LibFido2.INSTANCE.fido_dev_info_new(1);
        IntByReference olen = new IntByReference();
        int r = LibFido2.INSTANCE.fido_dev_info_manifest(fidoDevInfo, 1, olen);
        if (r != LibFido2.FIDO_OK) {
            throw new RuntimeException("Failed locating FIDO devices.");
        }
        LibFido2.FidoDevInfoT dev = LibFido2.INSTANCE.fido_dev_info_ptr(fidoDevInfo, 0);
        String path = LibFido2.INSTANCE.fido_dev_info_path(dev);

        LibFido2.INSTANCE.fido_dev_info_free(new PointerByReference(fidoDevInfo.getPointer()), 1);

        this.fidoDev = LibFido2.INSTANCE.fido_dev_new();
        r = LibFido2.INSTANCE.fido_dev_open(this.fidoDev, path);
        if (r != LibFido2.FIDO_OK) {
            throw new RuntimeException("Failed opening the FIDO device.");
        }

        this.supportsCredMan = LibFido2.INSTANCE.fido_dev_supports_credman(this.fidoDev);
    }

    boolean supportsCredentialManagement() {
        return this.supportsCredMan;
    }

    void setClienDataHash(byte[] clientDataHash) {
        this.clientDataHash = clientDataHash;
    }

    void setRelyingPartyId(String relyingPartyId) {
        this.relyingPartyId = relyingPartyId;
    }

    void setCredentialId(byte[] credentialId) {
        this.credentialId = credentialId;
    }

    void computeAssertions() {
        int r;
        this.fidoAssert = LibFido2.INSTANCE.fido_assert_new();

        // Set the Relying Party Id.
        r = LibFido2.INSTANCE.fido_assert_set_rp(this.fidoAssert, this.relyingPartyId);
        if (r != LibFido2.FIDO_OK) {
            throw new RuntimeException("Failed setting the relying party id.");
        }

        // Set the Client Data Hash.
        r = LibFido2.INSTANCE.fido_assert_set_clientdata_hash(this.fidoAssert, this.clientDataHash, this.clientDataHash.length);
        if (r != LibFido2.FIDO_OK) {
            throw new RuntimeException("Failed setting the client data hash.");
        }

        // Set the Credential Id. Not applicable when resident keys are used.
        if (this.credentialId.length > 0) {
            r = LibFido2.INSTANCE.fido_assert_allow_cred(this.fidoAssert, this.credentialId, this.credentialId.length);
            if (r != LibFido2.FIDO_OK) {
                throw new RuntimeException("Failed setting the credential id.");
            }
        }

        // Obtain the assertion(s) from the FIDO device.
        r = LibFido2.INSTANCE.fido_dev_get_assert(this.fidoDev, this.fidoAssert, null);
        if (r != LibFido2.FIDO_OK) {
            throw new RuntimeException("Failed obtaining the assertion(s) from the FIDO device.");
        }
    }

    public int getAssertCount() {
        int assertCount = LibFido2.INSTANCE.fido_assert_count(this.fidoAssert);
        return assertCount;
    }

    public byte[] getAuthenticatorData(int idx) {
        int authDataLen = LibFido2.INSTANCE.fido_assert_authdata_len(this.fidoAssert, idx);
        Pointer authData = LibFido2.INSTANCE.fido_assert_authdata_ptr(this.fidoAssert, idx);
        byte[] authenticatorData = authData.getByteArray(0, authDataLen);
        return authenticatorData;
    }

    public byte[] getSignature(int idx) {
        int sigLen = LibFido2.INSTANCE.fido_assert_sig_len(this.fidoAssert, idx);
        Pointer sigData = LibFido2.INSTANCE.fido_assert_sig_ptr(this.fidoAssert, idx);
        byte[] signature = sigData.getByteArray(0, sigLen);
        return signature;
    }

    public void freeResources() {
        LibFido2.INSTANCE.fido_dev_close(this.fidoDev);
        LibFido2.INSTANCE.fido_dev_free(new PointerByReference(this.fidoDev.getPointer()));
        LibFido2.INSTANCE.fido_assert_free(new PointerByReference(this.fidoAssert.getPointer()));
    }
}

I won't go into any details about the code above. It will be a good exercise for you to explore what we are doing here. You can consult the libfido2 manuals for all information you need.

We can now compile our class. A Java 8 compiler or above can be used here.

$ javac -classpath *:. FidoAssertion.java

Done! Let's move on.

Implementing the Authentication Callback

MySQL Connector/J uses a pluggable callback class that exchanges data between the authentication process and the interaction with the authentication device. This class must be an instance of the interface com.mysql.cj.callback.MysqlCallbackHandler, which defines one single method: void handle(MysqlCallback cb);. The MysqlCallback argument this method takes is an instance of com.mysql.cj.callback.WebAuthnAuthenticationCallback and it contains all the data required by the FIDO assertion code implemented earlier. Likewise, it also takes the output from the FIDO device (authenticator data and signatures) to the running authentication process.

Here is one possible implementation of the MysqlCallbackHandler.

import com.mysql.cj.callback.MysqlCallback;
import com.mysql.cj.callback.MysqlCallbackHandler;
import com.mysql.cj.callback.WebAuthnAuthenticationCallback;

public class AuthenticationWebAuthnCallbackHandler implements MysqlCallbackHandler {
    @Override
    public void handle(MysqlCallback cb) {
        if (!WebAuthnAuthenticationCallback.class.isAssignableFrom(cb.getClass())) {
            return;
        }

        WebAuthnAuthenticationCallback webAuthnAuthCallback = (WebAuthnAuthenticationCallback) cb;

        FidoAssertion libFido2Assertion = new FidoAssertion();
        webAuthnAuthCallback.setSupportsCredentialManagement(libFido2Assertion.supportsCredentialManagement());

        libFido2Assertion.setClienDataHash(webAuthnAuthCallback.getClientDataHash());
        libFido2Assertion.setRelyingPartyId(webAuthnAuthCallback.getRelyingPartyId());
        libFido2Assertion.setCredentialId(webAuthnAuthCallback.getCredentialId());

        System.out.println("Please perform the gesture action on your FIDO device.");
        libFido2Assertion.computeAssertions();

        for (int i = 0; i < libFido2Assertion.getAssertCount(); i++) {
            webAuthnAuthCallback.addAuthenticatorData(libFido2Assertion.getAuthenticatorData(i));
            webAuthnAuthCallback.addSignature(libFido2Assertion.getSignature(i));
        }

        libFido2Assertion.freeResources();
    }
}

Note how this implementation is responsible for asking the user to perform the gesture action. In a real use case, this would eventually trigger an event that would, for example, open a pop-up message to the user.

We can now compile this code.

$ javac -classpath *:.  AuthenticationWebAuthnCallbackHandler.java

The name of this class will later be supplied to Connector/J through the connection property authenticationWebAuthnCallbackHandler.

Another one down. Let's move on.

Implementing the Application

Finally it is time to implement our client application. The following implementation is just a proof of concept that creates a MySQL connection to the MySQL server with the user created earlier and checks if the connection was established successfully. Note that FIDO authentication requires some sort of human interactions, so this is not a solution to apply for a typical three-tier architecture, where there is usually a single database user configured in the application server and connections to the database are established from a remote machine.

So, here is our simple client application code.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.util.Properties;

import com.mysql.cj.conf.PropertyKey;

public class AuthenticationWebAuthnApp {
    private static final String HOST = "localhost";
    private static final String PORT = "3306";
    private static final String USER = "johndoe";
    private static final String PASS = "s3cr3t";

    public static void main(String[] args) throws Exception {
        Properties props = new Properties();
        props.setProperty(PropertyKey.authenticationWebAuthnCallbackHandler.getKeyName(), AuthenticationWebAuthnCallbackHandler.class.getName());

        String url = "jdbc:mysql://" + USER + ":" + PASS + "@" + HOST + ":" + PORT + "/";

        try (Connection conn = DriverManager.getConnection(url, props)) {
            ResultSet rs = conn.createStatement().executeQuery("SELECT CURRENT_USER()");
            rs.next();
            System.out.println(rs.getString(1) + " AUTHENTICATED SUCCESSFULLY!");
        }
    }
}

Let's compile it just as before.

$ javac -classpath *:.  AuthenticationWebAuthnApp.java

And now we can run it.

$ /usr/lib/jvm/jdk-17/bin/java -classpath *:. AuthenticationWebAuthnApp
Please perform the gesture action on your FIDO device.
johndoe@% AUTHENTICATED SUCCESSFULLY!

Time to sit back, watch and enjoy.

Conclusion

FIDO authentication in not straightforward for MySQL Connector/J, but it is not hard to get it working. We realize that this feature does not fit into most common application architectures where, typically, database users aren't exposed to the end user. So, hopefully, you will find this illustration helpful for cases where this is relevant.

But since a significant part of the authentication process is delegated to the application side, there are many possible ways of integrating the authentication interaction with the client application—we would love to hear from you on any creative solutions.

You can find the MySQL Connector/J development team in #connectors channel in MySQL Community Slack (Sign-up required if you do not have an Oracle account), the MySQL Connector/J, JDBC and Java forum, or you can file a bug in MySQL Bugs Database if you find something not working properly.