-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
add support for authentication plugins. #552
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package mysql | ||
|
||
import "bytes" | ||
|
||
const ( | ||
mysqlClearPassword = "mysql_clear_password" | ||
mysqlNativePassword = "mysql_native_password" | ||
mysqlOldPassword = "mysql_old_password" | ||
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) | ||
|
||
// Close cleans up the resources of the plugin. | ||
Close() | ||
} | ||
|
||
type clearTextPlugin struct { | ||
cfg *Config | ||
} | ||
|
||
func (p *clearTextPlugin) Next(challenge []byte) ([]byte, error) { | ||
if !p.cfg.AllowCleartextPasswords { | ||
return nil, ErrCleartextPassword | ||
} | ||
|
||
// NUL-terminated | ||
return append([]byte(p.cfg.Passwd), 0), nil | ||
} | ||
|
||
func (p *clearTextPlugin) Close() {} | ||
|
||
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 | ||
} | ||
|
||
func (p *nativePasswordPlugin) Close() {} | ||
|
||
type oldPasswordPlugin struct { | ||
cfg *Config | ||
} | ||
|
||
func (p *oldPasswordPlugin) Next(challenge []byte) ([]byte, error) { | ||
if !p.cfg.AllowOldPasswords { | ||
return nil, ErrOldPassword | ||
} | ||
|
||
// NUL-terminated | ||
return append(scrambleOldPassword(challenge, []byte(p.cfg.Passwd)), 0), nil | ||
} | ||
|
||
func (p *oldPasswordPlugin) Close() {} | ||
|
||
func handleAuthResult(mc *mysqlConn, 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 | ||
mc.authPlugin.Close() | ||
if len(data) > 1 { | ||
pluginEndIndex := bytes.IndexByte(data, 0x00) | ||
pluginName := string(data[1:pluginEndIndex]) | ||
if apf, ok := authPluginFactories[pluginName]; ok { | ||
mc.authPlugin = 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 | ||
mc.authPlugin = 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 = mc.authPlugin.Next(authData) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = mc.writeAuthDataPacket(authData) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return handleAuthResult(mc, authData) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like recursion. Please loop outside of this function. |
||
} |
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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ type mysqlConn struct { | |
sequence uint8 | ||
parseTime bool | ||
strict bool | ||
authPlugin AuthPlugin | ||
} | ||
|
||
// Handles parameters set in DSN after the connection is established | ||
|
@@ -92,6 +93,10 @@ func (mc *mysqlConn) Close() (err error) { | |
// closed the network connection. | ||
func (mc *mysqlConn) cleanup() { | ||
// Makes cleanup idempotent | ||
if mc.authPlugin != nil { | ||
mc.authPlugin.Close() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The auth plugin is not used again after the connection is established and can therefore be closed in the init phase already, I believe. We also don't have to add it as a field to |
||
mc.authPlugin = nil | ||
} | ||
if mc.netConn != nil { | ||
if err := mc.netConn.Close(); err != nil { | ||
errLog.Print(err) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this called
Next
and not e.g.Auth
(which I believe is what this does)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is called next because it might be called more than once. All the current plugins, clear, native, and old all are single step. Next is called once and they are complete. However, not all plugins will be this way, so calling it Auth and having it called multiple times is weird (to me). Precedent was pulled from Java's driver, which calls it Next as well. However, I'm certainly not tied to that name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense. Maybe also add a short comment to the code that it might be called more than once.