Browse Source

Merge pull request #8302 from mitake/token-ttl

auth: a new option for configuring TTL of jwt tokens
Gyuho Lee 7 years ago
parent
commit
ac50ef0812
4 changed files with 57 additions and 8 deletions
  1. 2 2
      Documentation/op-guide/configuration.md
  2. 21 6
      auth/jwt.go
  3. 11 0
      e2e/cluster_test.go
  4. 23 0
      e2e/ctl_v3_auth_test.go

+ 2 - 2
Documentation/op-guide/configuration.md

@@ -361,8 +361,8 @@ Follow the instructions when using these flags.
 ## Auth flags
 ## Auth flags
 
 
 ### --auth-token
 ### --auth-token
-+ Specify a token type and token specific options, especially for JWT. Its format is "type,var1=val1,var2=val2,...". Possible type is 'simple' or 'jwt'. Possible variables are 'sign-method' for specifying a sign method of jwt (its possible values are 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', or 'PS512'), 'pub-key' for specifying a path to a public key for verifying jwt, and 'priv-key' for specifying a path to a private key for signing jwt.
-+ Example option of JWT: '--auth-token jwt,pub-key=app.rsa.pub,priv-key=app.rsa,sign-method=RS512'
++ Specify a token type and token specific options, especially for JWT. Its format is "type,var1=val1,var2=val2,...". Possible type is 'simple' or 'jwt'. Possible variables are 'sign-method' for specifying a sign method of jwt (its possible values are 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', or 'PS512'), 'pub-key' for specifying a path to a public key for verifying jwt, 'priv-key' for specifying a path to a private key for signing jwt, and 'ttl' for specifying TTL of jwt tokens.
++ Example option of JWT: '--auth-token jwt,pub-key=app.rsa.pub,priv-key=app.rsa,sign-method=RS512,ttl=10m'
 + default: "simple"
 + default: "simple"
 
 
 ## Experimental flags
 ## Experimental flags

+ 21 - 6
auth/jwt.go

@@ -18,6 +18,7 @@ import (
 	"context"
 	"context"
 	"crypto/rsa"
 	"crypto/rsa"
 	"io/ioutil"
 	"io/ioutil"
+	"time"
 
 
 	jwt "github.com/dgrijalva/jwt-go"
 	jwt "github.com/dgrijalva/jwt-go"
 )
 )
@@ -26,6 +27,7 @@ type tokenJWT struct {
 	signMethod string
 	signMethod string
 	signKey    *rsa.PrivateKey
 	signKey    *rsa.PrivateKey
 	verifyKey  *rsa.PublicKey
 	verifyKey  *rsa.PublicKey
+	ttl        time.Duration
 }
 }
 
 
 func (t *tokenJWT) enable()                         {}
 func (t *tokenJWT) enable()                         {}
@@ -70,6 +72,7 @@ func (t *tokenJWT) assign(ctx context.Context, username string, revision uint64)
 		jwt.MapClaims{
 		jwt.MapClaims{
 			"username": username,
 			"username": username,
 			"revision": revision,
 			"revision": revision,
+			"exp":      time.Now().Add(t.ttl).Unix(),
 		})
 		})
 
 
 	token, err := tk.SignedString(t.signKey)
 	token, err := tk.SignedString(t.signKey)
@@ -83,7 +86,7 @@ func (t *tokenJWT) assign(ctx context.Context, username string, revision uint64)
 	return token, err
 	return token, err
 }
 }
 
 
-func prepareOpts(opts map[string]string) (jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath string, err error) {
+func prepareOpts(opts map[string]string) (jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath string, ttl time.Duration, err error) {
 	for k, v := range opts {
 	for k, v := range opts {
 		switch k {
 		switch k {
 		case "sign-method":
 		case "sign-method":
@@ -92,24 +95,36 @@ func prepareOpts(opts map[string]string) (jwtSignMethod, jwtPubKeyPath, jwtPrivK
 			jwtPubKeyPath = v
 			jwtPubKeyPath = v
 		case "priv-key":
 		case "priv-key":
 			jwtPrivKeyPath = v
 			jwtPrivKeyPath = v
+		case "ttl":
+			ttl, err = time.ParseDuration(v)
+			if err != nil {
+				plog.Errorf("failed to parse ttl option (%s)", err)
+				return "", "", "", 0, ErrInvalidAuthOpts
+			}
 		default:
 		default:
 			plog.Errorf("unknown token specific option: %s", k)
 			plog.Errorf("unknown token specific option: %s", k)
-			return "", "", "", ErrInvalidAuthOpts
+			return "", "", "", 0, ErrInvalidAuthOpts
 		}
 		}
 	}
 	}
 	if len(jwtSignMethod) == 0 {
 	if len(jwtSignMethod) == 0 {
-		return "", "", "", ErrInvalidAuthOpts
+		return "", "", "", 0, ErrInvalidAuthOpts
 	}
 	}
-	return jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, nil
+	return jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, ttl, nil
 }
 }
 
 
 func newTokenProviderJWT(opts map[string]string) (*tokenJWT, error) {
 func newTokenProviderJWT(opts map[string]string) (*tokenJWT, error) {
-	jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, err := prepareOpts(opts)
+	jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, ttl, err := prepareOpts(opts)
 	if err != nil {
 	if err != nil {
 		return nil, ErrInvalidAuthOpts
 		return nil, ErrInvalidAuthOpts
 	}
 	}
 
 
-	t := &tokenJWT{}
+	if ttl == 0 {
+		ttl = 5 * time.Minute
+	}
+
+	t := &tokenJWT{
+		ttl: ttl,
+	}
 
 
 	t.signMethod = jwtSignMethod
 	t.signMethod = jwtSignMethod
 
 

+ 11 - 0
e2e/cluster_test.go

@@ -78,6 +78,11 @@ var (
 		initialToken:          "new",
 		initialToken:          "new",
 		clientCertAuthEnabled: true,
 		clientCertAuthEnabled: true,
 	}
 	}
+	configJWT = etcdProcessClusterConfig{
+		clusterSize:   1,
+		initialToken:  "new",
+		authTokenOpts: "jwt,pub-key=../integration/fixtures/server.crt,priv-key=../integration/fixtures/server.key.insecure,sign-method=RS256,ttl=1s",
+	}
 )
 )
 
 
 func configStandalone(cfg etcdProcessClusterConfig) *etcdProcessClusterConfig {
 func configStandalone(cfg etcdProcessClusterConfig) *etcdProcessClusterConfig {
@@ -117,6 +122,7 @@ type etcdProcessClusterConfig struct {
 	quotaBackendBytes   int64
 	quotaBackendBytes   int64
 	noStrictReconfig    bool
 	noStrictReconfig    bool
 	initialCorruptCheck bool
 	initialCorruptCheck bool
+	authTokenOpts       string
 }
 }
 
 
 // newEtcdProcessCluster launches a new cluster from etcd processes, returning
 // newEtcdProcessCluster launches a new cluster from etcd processes, returning
@@ -238,6 +244,11 @@ func (cfg *etcdProcessClusterConfig) etcdServerProcessConfigs() []*etcdServerPro
 		}
 		}
 
 
 		args = append(args, cfg.tlsArgs()...)
 		args = append(args, cfg.tlsArgs()...)
+
+		if cfg.authTokenOpts != "" {
+			args = append(args, "--auth-token", cfg.authTokenOpts)
+		}
+
 		etcdCfgs[i] = &etcdServerProcessConfig{
 		etcdCfgs[i] = &etcdServerProcessConfig{
 			execPath:     cfg.execPath,
 			execPath:     cfg.execPath,
 			args:         args,
 			args:         args,

+ 23 - 0
e2e/ctl_v3_auth_test.go

@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 	"testing"
 	"testing"
+	"time"
 
 
 	"github.com/coreos/etcd/clientv3"
 	"github.com/coreos/etcd/clientv3"
 )
 )
@@ -58,6 +59,7 @@ func TestCtlV3AuthSnapshot(t *testing.T) { testCtl(t, authTestSnapshot) }
 func TestCtlV3AuthCertCNAndUsername(t *testing.T) {
 func TestCtlV3AuthCertCNAndUsername(t *testing.T) {
 	testCtl(t, authTestCertCNAndUsername, withCfg(configClientTLSCertAuth))
 	testCtl(t, authTestCertCNAndUsername, withCfg(configClientTLSCertAuth))
 }
 }
+func TestCtlV3AuthJWTExpire(t *testing.T) { testCtl(t, authTestJWTExpire, withCfg(configJWT)) }
 
 
 func authEnableTest(cx ctlCtx) {
 func authEnableTest(cx ctlCtx) {
 	if err := authEnable(cx); err != nil {
 	if err := authEnable(cx); err != nil {
@@ -1073,3 +1075,24 @@ func authTestCertCNAndUsername(cx ctlCtx) {
 		cx.t.Error(err)
 		cx.t.Error(err)
 	}
 	}
 }
 }
+
+func authTestJWTExpire(cx ctlCtx) {
+	if err := authEnable(cx); err != nil {
+		cx.t.Fatal(err)
+	}
+
+	cx.user, cx.pass = "root", "root"
+	authSetupTestUser(cx)
+
+	// try a granted key
+	if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil {
+		cx.t.Error(err)
+	}
+
+	// wait an expiration of my JWT token
+	<-time.After(3 * time.Second)
+
+	if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil {
+		cx.t.Error(err)
+	}
+}