-
Notifications
You must be signed in to change notification settings - Fork 1k
Implement AuthenticationHandler for custom auth mechanisms
#1072
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
base: master
Are you sure you want to change the base?
Changes from all commits
b2813fa
84e9fea
4e09810
63d5f38
dba2caf
12a0543
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 |
|---|---|---|
|
|
@@ -30,15 +30,15 @@ func (c *Conn) compareAuthData(authPluginName string, clientAuthData []byte) err | |
| return c.serverConf.authProvider.Authenticate(c, authPluginName, clientAuthData) | ||
| } | ||
|
|
||
| func (c *Conn) acquirePassword() error { | ||
| if c.credential.Password != "" { | ||
| func (c *Conn) acquireCredential() error { | ||
| if len(c.credential.Passwords) > 0 { | ||
| return nil | ||
| } | ||
| credential, found, err := c.credentialProvider.GetCredential(c.user) | ||
| credential, found, err := c.authHandler.GetCredential(c.user) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !found { | ||
| if !found || len(credential.Passwords) == 0 { | ||
|
Collaborator
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. We should add comments to |
||
| return mysql.NewDefaultError(mysql.ER_NO_SUCH_USER, c.user, c.RemoteAddr().String()) | ||
| } | ||
| c.credential = credential | ||
|
|
@@ -67,26 +67,32 @@ func scrambleValidation(cached, nonce, scramble []byte) bool { | |
|
|
||
| func (c *Conn) compareNativePasswordAuthData(clientAuthData []byte, credential Credential) error { | ||
| if len(clientAuthData) == 0 { | ||
| if credential.Password == "" { | ||
| if credential.hasEmptyPassword() { | ||
| return nil | ||
| } | ||
| return ErrAccessDeniedNoPassword | ||
| } | ||
|
|
||
| password, err := mysql.DecodePasswordHex(c.credential.Password) | ||
| if err != nil { | ||
| return ErrAccessDenied | ||
| } | ||
| if mysql.CompareNativePassword(clientAuthData, password, c.salt) { | ||
| return nil | ||
| for _, password := range credential.Passwords { | ||
| hash, err := credential.hashPassword(password) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| decoded, err := mysql.DecodePasswordHex(hash) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| if mysql.CompareNativePassword(clientAuthData, decoded, c.salt) { | ||
| return nil | ||
| } | ||
| } | ||
| return ErrAccessDenied | ||
| } | ||
|
|
||
| func (c *Conn) compareSha256PasswordAuthData(clientAuthData []byte, credential Credential) error { | ||
| // Empty passwords are not hashed, but sent as empty string | ||
| if len(clientAuthData) == 0 { | ||
| if credential.Password == "" { | ||
| if credential.hasEmptyPassword() { | ||
| return nil | ||
| } | ||
| return ErrAccessDeniedNoPassword | ||
|
|
@@ -112,20 +118,26 @@ func (c *Conn) compareSha256PasswordAuthData(clientAuthData []byte, credential C | |
| clientAuthData = clientAuthData[:l-1] | ||
| } | ||
| } | ||
| check, err := mysql.Check256HashingPassword([]byte(credential.Password), string(clientAuthData)) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if check { | ||
| return nil | ||
| for _, password := range credential.Passwords { | ||
| hash, err := credential.hashPassword(password) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| check, err := mysql.Check256HashingPassword([]byte(hash), string(clientAuthData)) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| if check { | ||
| return nil | ||
| } | ||
| } | ||
| return ErrAccessDenied | ||
| } | ||
|
|
||
| func (c *Conn) compareCacheSha2PasswordAuthData(clientAuthData []byte) error { | ||
| // Empty passwords are not hashed, but sent as empty string | ||
| if len(clientAuthData) == 0 { | ||
| if c.credential.Password == "" { | ||
| if c.credential.hasEmptyPassword() { | ||
| return nil | ||
| } | ||
| return ErrAccessDeniedNoPassword | ||
|
|
@@ -139,10 +151,8 @@ func (c *Conn) compareCacheSha2PasswordAuthData(clientAuthData []byte) error { | |
| // 'fast' auth: write "More data" packet (first byte == 0x01) with the second byte = 0x03 | ||
| return c.writeAuthMoreDataFastAuth() | ||
| } | ||
|
|
||
| return ErrAccessDenied | ||
| } | ||
| // cache miss, do full auth | ||
| // cache miss or validation failed, do full auth | ||
| if err := c.writeAuthMoreDataFullAuth(); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| package server | ||
|
|
||
| import ( | ||
| "slices" | ||
| "sync" | ||
|
|
||
| "github.com/go-mysql-org/go-mysql/mysql" | ||
| "github.com/pingcap/errors" | ||
| "github.com/pingcap/tidb/pkg/parser/auth" | ||
| ) | ||
|
|
||
| // AuthenticationHandler provides user credentials and authentication lifecycle hooks. | ||
| // | ||
| // # Important Note | ||
| // | ||
| // if the password in a third-party auth handler could be updated at runtime, we have to invalidate the caching | ||
| // for 'caching_sha2_password' by calling 'func (s *Server)InvalidateCache(string, string)'. | ||
| type AuthenticationHandler interface { | ||
| // GetCredential returns the user credential (supports multiple valid passwords per user). | ||
| // Implementations must be safe for concurrent use. | ||
| GetCredential(username string) (credential Credential, found bool, err error) | ||
|
|
||
| // OnAuthSuccess is called after successful authentication, before the OK packet. | ||
| // Return an error to reject the connection (error will be sent to client instead of OK). | ||
| // Return nil to proceed with sending the OK packet. | ||
| OnAuthSuccess(conn *Conn) error | ||
|
|
||
| // OnAuthFailure is called after authentication fails, before the error packet. | ||
| // This is informational only - the connection will be closed regardless. | ||
| OnAuthFailure(conn *Conn, err error) | ||
| } | ||
|
|
||
| func NewInMemoryAuthenticationHandler(defaultAuthMethod ...string) *InMemoryAuthenticationHandler { | ||
| d := mysql.AUTH_CACHING_SHA2_PASSWORD | ||
| if len(defaultAuthMethod) > 0 { | ||
| d = defaultAuthMethod[0] | ||
| } | ||
| return &InMemoryAuthenticationHandler{ | ||
| userPool: sync.Map{}, | ||
| defaultAuthMethod: d, | ||
| } | ||
| } | ||
|
|
||
| type Credential struct { | ||
| Passwords []string // raw passwords, hashed on demand during comparison | ||
| AuthPluginName string | ||
| } | ||
|
|
||
| // hashPassword computes the password hash for a given password using the credential's auth plugin. | ||
| func (c Credential) hashPassword(password string) (string, error) { | ||
| if password == "" { | ||
| return "", nil | ||
| } | ||
|
|
||
| switch c.AuthPluginName { | ||
| case mysql.AUTH_NATIVE_PASSWORD: | ||
| return mysql.EncodePasswordHex(mysql.NativePasswordHash([]byte(password))), nil | ||
|
|
||
| case mysql.AUTH_CACHING_SHA2_PASSWORD: | ||
| return auth.NewHashPassword(password, mysql.AUTH_CACHING_SHA2_PASSWORD), nil | ||
|
|
||
| case mysql.AUTH_SHA256_PASSWORD: | ||
| return mysql.NewSha256PasswordHash(password) | ||
|
|
||
| case mysql.AUTH_CLEAR_PASSWORD: | ||
| return password, nil | ||
|
|
||
| default: | ||
| return "", errors.Errorf("unknown authentication plugin name '%s'", c.AuthPluginName) | ||
| } | ||
| } | ||
|
|
||
| // hasEmptyPassword returns true if any password in the credential is empty. | ||
| func (c Credential) hasEmptyPassword() bool { | ||
| return slices.Contains(c.Passwords, "") | ||
| } | ||
|
|
||
| // InMemoryAuthenticationHandler implements AuthenticationHandler with in-memory credential storage. | ||
| type InMemoryAuthenticationHandler struct { | ||
| userPool sync.Map // username -> Credential | ||
| defaultAuthMethod string | ||
| } | ||
|
|
||
| func (h *InMemoryAuthenticationHandler) CheckUsername(username string) (found bool, err error) { | ||
| _, ok := h.userPool.Load(username) | ||
| return ok, nil | ||
| } | ||
|
|
||
| func (h *InMemoryAuthenticationHandler) GetCredential(username string) (credential Credential, found bool, err error) { | ||
| v, ok := h.userPool.Load(username) | ||
ramnes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if !ok { | ||
| return Credential{}, false, nil | ||
| } | ||
| c, valid := v.(Credential) | ||
| if !valid { | ||
| return Credential{}, true, errors.Errorf("invalid credential") | ||
| } | ||
| return c, true, nil | ||
| } | ||
|
|
||
| func (h *InMemoryAuthenticationHandler) AddUser(username, password string, optionalAuthPluginName ...string) error { | ||
| authPluginName := h.defaultAuthMethod | ||
| if len(optionalAuthPluginName) > 0 { | ||
| authPluginName = optionalAuthPluginName[0] | ||
| } | ||
|
|
||
| if !isAuthMethodSupported(authPluginName) { | ||
|
Collaborator
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. seems in the old code we also support AUTH_CLEAR_PASSWORD. @dveeden Should we change
Contributor
Author
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 can also remove this check entirely if it doesn't make sense in that context, as there was no check before. Feel free to tell me.
Collaborator
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 think it's better to remove the check. And let's wait @dveeden until all comments are resolved to understand if
Collaborator
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 think we probably should support Note that this is a client side plugin. The usecase for this is to have the client send the cleartext password to the server which then allows the server to use this to authenticate against LDAP (via binding, a hash stored in ldap doesn't need this) or via PAM or anything else that is custom. Note that on the client one needs to use The risk with cleartext is obvious. It should be used only over TLS connections. Note that
With a RSA keypair the public key has to be specified or you have to enable an option to fetch it from the server (which is insecure). I don't think this should be used. With |
||
| return errors.Errorf("unknown authentication plugin name '%s'", authPluginName) | ||
| } | ||
|
|
||
| h.userPool.Store(username, Credential{ | ||
| Passwords: []string{password}, | ||
| AuthPluginName: authPluginName, | ||
| }) | ||
| return nil | ||
| } | ||
|
|
||
| func (h *InMemoryAuthenticationHandler) OnAuthSuccess(conn *Conn) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (h *InMemoryAuthenticationHandler) OnAuthFailure(conn *Conn, err error) { | ||
| } | ||
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.
Seems we can also use hasEmptyPassword here?