From 04a3a2e7b60faeca39a42b05ad5d2b291fa4ed4d Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sat, 21 Oct 2023 12:52:27 -0700 Subject: [PATCH] clientv3: allow setting JWT directly etcd supports using signed JWTs in a verify-only mode where the server has access to only a public key and therefore can not create tokens but can validate them. For this to work a client must not call Authenticate and must instead submit a pre-signed JWT with their request. The server will validate this token, extract the username from it, and may allow the client access. This change allows setting the JWT directly and not setting a username and password. If a JWT is provided the client will no longer call Authenticate, which would not work anyhow. It also provides a public method UpdateAuthToken to allow a user of the client to update their auth token, for example, if it expires. In this flow all token lifecycle management is handled outside of the client as a concern of the client user. Signed-off-by: Mike Crute --- client/v3/client.go | 29 ++++++++++++++++++++++------- client/v3/config.go | 7 ++++++- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/client/v3/client.go b/client/v3/client.go index 2d415f10cc9c..5a0590cc7c61 100644 --- a/client/v3/client.go +++ b/client/v3/client.go @@ -68,7 +68,10 @@ type Client struct { // Username is a user name for authentication. Username string // Password is a password for authentication. - Password string + Password string + // Token is a JWT used for authentication instead of a password. + Token string + authTokenBundle credentials.PerRPCCredentialsBundle callOpts []grpc.CallOption @@ -262,9 +265,21 @@ func (c *Client) Dial(ep string) (*grpc.ClientConn, error) { return c.dial(creds, grpc.WithResolvers(resolver.New(ep))) } +// UpdateAuthToken allows updating the JWT auth token held by the +// client. It is safe to call this function concurrently with other +// operations. +func (c *Client) UpdateAuthToken(token string) { + c.authTokenBundle.UpdateAuthToken(token) +} + func (c *Client) getToken(ctx context.Context) error { var err error // return last error in a case of fail + if c.Token != "" { + c.UpdateAuthToken(c.Token) + return nil + } + if c.Username == "" || c.Password == "" { return nil } @@ -277,7 +292,7 @@ func (c *Client) getToken(ctx context.Context) error { } return err } - c.authTokenBundle.UpdateAuthToken(resp.Token) + c.UpdateAuthToken(resp.Token) return nil } @@ -386,11 +401,11 @@ func newClient(cfg *Config) (*Client, error) { return nil, err } - if cfg.Username != "" && cfg.Password != "" { - client.Username = cfg.Username - client.Password = cfg.Password - client.authTokenBundle = credentials.NewPerRPCCredentialBundle() - } + client.Username = cfg.Username + client.Password = cfg.Password + client.Token = cfg.Token + client.authTokenBundle = credentials.NewPerRPCCredentialBundle() + if cfg.MaxCallSendMsgSize > 0 || cfg.MaxCallRecvMsgSize > 0 { if cfg.MaxCallRecvMsgSize > 0 && cfg.MaxCallSendMsgSize > cfg.MaxCallRecvMsgSize { return nil, fmt.Errorf("gRPC message recv limit (%d bytes) must be greater than send limit (%d bytes)", cfg.MaxCallRecvMsgSize, cfg.MaxCallSendMsgSize) diff --git a/client/v3/config.go b/client/v3/config.go index 4a26714a8645..0074824d04cc 100644 --- a/client/v3/config.go +++ b/client/v3/config.go @@ -66,6 +66,9 @@ type Config struct { // Password is a password for authentication. Password string `json:"password"` + // Token is a JWT used for authentication instead of a password. + Token string `json:"token"` + // RejectOldCluster when set will refuse to create a client against an outdated cluster. RejectOldCluster bool `json:"reject-old-cluster"` @@ -119,10 +122,11 @@ type SecureConfig struct { type AuthConfig struct { Username string `json:"username"` Password string `json:"password"` + Token string `json:"token"` } func (cfg AuthConfig) Empty() bool { - return cfg.Username == "" && cfg.Password == "" + return cfg.Username == "" && cfg.Password == "" && cfg.Token == "" } // NewClientConfig creates a Config based on the provided ConfigSpec. @@ -143,6 +147,7 @@ func NewClientConfig(confSpec *ConfigSpec, lg *zap.Logger) (*Config, error) { if confSpec.Auth != nil { cfg.Username = confSpec.Auth.Username cfg.Password = confSpec.Auth.Password + cfg.Token = confSpec.Auth.Token } return cfg, nil