瀏覽代碼

*: support jwt token in v3 auth API

This commit adds jwt token support in v3 auth API.

Remaining major ToDos:
- Currently token type isn't hidden from etcdserver. In the near
  future the information should be completely invisible from
  etcdserver package.
- Configurable expiration of token. Currently tokens can be valid
  until keys are changed.

How to use:
1. generate keys for signing and verfying jwt tokens:
 $ openssl genrsa -out app.rsa 1024
 $ openssl rsa -in app.rsa -pubout > app.rsa.pub
2.  add command line options to etcd like below:
--auth-token-type jwt \
--auth-jwt-pub-key app.rsa.pub --auth-jwt-priv-key app.rsa \
--auth-jwt-sign-method RS512
3. launch etcd cluster

Below is a performance comparison of serializable read w/ and w/o jwt
token. Every (3) etcd node is executed on a single machine. Signing
method is RS512 and key length is 1024 bit. As the results show, jwt
based token introduces a performance overhead but it would be
acceptable for a case that requires authentication.

w/o jwt token auth (no auth):

Summary:
  Total:        1.6172 secs.
  Slowest:      0.0125 secs.
  Fastest:      0.0001 secs.
  Average:      0.0002 secs.
  Stddev:       0.0004 secs.
  Requests/sec: 6183.5877

Response time histogram:
  0.000 [1]     |
  0.001 [9982]  |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  0.003 [1]     |
  0.004 [1]     |
  0.005 [0]     |
  0.006 [0]     |
  0.008 [6]     |
  0.009 [0]     |
  0.010 [1]     |
  0.011 [5]     |
  0.013 [3]     |

Latency distribution:
  10% in 0.0001 secs.
  25% in 0.0001 secs.
  50% in 0.0001 secs.
  75% in 0.0001 secs.
  90% in 0.0002 secs.
  95% in 0.0002 secs.
  99% in 0.0003 secs.

w/ jwt token auth:

Summary:
  Total:        2.5364 secs.
  Slowest:      0.0182 secs.
  Fastest:      0.0002 secs.
  Average:      0.0003 secs.
  Stddev:       0.0005 secs.
  Requests/sec: 3942.5185

Response time histogram:
  0.000 [1]     |
  0.002 [9975]  |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  0.004 [0]     |
  0.006 [1]     |
  0.007 [11]    |
  0.009 [2]     |
  0.011 [4]     |
  0.013 [5]     |
  0.015 [0]     |
  0.016 [0]     |
  0.018 [1]     |

Latency distribution:
  10% in 0.0002 secs.
  25% in 0.0002 secs.
  50% in 0.0002 secs.
  75% in 0.0002 secs.
  90% in 0.0003 secs.
  95% in 0.0003 secs.
  99% in 0.0004 secs.
Hitoshi Mitake 9 年之前
父節點
當前提交
f8a290e7ca

+ 7 - 0
Documentation/op-guide/configuration.md

@@ -289,6 +289,13 @@ Follow the instructions when using these flags.
 + Set level of detail for exported metrics, specify 'extensive' to include histogram metrics.
 + default: basic
 
+## 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'
++ default: "simple"
+
 [build-cluster]: clustering.md#static
 [reconfig]: runtime-configuration.md
 [discovery]: clustering.md#discovery

+ 137 - 0
auth/jwt.go

@@ -0,0 +1,137 @@
+// Copyright 2017 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth
+
+import (
+	"crypto/rsa"
+	"io/ioutil"
+
+	jwt "github.com/dgrijalva/jwt-go"
+	"golang.org/x/net/context"
+)
+
+type tokenJWT struct {
+	signMethod string
+	signKey    *rsa.PrivateKey
+	verifyKey  *rsa.PublicKey
+}
+
+func (t *tokenJWT) enable()                         {}
+func (t *tokenJWT) disable()                        {}
+func (t *tokenJWT) invalidateUser(string)           {}
+func (t *tokenJWT) genTokenPrefix() (string, error) { return "", nil }
+
+func (t *tokenJWT) info(ctx context.Context, token string, rev uint64) (*AuthInfo, bool) {
+	// rev isn't used in JWT, it is only used in simple token
+	var (
+		username string
+		revision uint64
+	)
+
+	parsed, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
+		return t.verifyKey, nil
+	})
+
+	switch err.(type) {
+	case nil:
+		if !parsed.Valid {
+			plog.Warningf("invalid jwt token: %s", token)
+			return nil, false
+		}
+
+		claims := parsed.Claims.(jwt.MapClaims)
+
+		username = claims["username"].(string)
+		revision = uint64(claims["revision"].(float64))
+	default:
+		plog.Warningf("failed to parse jwt token: %s", err)
+		return nil, false
+	}
+
+	return &AuthInfo{Username: username, Revision: revision}, true
+}
+
+func (t *tokenJWT) assign(ctx context.Context, username string, revision uint64) (string, error) {
+	// Future work: let a jwt token include permission infomation would be useful for
+	// permission checking in proxy side.
+	tk := jwt.NewWithClaims(jwt.GetSigningMethod(t.signMethod),
+		jwt.MapClaims{
+			"username": username,
+			"revision": revision,
+		})
+
+	token, err := tk.SignedString(t.signKey)
+	if err != nil {
+		plog.Debugf("failed to sign jwt token: %s", err)
+		return "", err
+	}
+
+	plog.Debugf("jwt token: %s", token)
+
+	return token, err
+}
+
+func prepareOpts(opts map[string]string) (jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath string, err error) {
+	for k, v := range opts {
+		switch k {
+		case "sign-method":
+			jwtSignMethod = v
+		case "pub-key":
+			jwtPubKeyPath = v
+		case "priv-key":
+			jwtPrivKeyPath = v
+		default:
+			plog.Errorf("unknown token specific option: %s", k)
+			return "", "", "", ErrInvalidAuthOpts
+		}
+	}
+
+	return jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, nil
+}
+
+func newTokenProviderJWT(opts map[string]string) (*tokenJWT, error) {
+	jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, err := prepareOpts(opts)
+	if err != nil {
+		return nil, ErrInvalidAuthOpts
+	}
+
+	t := &tokenJWT{}
+
+	t.signMethod = jwtSignMethod
+
+	verifyBytes, err := ioutil.ReadFile(jwtPubKeyPath)
+	if err != nil {
+		plog.Errorf("failed to read public key (%s) for jwt: %s", jwtPubKeyPath, err)
+		return nil, err
+	}
+	t.verifyKey, err = jwt.ParseRSAPublicKeyFromPEM(verifyBytes)
+	if err != nil {
+		plog.Errorf("failed to parse public key (%s): %s", jwtPubKeyPath, err)
+		return nil, err
+	}
+
+	signBytes, err := ioutil.ReadFile(jwtPrivKeyPath)
+	if err != nil {
+		plog.Errorf("failed to read private key (%s) for jwt: %s", jwtPrivKeyPath, err)
+		return nil, err
+	}
+	t.signKey, err = jwt.ParseRSAPrivateKeyFromPEM(signBytes)
+	if err != nil {
+		plog.Errorf("failed to parse private key (%s): %s", jwtPrivKeyPath, err)
+		return nil, err
+	}
+
+	return t, nil
+}

+ 102 - 13
auth/simple_token.go

@@ -19,9 +19,14 @@ package auth
 
 import (
 	"crypto/rand"
+	"fmt"
 	"math/big"
+	"strconv"
 	"strings"
+	"sync"
 	"time"
+
+	"golang.org/x/net/context"
 )
 
 const (
@@ -100,7 +105,14 @@ func (tm *simpleTokenTTLKeeper) run() {
 	}
 }
 
-func (as *authStore) GenSimpleToken() (string, error) {
+type tokenSimple struct {
+	indexWaiter       func(uint64) <-chan struct{}
+	simpleTokenKeeper *simpleTokenTTLKeeper
+	simpleTokensMu    sync.RWMutex
+	simpleTokens      map[string]string // token -> username
+}
+
+func (t *tokenSimple) genTokenPrefix() (string, error) {
 	ret := make([]byte, defaultSimpleTokenLength)
 
 	for i := 0; i < defaultSimpleTokenLength; i++ {
@@ -115,27 +127,104 @@ func (as *authStore) GenSimpleToken() (string, error) {
 	return string(ret), nil
 }
 
-func (as *authStore) assignSimpleTokenToUser(username, token string) {
-	as.simpleTokensMu.Lock()
+func (t *tokenSimple) assignSimpleTokenToUser(username, token string) {
+	t.simpleTokensMu.Lock()
 
-	_, ok := as.simpleTokens[token]
+	_, ok := t.simpleTokens[token]
 	if ok {
 		plog.Panicf("token %s is alredy used", token)
 	}
 
-	as.simpleTokens[token] = username
-	as.simpleTokenKeeper.addSimpleToken(token)
-	as.simpleTokensMu.Unlock()
+	t.simpleTokens[token] = username
+	t.simpleTokenKeeper.addSimpleToken(token)
+	t.simpleTokensMu.Unlock()
 }
 
-func (as *authStore) invalidateUser(username string) {
-	as.simpleTokensMu.Lock()
-	defer as.simpleTokensMu.Unlock()
+func (t *tokenSimple) invalidateUser(username string) {
+	t.simpleTokensMu.Lock()
+	defer t.simpleTokensMu.Unlock()
 
-	for token, name := range as.simpleTokens {
+	for token, name := range t.simpleTokens {
 		if strings.Compare(name, username) == 0 {
-			delete(as.simpleTokens, token)
-			as.simpleTokenKeeper.deleteSimpleToken(token)
+			delete(t.simpleTokens, token)
+			t.simpleTokenKeeper.deleteSimpleToken(token)
 		}
 	}
 }
+
+func newDeleterFunc(t *tokenSimple) func(string) {
+	return func(tk string) {
+		t.simpleTokensMu.Lock()
+		defer t.simpleTokensMu.Unlock()
+		if username, ok := t.simpleTokens[tk]; ok {
+			plog.Infof("deleting token %s for user %s", tk, username)
+			delete(t.simpleTokens, tk)
+		}
+	}
+}
+
+func (t *tokenSimple) enable() {
+	t.simpleTokenKeeper = NewSimpleTokenTTLKeeper(newDeleterFunc(t))
+}
+
+func (t *tokenSimple) disable() {
+	if t.simpleTokenKeeper != nil {
+		t.simpleTokenKeeper.stop()
+		t.simpleTokenKeeper = nil
+	}
+
+	t.simpleTokensMu.Lock()
+	t.simpleTokens = make(map[string]string) // invalidate all tokens
+	t.simpleTokensMu.Unlock()
+}
+
+func (t *tokenSimple) info(ctx context.Context, token string, revision uint64) (*AuthInfo, bool) {
+	if !t.isValidSimpleToken(ctx, token) {
+		return nil, false
+	}
+
+	t.simpleTokensMu.RLock()
+	defer t.simpleTokensMu.RUnlock()
+	username, ok := t.simpleTokens[token]
+	if ok {
+		t.simpleTokenKeeper.resetSimpleToken(token)
+	}
+
+	return &AuthInfo{Username: username, Revision: revision}, ok
+}
+
+func (t *tokenSimple) assign(ctx context.Context, username string, rev uint64) (string, error) {
+	// rev isn't used in simple token, it is only used in JWT
+	index := ctx.Value("index").(uint64)
+	simpleToken := ctx.Value("simpleToken").(string)
+	token := fmt.Sprintf("%s.%d", simpleToken, index)
+	t.assignSimpleTokenToUser(username, token)
+
+	return token, nil
+}
+
+func (t *tokenSimple) isValidSimpleToken(ctx context.Context, token string) bool {
+	splitted := strings.Split(token, ".")
+	if len(splitted) != 2 {
+		return false
+	}
+	index, err := strconv.Atoi(splitted[1])
+	if err != nil {
+		return false
+	}
+
+	select {
+	case <-t.indexWaiter(uint64(index)):
+		return true
+	case <-ctx.Done():
+	}
+
+	return false
+}
+
+func newTokenProviderSimple(indexWaiter func(uint64) <-chan struct{}) *tokenSimple {
+	return &tokenSimple{
+		simpleTokens: make(map[string]string),
+		indexWaiter:  indexWaiter,
+	}
+}

+ 81 - 83
auth/store.go

@@ -18,9 +18,7 @@ import (
 	"bytes"
 	"encoding/binary"
 	"errors"
-	"fmt"
 	"sort"
-	"strconv"
 	"strings"
 	"sync"
 
@@ -62,6 +60,7 @@ var (
 	ErrAuthNotEnabled       = errors.New("auth: authentication is not enabled")
 	ErrAuthOldRevision      = errors.New("auth: revision in header is old")
 	ErrInvalidAuthToken     = errors.New("auth: invalid auth token")
+	ErrInvalidAuthOpts      = errors.New("auth: invalid auth options")
 
 	// BcryptCost is the algorithm cost / strength for hashing auth passwords
 	BcryptCost = bcrypt.DefaultCost
@@ -131,10 +130,6 @@ type AuthStore interface {
 	// RoleList gets a list of all roles
 	RoleList(r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error)
 
-	// AuthInfoFromToken gets a username from the given Token and current revision number
-	// (The revision number is used for preventing the TOCTOU problem)
-	AuthInfoFromToken(token string) (*AuthInfo, bool)
-
 	// IsPutPermitted checks put permission of the user
 	IsPutPermitted(authInfo *AuthInfo, key []byte) error
 
@@ -147,8 +142,9 @@ type AuthStore interface {
 	// IsAdminPermitted checks admin permission of the user
 	IsAdminPermitted(authInfo *AuthInfo) error
 
-	// GenSimpleToken produces a simple random string
-	GenSimpleToken() (string, error)
+	// GenTokenPrefix produces a random string in a case of simple token
+	// in a case of JWT, it produces an empty string
+	GenTokenPrefix() (string, error)
 
 	// Revision gets current revision of authStore
 	Revision() uint64
@@ -166,6 +162,16 @@ type AuthStore interface {
 	AuthInfoFromTLS(ctx context.Context) *AuthInfo
 }
 
+type TokenProvider interface {
+	info(ctx context.Context, token string, revision uint64) (*AuthInfo, bool)
+	assign(ctx context.Context, username string, revision uint64) (string, error)
+	enable()
+	disable()
+
+	invalidateUser(string)
+	genTokenPrefix() (string, error)
+}
+
 type authStore struct {
 	be        backend.Backend
 	enabled   bool
@@ -173,24 +179,9 @@ type authStore struct {
 
 	rangePermCache map[string]*unifiedRangePermissions // username -> unifiedRangePermissions
 
-	simpleTokensMu    sync.RWMutex
-	simpleTokens      map[string]string // token -> username
-	simpleTokenKeeper *simpleTokenTTLKeeper
-
 	revision uint64
 
-	indexWaiter func(uint64) <-chan struct{}
-}
-
-func newDeleterFunc(as *authStore) func(string) {
-	return func(t string) {
-		as.simpleTokensMu.Lock()
-		defer as.simpleTokensMu.Unlock()
-		if username, ok := as.simpleTokens[t]; ok {
-			plog.Infof("deleting token %s for user %s", t, username)
-			delete(as.simpleTokens, t)
-		}
-	}
+	tokenProvider TokenProvider
 }
 
 func (as *authStore) AuthEnable() error {
@@ -220,8 +211,7 @@ func (as *authStore) AuthEnable() error {
 	tx.UnsafePut(authBucketName, enableFlagKey, authEnabled)
 
 	as.enabled = true
-
-	as.simpleTokenKeeper = NewSimpleTokenTTLKeeper(newDeleterFunc(as))
+	as.tokenProvider.enable()
 
 	as.rangePermCache = make(map[string]*unifiedRangePermissions)
 
@@ -247,14 +237,7 @@ func (as *authStore) AuthDisable() {
 	b.ForceCommit()
 
 	as.enabled = false
-
-	as.simpleTokensMu.Lock()
-	as.simpleTokens = make(map[string]string) // invalidate all tokens
-	as.simpleTokensMu.Unlock()
-	if as.simpleTokenKeeper != nil {
-		as.simpleTokenKeeper.stop()
-		as.simpleTokenKeeper = nil
-	}
+	as.tokenProvider.disable()
 
 	plog.Noticef("Authentication disabled")
 }
@@ -265,10 +248,7 @@ func (as *authStore) Close() error {
 	if !as.enabled {
 		return nil
 	}
-	if as.simpleTokenKeeper != nil {
-		as.simpleTokenKeeper.stop()
-		as.simpleTokenKeeper = nil
-	}
+	as.tokenProvider.disable()
 	return nil
 }
 
@@ -277,10 +257,6 @@ func (as *authStore) Authenticate(ctx context.Context, username, password string
 		return nil, ErrAuthNotEnabled
 	}
 
-	// TODO(mitake): after adding jwt support, branching based on values of ctx is required
-	index := ctx.Value("index").(uint64)
-	simpleToken := ctx.Value("simpleToken").(string)
-
 	tx := as.be.BatchTx()
 	tx.Lock()
 	defer tx.Unlock()
@@ -290,10 +266,15 @@ func (as *authStore) Authenticate(ctx context.Context, username, password string
 		return nil, ErrAuthFailed
 	}
 
-	token := fmt.Sprintf("%s.%d", simpleToken, index)
-	as.assignSimpleTokenToUser(username, token)
+	// Password checking is already performed in the API layer, so we don't need to check for now.
+	// Staleness of password can be detected with OCC in the API layer, too.
+
+	token, err := as.tokenProvider.assign(ctx, username, as.revision)
+	if err != nil {
+		return nil, err
+	}
 
-	plog.Infof("authorized %s, token is %s", username, token)
+	plog.Debugf("authorized %s, token is %s", username, token)
 	return &pb.AuthenticateResponse{Token: token}, nil
 }
 
@@ -385,7 +366,7 @@ func (as *authStore) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDelete
 	as.commitRevision(tx)
 
 	as.invalidateCachedPerm(r.Name)
-	as.invalidateUser(r.Name)
+	as.tokenProvider.invalidateUser(r.Name)
 
 	plog.Noticef("deleted a user: %s", r.Name)
 
@@ -421,7 +402,7 @@ func (as *authStore) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*p
 	as.commitRevision(tx)
 
 	as.invalidateCachedPerm(r.Name)
-	as.invalidateUser(r.Name)
+	as.tokenProvider.invalidateUser(r.Name)
 
 	plog.Noticef("changed a password of a user: %s", r.Name)
 
@@ -650,14 +631,8 @@ func (as *authStore) RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse,
 	return &pb.AuthRoleAddResponse{}, nil
 }
 
-func (as *authStore) AuthInfoFromToken(token string) (*AuthInfo, bool) {
-	as.simpleTokensMu.RLock()
-	defer as.simpleTokensMu.RUnlock()
-	t, ok := as.simpleTokens[token]
-	if ok {
-		as.simpleTokenKeeper.resetSimpleToken(token)
-	}
-	return &AuthInfo{Username: t, Revision: as.revision}, ok
+func (as *authStore) authInfoFromToken(ctx context.Context, token string) (*AuthInfo, bool) {
+	return as.tokenProvider.info(ctx, token, as.revision)
 }
 
 type permSlice []*authpb.Permission
@@ -887,7 +862,7 @@ func (as *authStore) isAuthEnabled() bool {
 	return as.enabled
 }
 
-func NewAuthStore(be backend.Backend, indexWaiter func(uint64) <-chan struct{}) *authStore {
+func NewAuthStore(be backend.Backend, tp TokenProvider) *authStore {
 	tx := be.BatchTx()
 	tx.Lock()
 
@@ -905,15 +880,14 @@ func NewAuthStore(be backend.Backend, indexWaiter func(uint64) <-chan struct{})
 
 	as := &authStore{
 		be:             be,
-		simpleTokens:   make(map[string]string),
 		revision:       getRevision(tx),
-		indexWaiter:    indexWaiter,
 		enabled:        enabled,
 		rangePermCache: make(map[string]*unifiedRangePermissions),
+		tokenProvider:  tp,
 	}
 
 	if enabled {
-		as.simpleTokenKeeper = NewSimpleTokenTTLKeeper(newDeleterFunc(as))
+		as.tokenProvider.enable()
 	}
 
 	if as.revision == 0 {
@@ -956,25 +930,6 @@ func (as *authStore) Revision() uint64 {
 	return as.revision
 }
 
-func (as *authStore) isValidSimpleToken(token string, ctx context.Context) bool {
-	splitted := strings.Split(token, ".")
-	if len(splitted) != 2 {
-		return false
-	}
-	index, err := strconv.Atoi(splitted[1])
-	if err != nil {
-		return false
-	}
-
-	select {
-	case <-as.indexWaiter(uint64(index)):
-		return true
-	case <-ctx.Done():
-	}
-
-	return false
-}
-
 func (as *authStore) AuthInfoFromTLS(ctx context.Context) *AuthInfo {
 	peer, ok := peer.FromContext(ctx)
 	if !ok || peer == nil || peer.AuthInfo == nil {
@@ -1009,14 +964,57 @@ func (as *authStore) AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error) {
 	}
 
 	token := ts[0]
-	if !as.isValidSimpleToken(token, ctx) {
-		return nil, ErrInvalidAuthToken
-	}
-
-	authInfo, uok := as.AuthInfoFromToken(token)
+	authInfo, uok := as.authInfoFromToken(ctx, token)
 	if !uok {
 		plog.Warningf("invalid auth token: %s", token)
 		return nil, ErrInvalidAuthToken
 	}
 	return authInfo, nil
 }
+
+func (as *authStore) GenTokenPrefix() (string, error) {
+	return as.tokenProvider.genTokenPrefix()
+}
+
+func decomposeOpts(optstr string) (string, map[string]string, error) {
+	opts := strings.Split(optstr, ",")
+	tokenType := opts[0]
+
+	typeSpecificOpts := make(map[string]string)
+	for i := 1; i < len(opts); i++ {
+		pair := strings.Split(opts[i], "=")
+
+		if len(pair) != 2 {
+			plog.Errorf("invalid token specific option: %s", optstr)
+			return "", nil, ErrInvalidAuthOpts
+		}
+
+		if _, ok := typeSpecificOpts[pair[0]]; ok {
+			plog.Errorf("invalid token specific option, duplicated parameters (%s): %s", pair[0], optstr)
+			return "", nil, ErrInvalidAuthOpts
+		}
+
+		typeSpecificOpts[pair[0]] = pair[1]
+	}
+
+	return tokenType, typeSpecificOpts, nil
+
+}
+
+func NewTokenProvider(tokenOpts string, indexWaiter func(uint64) <-chan struct{}) (TokenProvider, error) {
+	tokenType, typeSpecificOpts, err := decomposeOpts(tokenOpts)
+	if err != nil {
+		return nil, ErrInvalidAuthOpts
+	}
+
+	switch tokenType {
+	case "simple":
+		plog.Warningf("simple token is not cryptographically signed")
+		return newTokenProviderSimple(indexWaiter), nil
+	case "jwt":
+		return newTokenProviderJWT(typeSpecificOpts)
+	default:
+		plog.Errorf("unknown token type: %s", tokenType)
+		return nil, ErrInvalidAuthOpts
+	}
+}

+ 21 - 9
auth/store_test.go

@@ -44,8 +44,12 @@ func TestNewAuthStoreRevision(t *testing.T) {
 	b, tPath := backend.NewDefaultTmpBackend()
 	defer os.Remove(tPath)
 
-	as := NewAuthStore(b, dummyIndexWaiter)
-	err := enableAuthAndCreateRoot(as)
+	tp, err := NewTokenProvider("simple", dummyIndexWaiter)
+	if err != nil {
+		t.Fatal(err)
+	}
+	as := NewAuthStore(b, tp)
+	err = enableAuthAndCreateRoot(as)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -55,7 +59,7 @@ func TestNewAuthStoreRevision(t *testing.T) {
 
 	// no changes to commit
 	b2 := backend.NewDefaultBackend(tPath)
-	as = NewAuthStore(b2, dummyIndexWaiter)
+	as = NewAuthStore(b2, tp)
 	new := as.Revision()
 	b2.Close()
 	as.Close()
@@ -68,8 +72,12 @@ func TestNewAuthStoreRevision(t *testing.T) {
 func setupAuthStore(t *testing.T) (store *authStore, teardownfunc func(t *testing.T)) {
 	b, tPath := backend.NewDefaultTmpBackend()
 
-	as := NewAuthStore(b, dummyIndexWaiter)
-	err := enableAuthAndCreateRoot(as)
+	tp, err := NewTokenProvider("simple", dummyIndexWaiter)
+	if err != nil {
+		t.Fatal(err)
+	}
+	as := NewAuthStore(b, tp)
+	err = enableAuthAndCreateRoot(as)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -188,18 +196,18 @@ func TestUserChangePassword(t *testing.T) {
 	defer tearDown(t)
 
 	ctx1 := context.WithValue(context.WithValue(context.TODO(), "index", uint64(1)), "simpleToken", "dummy")
-	_, err := as.Authenticate(ctx1, "foo", "")
+	_, err := as.Authenticate(ctx1, "foo", "bar")
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	_, err = as.UserChangePassword(&pb.AuthUserChangePasswordRequest{Name: "foo", Password: "bar"})
+	_, err = as.UserChangePassword(&pb.AuthUserChangePasswordRequest{Name: "foo", Password: "baz"})
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	ctx2 := context.WithValue(context.WithValue(context.TODO(), "index", uint64(2)), "simpleToken", "dummy")
-	_, err = as.Authenticate(ctx2, "foo", "bar")
+	_, err = as.Authenticate(ctx2, "foo", "baz")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -544,7 +552,11 @@ func TestRecoverFromSnapshot(t *testing.T) {
 
 	as.Close()
 
-	as2 := NewAuthStore(as.be, dummyIndexWaiter)
+	tp, err := NewTokenProvider("simple", dummyIndexWaiter)
+	if err != nil {
+		t.Fatal(err)
+	}
+	as2 := NewAuthStore(as.be, tp)
 	defer func(a *authStore) {
 		a.Close()
 	}(as2)

+ 5 - 0
embed/config.go

@@ -128,6 +128,10 @@ type Config struct {
 	//	}
 	//	embed.StartEtcd(cfg)
 	ServiceRegister func(*grpc.Server) `json:"-"`
+
+	// auth
+
+	AuthToken string `json:"auth-token"`
 }
 
 // configYAML holds the config suitable for yaml parsing
@@ -179,6 +183,7 @@ func NewConfig() *Config {
 		StrictReconfigCheck: true,
 		Metrics:             "basic",
 		EnableV2:            true,
+		AuthToken:           "simple",
 	}
 	cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)
 	return cfg

+ 1 - 0
embed/etcd.go

@@ -118,6 +118,7 @@ func StartEtcd(inCfg *Config) (e *Etcd, err error) {
 		QuotaBackendBytes:       cfg.QuotaBackendBytes,
 		StrictReconfigCheck:     cfg.StrictReconfigCheck,
 		ClientCertAuthEnabled:   cfg.ClientTLSInfo.ClientCertAuth,
+		AuthToken:               cfg.AuthToken,
 	}
 
 	if e.Server, err = etcdserver.NewServer(srvcfg); err != nil {

+ 3 - 0
etcdmain/config.go

@@ -205,6 +205,9 @@ func newConfig() *config {
 	// additional metrics
 	fs.StringVar(&cfg.Metrics, "metrics", cfg.Metrics, "Set level of detail for exported metrics, specify 'extensive' to include histogram metrics")
 
+	// auth
+	fs.StringVar(&cfg.AuthToken, "auth-token", cfg.AuthToken, "Specify auth token specific options.")
+
 	// ignored
 	for _, f := range cfg.ignored {
 		fs.Var(&flags.IgnoredFlag{Name: f}, f, "")

+ 4 - 0
etcdmain/help.go

@@ -163,5 +163,9 @@ profiling flags:
 		Enable runtime profiling data via HTTP server. Address is at client URL + "/debug/pprof/"
 	--metrics 'basic'
 	  Set level of detail for exported metrics, specify 'extensive' to include histogram metrics.
+
+auth flags:
+	--auth-token 'simple'
+		Specify a v3 authentication token type and its options ('simple' or 'jwt').
 `
 )

+ 2 - 0
etcdserver/config.go

@@ -59,6 +59,8 @@ type ServerConfig struct {
 
 	// ClientCertAuthEnabled is true when cert has been signed by the client CA.
 	ClientCertAuthEnabled bool
+
+	AuthToken string
 }
 
 // VerifyBootstrap sanity-checks the initial config for bootstrap case

+ 8 - 3
etcdserver/server.go

@@ -458,11 +458,16 @@ func NewServer(cfg *ServerConfig) (srv *EtcdServer, err error) {
 		}
 	}
 	srv.consistIndex.setConsistentIndex(srv.kv.ConsistentIndex())
-
-	srv.authStore = auth.NewAuthStore(srv.be,
+	tp, err := auth.NewTokenProvider(cfg.AuthToken,
 		func(index uint64) <-chan struct{} {
 			return srv.applyWait.Wait(index)
-		})
+		},
+	)
+	if err != nil {
+		plog.Errorf("failed to create token provider: %s", err)
+		return nil, err
+	}
+	srv.authStore = auth.NewAuthStore(srv.be, tp)
 	if h := cfg.AutoCompactionRetention; h != 0 {
 		srv.compactor = compactor.NewPeriodic(h, srv.kv, srv)
 		srv.compactor.Run()

+ 1 - 1
etcdserver/v3_server.go

@@ -441,7 +441,7 @@ func (s *EtcdServer) Authenticate(ctx context.Context, r *pb.AuthenticateRequest
 			return nil, err
 		}
 
-		st, err := s.AuthStore().GenSimpleToken()
+		st, err := s.AuthStore().GenTokenPrefix()
 		if err != nil {
 			return nil, err
 		}

+ 1 - 0
integration/cluster.go

@@ -515,6 +515,7 @@ func mustNewMember(t *testing.T, mcfg memberConfig) *member {
 	m.ElectionTicks = electionTicks
 	m.TickMs = uint(tickDuration / time.Millisecond)
 	m.QuotaBackendBytes = mcfg.quotaBackendBytes
+	m.AuthToken = "simple" // for the purpose of integration testing, simple token is enough
 	return m
 }