Skip to content

Commit

Permalink
add support for authentication plugins.
Browse files Browse the repository at this point in the history
  • Loading branch information
craiggwilson committed Mar 17, 2017
1 parent 2e00b5c commit 030d761
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 152 deletions.
127 changes: 127 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package mysql

import "bytes"

const mysqlClearPassword = "mysql_clear_password"
const mysqlNativePassword = "mysql_native_password"
const mysqlOldPassword = "mysql_old_password"
const defaultAuthPluginName = mysqlNativePassword

var authPluginFactories map[string]func(*Config) AuthPlugin

func init() {
authPluginFactories = make(map[string]func(*Config) AuthPlugin)
authPluginFactories[mysqlClearPassword] = func(cfg *Config) AuthPlugin {
return &clearTextPlugin{cfg}
}
authPluginFactories[mysqlNativePassword] = func(cfg *Config) AuthPlugin {
return &nativePasswordPlugin{cfg}
}
authPluginFactories[mysqlOldPassword] = func(cfg *Config) AuthPlugin {
return &oldPasswordPlugin{cfg}
}
}

// RegisterAuthPlugin registers an authentication plugin to be used during
// negotiation with the server. If a plugin with the given name already exists,
// it will be overwritten.
func RegisterAuthPlugin(name string, factory func(*Config) AuthPlugin) {
authPluginFactories[name] = factory
}

// AuthPlugin handles authenticating a user.
type AuthPlugin interface {
// Next takes a server's challenge and returns
// the bytes to send back or an error.
Next(challenge []byte) ([]byte, error)
}

type clearTextPlugin struct {
cfg *Config
}

func (p *clearTextPlugin) Next(challenge []byte) ([]byte, error) {
if !p.cfg.AllowCleartextPasswords {
return nil, ErrCleartextPassword
}

// \0-terminated
return append([]byte(p.cfg.Passwd), 0), nil
}

type nativePasswordPlugin struct {
cfg *Config
}

func (p *nativePasswordPlugin) Next(challenge []byte) ([]byte, error) {
// NOTE: this seems to always be disabled...
// if !p.cfg.AllowNativePasswords {
// return nil, ErrNativePassword
// }

return scramblePassword(challenge, []byte(p.cfg.Passwd)), nil
}

type oldPasswordPlugin struct {
cfg *Config
}

func (p *oldPasswordPlugin) Next(challenge []byte) ([]byte, error) {
if !p.cfg.AllowOldPasswords {
return nil, ErrOldPassword
}

// \0-terminated
return append(scrambleOldPassword(challenge, []byte(p.cfg.Passwd)), 0), nil
}

func handleAuthResult(mc *mysqlConn, plugin AuthPlugin, oldCipher []byte) error {
data, err := mc.readPacket()
if err != nil {
return err
}

var authData []byte

// packet indicator
switch data[0] {
case iOK:
return mc.handleOkPacket(data)

case iEOF: // auth switch
if len(data) > 1 {
pluginEndIndex := bytes.IndexByte(data, 0x00)
pluginName := string(data[1:pluginEndIndex])
if apf, ok := authPluginFactories[pluginName]; ok {
plugin = apf(mc.cfg)
} else {
return ErrUnknownPlugin
}

if len(data) > pluginEndIndex+1 {
authData = data[pluginEndIndex+1 : len(data)-1]
}
} else {
// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::OldAuthSwitchRequest
plugin = authPluginFactories[mysqlOldPassword](mc.cfg)
authData = oldCipher
}
case iAuthContinue:
// continue packet for a plugin.
authData = data[1:] // strip off the continue flag
default: // Error otherwise
return mc.handleErrorPacket(data)
}

authData, err = plugin.Next(authData)
if err != nil {
return err
}

err = mc.writeAuthDataPacket(authData)
if err != nil {
return err
}

return handleAuthResult(mc, plugin, authData)
}
72 changes: 72 additions & 0 deletions auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package mysql

import "testing"
import "bytes"

func TestAuthPlugin_Cleartext(t *testing.T) {
cfg := &Config{
Passwd: "funny",
}

plugin := authPluginFactories[mysqlClearPassword](cfg)

_, err := plugin.Next(nil)
if err == nil {
t.Fatalf("expected error when AllowCleartextPasswords is false")
}

cfg.AllowCleartextPasswords = true

actual, err := plugin.Next(nil)
if err != nil {
t.Fatalf("expected no error but got: %s", err)
}

expected := append([]byte("funny"), 0)
if bytes.Compare(actual, expected) != 0 {
t.Fatalf("expected data to be %v, but got: %v", expected, actual)
}
}

func TestAuthPlugin_NativePassword(t *testing.T) {
cfg := &Config{
Passwd: "pass ",
}

plugin := authPluginFactories[mysqlNativePassword](cfg)

actual, err := plugin.Next([]byte{9, 8, 7, 6, 5, 4, 3, 2})
if err != nil {
t.Fatalf("expected no error but got: %s", err)
}

expected := []byte{195, 146, 3, 213, 111, 95, 252, 192, 97, 226, 173, 176, 91, 175, 131, 138, 89, 45, 75, 179}
if bytes.Compare(actual, expected) != 0 {
t.Fatalf("expected data to be %v, but got: %v", expected, actual)
}
}

func TestAuthPlugin_OldPassword(t *testing.T) {
cfg := &Config{
Passwd: "pass ",
}

plugin := authPluginFactories[mysqlOldPassword](cfg)

_, err := plugin.Next(nil)
if err == nil {
t.Fatalf("expected error when AllowOldPasswords is false")
}

cfg.AllowOldPasswords = true

actual, err := plugin.Next([]byte{9, 8, 7, 6, 5, 4, 3, 2})
if err != nil {
t.Fatalf("expected no error but got: %s", err)
}

expected := []byte{71, 87, 92, 90, 67, 91, 66, 81, 0}
if bytes.Compare(actual, expected) != 0 {
t.Fatalf("expected data to be %v, but got: %v", expected, actual)
}
}
9 changes: 5 additions & 4 deletions const.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ const (
// http://dev.mysql.com/doc/internals/en/client-server-protocol.html

const (
iOK byte = 0x00
iLocalInFile byte = 0xfb
iEOF byte = 0xfe
iERR byte = 0xff
iOK byte = 0x00
iAuthContinue byte = 0x01
iLocalInFile byte = 0xfb
iEOF byte = 0xfe
iERR byte = 0xff
)

// https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags
Expand Down
80 changes: 33 additions & 47 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,50 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
mc.writeTimeout = mc.cfg.WriteTimeout

// Reading Handshake Initialization Packet
cipher, err := mc.readInitPacket()
authPluginName, authData, err := mc.readInitPacket()
if err != nil {
mc.cleanup()
return nil, err
}

// save the old auth data in case the server
// needs to use the old password scheme.
oldCipher := make([]byte, len(authData))
copy(oldCipher, authData)

// Handle pluggable authentication
if authPluginName == "" {
// assume that without a name, we are using
// the default.
authPluginName = defaultAuthPluginName
}

var authPlugin AuthPlugin
if apf, ok := authPluginFactories[authPluginName]; ok {
authPlugin = apf(mc.cfg)
authData, err = authPlugin.Next(authData)
if err != nil {
return nil, err
}
} else {
// we'll tell the server in response that we are switching to our
// default plugin because we didn't recognize the one they sent us.
authPluginName = defaultAuthPluginName
authPlugin = authPluginFactories[authPluginName](mc.cfg)

// zero-out the authData because the current authData was for
// a plugin we don't know about.
authData = make([]byte, 0)
}

// Send Client Authentication Packet
if err = mc.writeAuthPacket(cipher); err != nil {
if err = mc.writeAuthPacket(authPluginName, authData); err != nil {
mc.cleanup()
return nil, err
}

// Handle response to auth packet, switch methods if possible
if err = handleAuthResult(mc, cipher); err != nil {
if err = handleAuthResult(mc, authPlugin, oldCipher); err != nil {
// Authentication failed and MySQL has already closed the connection
// (https://dev.mysql.com/doc/internals/en/authentication-fails.html).
// Do not send COM_QUIT, just cleanup and return the error.
Expand Down Expand Up @@ -134,50 +164,6 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
return mc, nil
}

func handleAuthResult(mc *mysqlConn, oldCipher []byte) error {
// Read Result Packet
cipher, err := mc.readResultOK()
if err == nil {
return nil // auth successful
}

if mc.cfg == nil {
return err // auth failed and retry not possible
}

// Retry auth if configured to do so.
if mc.cfg.AllowOldPasswords && err == ErrOldPassword {
// Retry with old authentication method. Note: there are edge cases
// where this should work but doesn't; this is currently "wontfix":
// https://github.com/go-sql-driver/mysql/issues/184

// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
// sent and we have to keep using the cipher sent in the init packet.
if cipher == nil {
cipher = oldCipher
}

if err = mc.writeOldAuthPacket(cipher); err != nil {
return err
}
_, err = mc.readResultOK()
} else if mc.cfg.AllowCleartextPasswords && err == ErrCleartextPassword {
// Retry with clear text password for
// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
if err = mc.writeClearAuthPacket(); err != nil {
return err
}
_, err = mc.readResultOK()
} else if mc.cfg.AllowNativePasswords && err == ErrNativePassword {
if err = mc.writeNativeAuthPacket(cipher); err != nil {
return err
}
_, err = mc.readResultOK()
}
return err
}

func init() {
sql.Register("mysql", &MySQLDriver{})
}
Loading

0 comments on commit 030d761

Please sign in to comment.