Skip to content

Commit e273589

Browse files
committed
add support for authentication plugins.
1 parent 2e00b5c commit e273589

File tree

6 files changed

+273
-152
lines changed

6 files changed

+273
-152
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Aaron Hopkins <go-sql-driver at die.net>
1515
Arne Hormann <arnehormann at gmail.com>
1616
Carlos Nieto <jose.carlos at menteslibres.net>
1717
Chris Moos <chris at tech9computers.com>
18+
Craig Wilson <[email protected]>
1819
Daniel Nichter <nil at codenode.com>
1920
Daniël van Eeden <git at myname.nl>
2021
DisposaBoy <disposaboy at dby.me>

auth.go

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package mysql
2+
3+
import "bytes"
4+
5+
const (
6+
mysqlClearPassword = "mysql_clear_password"
7+
mysqlNativePassword = "mysql_native_password"
8+
mysqlOldPassword = "mysql_old_password"
9+
defaultAuthPluginName = mysqlNativePassword
10+
)
11+
12+
var authPluginFactories map[string]func(*Config) AuthPlugin
13+
14+
func init() {
15+
authPluginFactories = make(map[string]func(*Config) AuthPlugin)
16+
authPluginFactories[mysqlClearPassword] = func(cfg *Config) AuthPlugin {
17+
return &clearTextPlugin{cfg}
18+
}
19+
authPluginFactories[mysqlNativePassword] = func(cfg *Config) AuthPlugin {
20+
return &nativePasswordPlugin{cfg}
21+
}
22+
authPluginFactories[mysqlOldPassword] = func(cfg *Config) AuthPlugin {
23+
return &oldPasswordPlugin{cfg}
24+
}
25+
}
26+
27+
// RegisterAuthPlugin registers an authentication plugin to be used during
28+
// negotiation with the server. If a plugin with the given name already exists,
29+
// it will be overwritten.
30+
func RegisterAuthPlugin(name string, factory func(*Config) AuthPlugin) {
31+
authPluginFactories[name] = factory
32+
}
33+
34+
// AuthPlugin handles authenticating a user.
35+
type AuthPlugin interface {
36+
// Next takes a server's challenge and returns
37+
// the bytes to send back or an error.
38+
Next(challenge []byte) ([]byte, error)
39+
}
40+
41+
type clearTextPlugin struct {
42+
cfg *Config
43+
}
44+
45+
func (p *clearTextPlugin) Next(challenge []byte) ([]byte, error) {
46+
if !p.cfg.AllowCleartextPasswords {
47+
return nil, ErrCleartextPassword
48+
}
49+
50+
// NUL-terminated
51+
return append([]byte(p.cfg.Passwd), 0), nil
52+
}
53+
54+
type nativePasswordPlugin struct {
55+
cfg *Config
56+
}
57+
58+
func (p *nativePasswordPlugin) Next(challenge []byte) ([]byte, error) {
59+
// NOTE: this seems to always be disabled...
60+
// if !p.cfg.AllowNativePasswords {
61+
// return nil, ErrNativePassword
62+
// }
63+
64+
return scramblePassword(challenge, []byte(p.cfg.Passwd)), nil
65+
}
66+
67+
type oldPasswordPlugin struct {
68+
cfg *Config
69+
}
70+
71+
func (p *oldPasswordPlugin) Next(challenge []byte) ([]byte, error) {
72+
if !p.cfg.AllowOldPasswords {
73+
return nil, ErrOldPassword
74+
}
75+
76+
// NUL-terminated
77+
return append(scrambleOldPassword(challenge, []byte(p.cfg.Passwd)), 0), nil
78+
}
79+
80+
func handleAuthResult(mc *mysqlConn, plugin AuthPlugin, oldCipher []byte) error {
81+
data, err := mc.readPacket()
82+
if err != nil {
83+
return err
84+
}
85+
86+
var authData []byte
87+
88+
// packet indicator
89+
switch data[0] {
90+
case iOK:
91+
return mc.handleOkPacket(data)
92+
93+
case iEOF: // auth switch
94+
if len(data) > 1 {
95+
pluginEndIndex := bytes.IndexByte(data, 0x00)
96+
pluginName := string(data[1:pluginEndIndex])
97+
if apf, ok := authPluginFactories[pluginName]; ok {
98+
plugin = apf(mc.cfg)
99+
} else {
100+
return ErrUnknownPlugin
101+
}
102+
103+
if len(data) > pluginEndIndex+1 {
104+
authData = data[pluginEndIndex+1 : len(data)-1]
105+
}
106+
} else {
107+
// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::OldAuthSwitchRequest
108+
plugin = authPluginFactories[mysqlOldPassword](mc.cfg)
109+
authData = oldCipher
110+
}
111+
case iAuthContinue:
112+
// continue packet for a plugin.
113+
authData = data[1:] // strip off the continue flag
114+
default: // Error otherwise
115+
return mc.handleErrorPacket(data)
116+
}
117+
118+
authData, err = plugin.Next(authData)
119+
if err != nil {
120+
return err
121+
}
122+
123+
err = mc.writeAuthDataPacket(authData)
124+
if err != nil {
125+
return err
126+
}
127+
128+
return handleAuthResult(mc, plugin, authData)
129+
}

auth_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package mysql
2+
3+
import "testing"
4+
import "bytes"
5+
6+
func TestAuthPlugin_Cleartext(t *testing.T) {
7+
cfg := &Config{
8+
Passwd: "funny",
9+
}
10+
11+
plugin := authPluginFactories[mysqlClearPassword](cfg)
12+
13+
_, err := plugin.Next(nil)
14+
if err == nil {
15+
t.Fatalf("expected error when AllowCleartextPasswords is false")
16+
}
17+
18+
cfg.AllowCleartextPasswords = true
19+
20+
actual, err := plugin.Next(nil)
21+
if err != nil {
22+
t.Fatalf("expected no error but got: %s", err)
23+
}
24+
25+
expected := append([]byte("funny"), 0)
26+
if bytes.Compare(actual, expected) != 0 {
27+
t.Fatalf("expected data to be %v, but got: %v", expected, actual)
28+
}
29+
}
30+
31+
func TestAuthPlugin_NativePassword(t *testing.T) {
32+
cfg := &Config{
33+
Passwd: "pass ",
34+
}
35+
36+
plugin := authPluginFactories[mysqlNativePassword](cfg)
37+
38+
actual, err := plugin.Next([]byte{9, 8, 7, 6, 5, 4, 3, 2})
39+
if err != nil {
40+
t.Fatalf("expected no error but got: %s", err)
41+
}
42+
43+
expected := []byte{195, 146, 3, 213, 111, 95, 252, 192, 97, 226, 173, 176, 91, 175, 131, 138, 89, 45, 75, 179}
44+
if bytes.Compare(actual, expected) != 0 {
45+
t.Fatalf("expected data to be %v, but got: %v", expected, actual)
46+
}
47+
}
48+
49+
func TestAuthPlugin_OldPassword(t *testing.T) {
50+
cfg := &Config{
51+
Passwd: "pass ",
52+
}
53+
54+
plugin := authPluginFactories[mysqlOldPassword](cfg)
55+
56+
_, err := plugin.Next(nil)
57+
if err == nil {
58+
t.Fatalf("expected error when AllowOldPasswords is false")
59+
}
60+
61+
cfg.AllowOldPasswords = true
62+
63+
actual, err := plugin.Next([]byte{9, 8, 7, 6, 5, 4, 3, 2})
64+
if err != nil {
65+
t.Fatalf("expected no error but got: %s", err)
66+
}
67+
68+
expected := []byte{71, 87, 92, 90, 67, 91, 66, 81, 0}
69+
if bytes.Compare(actual, expected) != 0 {
70+
t.Fatalf("expected data to be %v, but got: %v", expected, actual)
71+
}
72+
}

const.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ const (
1818
// http://dev.mysql.com/doc/internals/en/client-server-protocol.html
1919

2020
const (
21-
iOK byte = 0x00
22-
iLocalInFile byte = 0xfb
23-
iEOF byte = 0xfe
24-
iERR byte = 0xff
21+
iOK byte = 0x00
22+
iAuthContinue byte = 0x01
23+
iLocalInFile byte = 0xfb
24+
iEOF byte = 0xfe
25+
iERR byte = 0xff
2526
)
2627

2728
// https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags

driver.go

+33-47
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,50 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
8888
mc.writeTimeout = mc.cfg.WriteTimeout
8989

9090
// Reading Handshake Initialization Packet
91-
cipher, err := mc.readInitPacket()
91+
authPluginName, authData, err := mc.readInitPacket()
9292
if err != nil {
9393
mc.cleanup()
9494
return nil, err
9595
}
9696

97+
// save the old auth data in case the server
98+
// needs to use the old password scheme.
99+
oldCipher := make([]byte, len(authData))
100+
copy(oldCipher, authData)
101+
102+
// Handle pluggable authentication
103+
if authPluginName == "" {
104+
// assume that without a name, we are using
105+
// the default.
106+
authPluginName = defaultAuthPluginName
107+
}
108+
109+
var authPlugin AuthPlugin
110+
if apf, ok := authPluginFactories[authPluginName]; ok {
111+
authPlugin = apf(mc.cfg)
112+
authData, err = authPlugin.Next(authData)
113+
if err != nil {
114+
return nil, err
115+
}
116+
} else {
117+
// we'll tell the server in response that we are switching to our
118+
// default plugin because we didn't recognize the one they sent us.
119+
authPluginName = defaultAuthPluginName
120+
authPlugin = authPluginFactories[authPluginName](mc.cfg)
121+
122+
// zero-out the authData because the current authData was for
123+
// a plugin we don't know about.
124+
authData = make([]byte, 0)
125+
}
126+
97127
// Send Client Authentication Packet
98-
if err = mc.writeAuthPacket(cipher); err != nil {
128+
if err = mc.writeAuthPacket(authPluginName, authData); err != nil {
99129
mc.cleanup()
100130
return nil, err
101131
}
102132

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

137-
func handleAuthResult(mc *mysqlConn, oldCipher []byte) error {
138-
// Read Result Packet
139-
cipher, err := mc.readResultOK()
140-
if err == nil {
141-
return nil // auth successful
142-
}
143-
144-
if mc.cfg == nil {
145-
return err // auth failed and retry not possible
146-
}
147-
148-
// Retry auth if configured to do so.
149-
if mc.cfg.AllowOldPasswords && err == ErrOldPassword {
150-
// Retry with old authentication method. Note: there are edge cases
151-
// where this should work but doesn't; this is currently "wontfix":
152-
// https://github.com/go-sql-driver/mysql/issues/184
153-
154-
// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
155-
// sent and we have to keep using the cipher sent in the init packet.
156-
if cipher == nil {
157-
cipher = oldCipher
158-
}
159-
160-
if err = mc.writeOldAuthPacket(cipher); err != nil {
161-
return err
162-
}
163-
_, err = mc.readResultOK()
164-
} else if mc.cfg.AllowCleartextPasswords && err == ErrCleartextPassword {
165-
// Retry with clear text password for
166-
// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
167-
// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
168-
if err = mc.writeClearAuthPacket(); err != nil {
169-
return err
170-
}
171-
_, err = mc.readResultOK()
172-
} else if mc.cfg.AllowNativePasswords && err == ErrNativePassword {
173-
if err = mc.writeNativeAuthPacket(cipher); err != nil {
174-
return err
175-
}
176-
_, err = mc.readResultOK()
177-
}
178-
return err
179-
}
180-
181167
func init() {
182168
sql.Register("mysql", &MySQLDriver{})
183169
}

0 commit comments

Comments
 (0)