-
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
b2b3ca0
140512d
dc02d48
7179a9e
6e42887
14ce3d5
5c8bd7c
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,127 @@ | ||
| 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, | ||
| } | ||
| } | ||
|
|
||
| // Credential holds authentication settings for a user. | ||
| // Passwords contains all valid raw passwords for the user. They are hashed on demand during comparison. | ||
| // If empty password authentication is allowed, Passwords must contain an empty string (e.g., []string{""}) | ||
| // rather than being a zero-length slice. A zero-length slice means no valid passwords are configured. | ||
| type Credential struct { | ||
| Passwords []string | ||
| 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
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. @dveeden Do you mean we should add |
||
| 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?
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.
Hm, I'm not sure to understand why you would want to use
hasEmptyPasswordhere.We can't replace
len(c.credential.Passwords) > 0withc.credential.hasEmptyPassword.The only scenario I could imagine would be to make the check more explicit with something like
but then it would be redundant, because if
hasEmptyPassword()returns true, thenlen(Passwords) >)is true as well.What do you have in mind?