sidorares / node-mysql2

:zap: fast mysqljs/mysql compatible mysql driver for node.js
https://sidorares.github.io/node-mysql2/
MIT License
3.99k stars 603 forks source link

allow to use arbitrary plugin as first auth method #560

Open sidorares opened 7 years ago

sidorares commented 7 years ago

currently only allowed connect method initially is mysql_native_password. Some servers can potentially prefer to start with custom auth immediately, instead of rejecting mysql_native_password and doing AUTH_SWITH_HANDLER sequence afterwards. Also some servers can be configured to allow 'plugin based auth' but not 'auth switch request' - those are two different capabilities flags

1) respect handshake packet plugin name 2) deprecate authSwitchHandler and rename it to be authPluginHandler 3) provide default handler for mysql_native_password

also need to think of something to make it easy to chain handlers:

const mysqlIamAuth = require('mysql-iam-auth'); // imaginary, does not exist
const mysqMyCustomAuth = require('@internal/customauth');

const pool = mysql2.createPool({
  authPluginHandler: combineAuthHandlers(mysqlIamAuth, mysqMyCustomAuth)
})

ref http://stackoverflow.com/questions/43448563/connecting-to-mariadb-with-nodejs-over-ssl-with-clear-text-password/43450396#43450396

sidorares commented 7 years ago

thinking about merging plugin auth and auth switch. Maybe api like this:

const mysql2 = require('mysql');
const pool = mysql2.createPool({
  authPlugins: {
     sha256_password: (data, cb) => { /* ... probably going to be included by default. You should be able to disable it by specifying  authPlugins: { sha256_password: null } */ },
     mysql_clear_password: mysql2.authPlugins.mysql_clear_password(validateFunction), // not enabled by default but included with driver and documented
  }, 

  // better name for this option? "defaultPlugin" ? "initialConnectionPlugin" ?
  // this is plugin name sent with initial connection (also handler executed at connection time and result of the handler included with initial connection"
  //  error if name does not match authPlugins keys ( or built-in plugins ). Default to "mysql_native_password" if not specified
  connectAuthPluginName: 'mysql_native_password'  // not used if server does not support PLUGIN_AUTH
                                                                               // should it error if specified and no server support for PLUGIN_AUTH ? 
  // some way to handle dynamic plugin names? currently possible with authSwitchHandler but I want to deprecate it
}
normano commented 5 years ago

defaultAuthPlugin sounds like a good name to me. An auth plugin should likely be able to write it's own kinds of packets though or at least find some way for the plugin/function to return the next handler for the command. Considering how the caching_sha2 one appears to want a response to the fullauthentication request and there is also the fast auth path (https://dev.mysql.com/doc/dev/mysql-server/8.0.4/page_caching_sha2_authentication_exchanges.html), you'd want the plugin to handle all success responses until it says it is done (return null).

sidorares commented 5 years ago

it's own kinds of packets though

I think it's ok to assume plugin is only able to send data or null, afaik it's never allowed to send any other packet than AuthMoreData or OK, but there can be possible multiple exchanges and just having function as plugin is not enough, we need instanse of object that updates it state as it progresses from AuthMoreData to final OK

normano commented 5 years ago

I think function that returns the next handler is fine. You as the middle man don't care for state, only what comes next. If I write a plugin then I'll worry about state on my side and know only to return to you what should be called next.

Object is fine too but then you have to figure out how to structure the methods it should call right? Not too big a fan, but done right it will work.

terrisgit commented 4 years ago

Is this stable? Is there a better example? My use case is AWS RDS IAM tokens.

sidorares commented 4 years ago

I believe new plugin auth api is stable. Currently connection is always started with mysql_native_password and later uses plugin via AuthSwitch packets exchange. This issue tracks possible change to allow to connect with custom plugin in the initial auth packet, saving couple of network roundtrips time

Good example in the answer here: https://stackoverflow.com/a/60013378/705115

( I'll copy it here as well with some minor changes )


const iamTokenPlugin =({connection, command}) => (authPluginDataFromServer) => {
   return signer.getAuthToken({
                region: '...',
                hostname: '...',
                port: '...',
                username: '...'
            })
}

mysql.createPool({
    ...,
    ssl: 'Amazon RDS',
    authPlugins: {
        mysql_clear_password: iamTokenPlugin
    }
});

The reason plugin has ({connection, command}) => Buffer => return Promise<Buffer> signature is to allow plugins to have internal state and update it as a result to AuthSwitchMoreData commands.

see how plugin is "instantinated" here https://github.com/sidorares/node-mysql2/blob/ebc2cb438d380d81d57ae6e2d227bfafc684eb2a/lib/commands/auth_switch.js#L50 and later called (potentially many times) with data here https://github.com/sidorares/node-mysql2/blob/ebc2cb438d380d81d57ae6e2d227bfafc684eb2a/lib/commands/auth_switch.js#L79-L82

See mysql AuthSwitch docs here - https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest

terrisgit commented 4 years ago

Amazon has been planning for some time to change their certificates for TLS to RDS, at least for mySQL.

If your RDS server has been upgraded (which Amazon will do in June 2020), when attempting IAM token login, you will get the error:

Error: 139965154551680:error:1425F102:SSL routines:ssl_choose_client_version:unsupported protocol:../deps/openssl/openssl/ssl/statem/statem_lib.c:1929:

If you use NodeJS 12 or lower, start node with --tls-min-v1.0

silverbullettruck2001 commented 2 years ago

@sidorares I am trying to use this with AWS RDS IAM and when I execute it the error that is returned is TypeError: authPlugin is not a function. How can I resolve this? Here is my code:


const mysql = require('mysql2');

// create the connection to database
const pool = mysql.createPool({
  host: 'www.myawsDatabase.com',
  user: 'someUsername',
  database: 'databaseServer',
  port: 3306,
  ssl: 'Amazon RDS',
  authPlugins: {
    mysql_clear_password:
      'mytoken',
  },
});

setInterval(() => {
  for (let i = 0; i < 5; ++i) {
    pool.query((err, db) => {
      console.log(rows, fields);
      // Connection is automatically released once query resolves
    });
  }
}, 1000);

setInterval(() => {
  for (let i = 0; i < 5; ++i) {
    pool.getConnection((err, db) => {
      db.query(
        'SELECT * FROM `randomTable`',
        (err, rows, fields) => {
          console.log(rows, fields);
          db.release();
        }
      );
    });
  }
}, 1000);
terrisgit commented 2 years ago

@silverbullettruck2001 : Provide a function to mysql_clear_password . See the comment above from Feb 20 2020.

silverbullettruck2001 commented 2 years ago

@terrisgit Here is what I put together. I am trying to avoid using the signer library and just provide the raw token I have pre-generated to establish the connection. With the following code it is throwing this error: Uncaught TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received type function ([Function (anonymous)]). Is there a way to accomplish what I am trying to do?


const mysql = require('mysql2');

const iamTokenPlugin =
  ({ connection, command }) =>
  (authPluginDataFromServer) => {
    const myToken = 'myToken';
    return myToken;
  };

// create the connection to database
const pool = mysql.createPool({
  host: 'www.myawsDatabase.com',
  user: 'someUsername',
  database: 'databaseServer',
  port: 3306,
  ssl: 'Amazon RDS',
  authPlugins: {
    mysql_clear_password:  iamTokenPlugin
  },
});

setInterval(() => {
  for (let i = 0; i < 5; ++i) {
    pool.query((err, db) => {
      console.log(rows, fields);
      // Connection is automatically released once query resolves
    });
  }
}, 1000);

setInterval(() => {
  for (let i = 0; i < 5; ++i) {
    pool.getConnection((err, db) => {
      db.query(
        'SELECT * FROM `randomTable`',
        (err, rows, fields) => {
          console.log(rows, fields);
          db.release();
        }
      );
    });
  }
}, 1000);
silverbullettruck2001 commented 2 years ago

@sidorares @terrisgit I made some progress, but still not working...It now errors with: "Connection lost: The server closed the connection.". Any suggestions on how to get this to work appropriately? I have validated that the server is up and that I can connect to using mySQL Workbench using the same connection information and token.

Here's my latest code:


import mysql from 'mysql2';

const iamTokenPlugin =
  ({ connection, command }) =>
  (authPluginDataFromServer) => {
    const myToken =
      'somerandomtoken';
    return Promise.resolve(authPluginDataFromServer.write(myToken));
  };

// create the connection to database
const pool = mysql.createPool({
  host: 'www.myawsDatabase.com',
  user: 'someUsername',
  database: 'databaseServer',
  port: 3306,
  ssl: 'Amazon RDS',
  authPlugins: {
    mysql_clear_password:  iamTokenPlugin
  },
});

pool.getConnection((err, db) => {
  db.query(
    'SELECT * FROM `randomTable`',
    (err, rows, fields) => {
      console.log(err);
      console.log(rows, fields);
      db.release();
    }
  );
});
terrisgit commented 2 years ago

@silverbullettruck2001 please see this mysql2 helper package that supports AWS RDS passwordless connections. Pooling is optional. https://www.npmjs.com/package/@goodware/mysql

silverbullettruck2001 commented 2 years ago

@terrisgit I reviewed this helper package, but it also implements RDS.Signer to generate the token. I would like to not use that library and just pass in a self-generated token. I haven't been able to figure out how to do this though. Do you have any suggestions on how that could be done?