Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AWS.RDS.signer + Connection pooling ? #1017

Closed
rabbitfufu opened this issue Sep 23, 2019 · 15 comments
Closed

AWS.RDS.signer + Connection pooling ? #1017

rabbitfufu opened this issue Sep 23, 2019 · 15 comments

Comments

@rabbitfufu
Copy link

rabbitfufu commented Sep 23, 2019

I am wondering if you have any suggestions for the following situation:

I would like to use AWS.RDS.signer to generate password tokens for use in a connection pool. I have put together something like the following...

const createPool = () => new Promise((resolve, reject) => {
    signer.getAuthToken({
        region: '...',
        hostname: '...',
        port: '...',
        username: '...'
    }, (err, token) => {
        if (err) reject(err)
        const pool = mysql.createPool({
            host: '...',
            port: '...',
            user: '...',
            database: '...',
            password: token,
            ssl: 'Amazon RDS'
            authSwitchHandler: (data, cb) => {
                if (data.pluginName === 'mysql_clear_password') cb(null, Buffer.from(token + '\0'))
            }
        })
        resolve(pool)
    })
})

...and it works great -- but only for 15 minutes, and then the AWS token expires. At that point new connections cannot be established in the pool.

Is there any way to update the pool config options after the pool has been created, so I can keep the token fresh? Would you perhaps consider letting a function be passed as the password property, so that it can return a dynamic value at the time a connection is created? Otherwise, it really isn't practical to use the aws signer with connection pooling, as I understand it.

@sidorares
Copy link
Owner

You can change the order and generate token at the time auth switch request is handled, something like this:

const createPool = () =>
  mysql.createPool({
    host: '...',
    port: '...',
    user: '...',
    database: '...',
    password: token,
    ssl: 'Amazon RDS',
    authSwitchHandler: (data, cb) => {
      if (data.pluginName === 'mysql_clear_password') {
        signer.getAuthToken(
          {
            region: '...',
            hostname: '...',
            port: '...',
            username: '...'
          },
          (err, token) => {
            if (err) {
              cb(err);
            } else {
              cb(null, Buffer.from(token + '\0'));
            }
          }
        );
      } else {
        cb(new Error(`Authentication method ${data.pluginName} is not supported`));
      }
    }
  });

Note that I'm going to deprecate authSwitchHandler api ( initially soft deprecation with warning ) and instead add new api similar to what described in #560 (comment)

future api will look like this (not released yet, just heads up: )

const pool = mysql.createPool({
  host: '...',
  port: '...',
  user: '...',
  database: '...',
  password: token,
  ssl: 'Amazon RDS',
  authPlugins: {
    mysql_clear_password: () => () =>
      signer
        .getAuthToken({
          region: '...',
          hostname: '...',
          port: '...',
          username: '...'
        })
        .promise()
  }
});

@rabbitfufu
Copy link
Author

Thanks for the super fast response, and thanks for the extremely helpful advice! That is exactly what I am looking for. Much appreciated.

@sidorares
Copy link
Owner

Need a bit of cleanup in my code, I left token as password, which is now not defined. Try without password: token, line, let me know if it fails initial connect. What I expect is that initial credentials are not checked at all with handshake as client tries to connect using mysql_native_password method, so server sends AuthSwitchRequest packet saying that it wants mysql_clear_password

@rabbitfufu
Copy link
Author

Yes, you are right -- it works like a charm. (Once the password property is removed.) Thanks again, this is very helpful!

@MelerEcckmanLawler
Copy link

MelerEcckmanLawler commented Mar 10, 2020

Why doesn't it work for me?

message: "Access denied for user 'administrator'@'xx.xxx.xxx.xxx' (using password: YES)",
code: 'ER_ACCESS_DENIED_ERROR',
errno: 1045,
sqlState: '28000',
sqlMessage: "Access denied for user 'administrator'@'xx.xxx.xxx.xxx' (using password: YES)"

I'm using the code shared above:

const createPool = () =>
  mysql.createPool({
    host: '...',
    port: '...',
    user: '...',
    database: '...',
    password: token,
    ssl: 'Amazon RDS',
    authSwitchHandler: (data, cb) => {
      if (data.pluginName === 'mysql_clear_password') {
        signer.getAuthToken(
          {
            region: '...',
            hostname: '...',
            port: '...',
            username: '...'
          },
          (err, token) => {
            if (err) {
              cb(err);
            } else {
              cb(null, Buffer.from(token + '\0'));
            }
          }
        );
      } else {
        cb(new Error(`Authentication method ${data.pluginName} is not supported`));
      }
    }
  });

@buildgreatthings
Copy link

@sidorares Do you have an updated version of the authPlugins code? I tried what you shared above, but it didn't work. authSwitchHandler is deprecated, so I want to avoid using that.

@sidorares
Copy link
Owner

@awcchungster can you add logging and check if auth plugin is called at all? Also maybe try with regular connection, not sure if we pass authPlugins option to PoolConfig correctly

const pool = mysql.createConnection({
  host: '...',
  port: '...',
  user: '...',
  database: '...',
  password: token,
  ssl: 'Amazon RDS',
  authPlugins: {
    mysql_clear_password: () => {
      console.log('mysql_clear_password plugin init');
   return () => {
      console.log('mysql_clear_password plugin get data');
      return signer
        .getAuthToken({
          region: '...',
          hostname: '...',
          port: '...',
          username: '...'
        })
        .promise()
    }
  }
  }
});

@buildgreatthings
Copy link

buildgreatthings commented May 21, 2020

I'm using a pool config (createPool). When I run that code, I get this error. The connection was never successful.

mysql_clear_password plugin init
mysql_clear_password plugin get data
2020-05-21 04:33:23 [info]: Query error: TypeError: signer.getAuthToken(...).promise is not a function, rows: undefined

at Object.authSwitchRequest (/home/cocatalyst/repo/workflow/node_modules/mysql2/lib/commands/auth_switch.js:51:30)
    at ClientHandshake.handshakeResult (/home/cocatalyst/repo/workflow/node_modules/mysql2/lib/commands/client_handshake.js:150:22)
    at ClientHandshake.execute (/home/cocatalyst/repo/workflow/node_modules/mysql2/lib/commands/command.js:39:22)
    at PoolConnection.handlePacket (/home/cocatalyst/repo/workflow/node_modules/mysql2/lib/connection.js:417:32)
    at PacketParser.Connection.packetParser.p [as onPacket] (/home/cocatalyst/repo/workflow/node_modules/mysql2/lib/connection.js:75:12)
    at PacketParser.executeStart (/home/cocatalyst/repo/workflow/node_modules/mysql2/lib/packet_parser.js:75:16)
    at TLSSocket.secureSocket.on.data (/home/cocatalyst/repo/workflow/node_modules/mysql2/lib/connection.js:337:25)
    at TLSSocket.emit (events.js:198:13)
    at TLSSocket.EventEmitter.emit (domain.js:448:20)

@sidorares
Copy link
Owner

@awcchungster what's your version of aws-sdk?

@buildgreatthings
Copy link

I'm on 2.678.0. It's working with your older code format, but shows the deprecation warning.

const createPool = () =>
  mysql.createPool({
    host: '...',
    port: '...',
    user: '...',
    database: '...',
    password: token,
    ssl: 'Amazon RDS',
    authSwitchHandler: (data, cb) => {
      if (data.pluginName === 'mysql_clear_password') {
        signer.getAuthToken(
          {
            region: '...',
            hostname: '...',
            port: '...',
            username: '...'
          },
          (err, token) => {
            if (err) {
              cb(err);
            } else {
              cb(null, Buffer.from(token + '\0'));
            }
          }
        );
      } else {
        cb(new Error(`Authentication method ${data.pluginName} is not supported`));
      }
    }
  });

@sidorares
Copy link
Owner

Probably version you have does not return promise wrapper with .promise(), you can add that yourself:

const pool = mysql.createConnection({
  host: '...',
  port: '...',
  user: '...',
  database: '...',
  password: token,
  ssl: 'Amazon RDS',
  authPlugins: {
    mysql_clear_password: () => {
      console.log('mysql_clear_password plugin init');
   return () => {
      console.log('mysql_clear_password plugin get data');
      return new Promise((accept, reject) => {
         signer.getAuthToken({
          region: '...',
          hostname: '...',
          port: '...',
          username: '...'
        }, (err, token) => {
           if (err) return reject(err)
           return accept(token)
        })
        })
    }
  }
  }
});

@buildgreatthings
Copy link

buildgreatthings commented May 22, 2020

Thanks for your help btw.

Both of the print lines now show up successfully.

authPlugins: {
            mysql_clear_password: () => {
                console.log('mysql_clear_password plugin init');
                return new Promise((accept, reject) => {
                    console.log('mysql_clear_password plugin get data');
                    signer.getAuthToken((err, token) => {
                        if (err)
                            return reject(err)
                        return accept(token)
                    })
                })
            }
        }

However, I received a slightly different error:

Query error: TypeError: connection._authPlugin is not a function, rows: undefined
unhandledRejection TypeError: connection._authPlugin is not a function

@sidorares
Copy link
Owner

sidorares commented May 22, 2020

@awcchungster your example is different from my example above. Plugin signature should be () => () => (Buffer|String)|Promise<Buffer|String> and your is () => Promise<Buffer>

@buildgreatthings
Copy link

That worked! Thanks. I really appreciate it.

It would be great to have this in documentation.

For my own learning, how does return () => { change the response object? Is this a specific structure in ES6?

@sidorares
Copy link
Owner

For my own learning, how does return () => { change the response object? Is this a specific structure in ES6?

return () => { /*...*/ } is almost the same as return function() { /* ... */ }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants