From 8fd01f56d630bab9face30a7da1c8d4c052e2149 Mon Sep 17 00:00:00 2001 From: Hitoshi Mitake Date: Tue, 25 Jul 2017 16:22:30 +0900 Subject: [PATCH 1/2] auth: a new option for configuring TTL of jwt tokens This commit adds a new option of --auth-token, ttl, for configuring TTL of jwt tokens. It can be specified like this: ``` --auth-token jwt,pub-key=,priv-key=,sign-method=,ttl=5m ``` In the above case, TTL will be 5 minutes. --- Documentation/op-guide/configuration.md | 4 ++-- auth/jwt.go | 27 +++++++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Documentation/op-guide/configuration.md b/Documentation/op-guide/configuration.md index 7745bf490..d837d8b8a 100644 --- a/Documentation/op-guide/configuration.md +++ b/Documentation/op-guide/configuration.md @@ -361,8 +361,8 @@ Follow the instructions when using these flags. ## Auth flags ### --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" ## Experimental flags diff --git a/auth/jwt.go b/auth/jwt.go index 99b2d6b5c..64535043c 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -18,6 +18,7 @@ import ( "context" "crypto/rsa" "io/ioutil" + "time" jwt "github.com/dgrijalva/jwt-go" ) @@ -26,6 +27,7 @@ type tokenJWT struct { signMethod string signKey *rsa.PrivateKey verifyKey *rsa.PublicKey + ttl time.Duration } func (t *tokenJWT) enable() {} @@ -70,6 +72,7 @@ func (t *tokenJWT) assign(ctx context.Context, username string, revision uint64) jwt.MapClaims{ "username": username, "revision": revision, + "exp": time.Now().Add(t.ttl).Unix(), }) token, err := tk.SignedString(t.signKey) @@ -83,7 +86,7 @@ func (t *tokenJWT) assign(ctx context.Context, username string, revision uint64) 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 { switch k { case "sign-method": @@ -92,24 +95,36 @@ func prepareOpts(opts map[string]string) (jwtSignMethod, jwtPubKeyPath, jwtPrivK jwtPubKeyPath = v case "priv-key": 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: plog.Errorf("unknown token specific option: %s", k) - return "", "", "", ErrInvalidAuthOpts + return "", "", "", 0, ErrInvalidAuthOpts } } 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) { - jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, err := prepareOpts(opts) + jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, ttl, err := prepareOpts(opts) if err != nil { return nil, ErrInvalidAuthOpts } - t := &tokenJWT{} + if ttl == 0 { + ttl = 5 * time.Minute + } + + t := &tokenJWT{ + ttl: ttl, + } t.signMethod = jwtSignMethod From 2a54e32819a25ad0af8aa6e7b4135750e0b724af Mon Sep 17 00:00:00 2001 From: Hitoshi Mitake Date: Wed, 11 Oct 2017 11:50:10 +0900 Subject: [PATCH 2/2] e2e: add a test case of JWT token expiration --- e2e/cluster_test.go | 11 +++++++++++ e2e/ctl_v3_auth_test.go | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/e2e/cluster_test.go b/e2e/cluster_test.go index 69896dd48..4ca5072f6 100644 --- a/e2e/cluster_test.go +++ b/e2e/cluster_test.go @@ -78,6 +78,11 @@ var ( initialToken: "new", 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 { @@ -117,6 +122,7 @@ type etcdProcessClusterConfig struct { quotaBackendBytes int64 noStrictReconfig bool initialCorruptCheck bool + authTokenOpts string } // newEtcdProcessCluster launches a new cluster from etcd processes, returning @@ -238,6 +244,11 @@ func (cfg *etcdProcessClusterConfig) etcdServerProcessConfigs() []*etcdServerPro } args = append(args, cfg.tlsArgs()...) + + if cfg.authTokenOpts != "" { + args = append(args, "--auth-token", cfg.authTokenOpts) + } + etcdCfgs[i] = &etcdServerProcessConfig{ execPath: cfg.execPath, args: args, diff --git a/e2e/ctl_v3_auth_test.go b/e2e/ctl_v3_auth_test.go index e0555a225..db38d5fc6 100644 --- a/e2e/ctl_v3_auth_test.go +++ b/e2e/ctl_v3_auth_test.go @@ -18,6 +18,7 @@ import ( "fmt" "os" "testing" + "time" "github.com/coreos/etcd/clientv3" ) @@ -58,6 +59,7 @@ func TestCtlV3AuthSnapshot(t *testing.T) { testCtl(t, authTestSnapshot) } func TestCtlV3AuthCertCNAndUsername(t *testing.T) { testCtl(t, authTestCertCNAndUsername, withCfg(configClientTLSCertAuth)) } +func TestCtlV3AuthJWTExpire(t *testing.T) { testCtl(t, authTestJWTExpire, withCfg(configJWT)) } func authEnableTest(cx ctlCtx) { if err := authEnable(cx); err != nil { @@ -1073,3 +1075,24 @@ func authTestCertCNAndUsername(cx ctlCtx) { 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) + } +}