auth, etcdserver: check password at API layer

The cost of bcrypt password checking is quite high (almost 100ms on a
modern machine) so executing it in apply loop will be
problematic. This commit exclude the checking mechanism to the API
layer. The password checking is validated with the OCC like way
similar to the auth of serializable get.

This commit also removes a unit test of Authenticate RPC from
auth/store_test.go. It is because the RPC now accepts an auth request
unconditionally and delegates the checking functionality to
authStore.CheckPassword() (so a unit test for CheckPassword() is
added). The combination of the two functionalities can be tested by
e2e (e.g. TestCtlV3AuthWriteKey).

Fixes https://github.com/coreos/etcd/issues/6530
This commit is contained in:
Hitoshi Mitake
2016-10-13 19:30:35 +09:00
committed by Hitoshi Mitake
parent cc96f91156
commit 39e9b1f75a
3 changed files with 59 additions and 28 deletions

View File

@ -146,6 +146,9 @@ type AuthStore interface {
// Revision gets current revision of authStore // Revision gets current revision of authStore
Revision() uint64 Revision() uint64
// CheckPassword checks a given pair of username and password is correct
CheckPassword(username, password string) (uint64, error)
} }
type authStore struct { type authStore struct {
@ -232,11 +235,6 @@ func (as *authStore) Authenticate(ctx context.Context, username, password string
return nil, ErrAuthFailed return nil, ErrAuthFailed
} }
if bcrypt.CompareHashAndPassword(user.Password, []byte(password)) != nil {
plog.Noticef("authentication failed, invalid password for user %s", username)
return &pb.AuthenticateResponse{}, ErrAuthFailed
}
token := fmt.Sprintf("%s.%d", simpleToken, index) token := fmt.Sprintf("%s.%d", simpleToken, index)
as.assignSimpleTokenToUser(username, token) as.assignSimpleTokenToUser(username, token)
@ -244,6 +242,24 @@ func (as *authStore) Authenticate(ctx context.Context, username, password string
return &pb.AuthenticateResponse{Token: token}, nil return &pb.AuthenticateResponse{Token: token}, nil
} }
func (as *authStore) CheckPassword(username, password string) (uint64, error) {
tx := as.be.BatchTx()
tx.Lock()
defer tx.Unlock()
user := getUser(tx, username)
if user == nil {
return 0, ErrAuthFailed
}
if bcrypt.CompareHashAndPassword(user.Password, []byte(password)) != nil {
plog.Noticef("authentication failed, invalid password for user %s", username)
return 0, ErrAuthFailed
}
return getRevision(tx), nil
}
func (as *authStore) Recover(be backend.Backend) { func (as *authStore) Recover(be backend.Backend) {
enabled := false enabled := false
as.be = be as.be = be

View File

@ -67,7 +67,7 @@ func enableAuthAndCreateRoot(as *authStore) error {
return as.AuthEnable() return as.AuthEnable()
} }
func TestAuthenticate(t *testing.T) { func TestCheckPassword(t *testing.T) {
b, tPath := backend.NewDefaultTmpBackend() b, tPath := backend.NewDefaultTmpBackend()
defer func() { defer func() {
b.Close() b.Close()
@ -87,8 +87,7 @@ func TestAuthenticate(t *testing.T) {
} }
// auth a non-existing user // auth a non-existing user
ctx1 := context.WithValue(context.WithValue(context.TODO(), "index", uint64(1)), "simpleToken", "dummy") _, err = as.CheckPassword("foo-test", "bar")
_, err = as.Authenticate(ctx1, "foo-test", "bar")
if err == nil { if err == nil {
t.Fatalf("expected %v, got %v", ErrAuthFailed, err) t.Fatalf("expected %v, got %v", ErrAuthFailed, err)
} }
@ -97,15 +96,13 @@ func TestAuthenticate(t *testing.T) {
} }
// auth an existing user with correct password // auth an existing user with correct password
ctx2 := context.WithValue(context.WithValue(context.TODO(), "index", uint64(2)), "simpleToken", "dummy") _, err = as.CheckPassword("foo", "bar")
_, err = as.Authenticate(ctx2, "foo", "bar")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// auth an existing user but with wrong password // auth an existing user but with wrong password
ctx3 := context.WithValue(context.WithValue(context.TODO(), "index", uint64(3)), "simpleToken", "dummy") _, err = as.CheckPassword("foo", "")
_, err = as.Authenticate(ctx3, "foo", "")
if err == nil { if err == nil {
t.Fatalf("expected %v, got %v", ErrAuthFailed, err) t.Fatalf("expected %v, got %v", ErrAuthFailed, err)
} }

View File

@ -419,6 +419,15 @@ func (s *EtcdServer) AuthDisable(ctx context.Context, r *pb.AuthDisableRequest)
} }
func (s *EtcdServer) Authenticate(ctx context.Context, r *pb.AuthenticateRequest) (*pb.AuthenticateResponse, error) { func (s *EtcdServer) Authenticate(ctx context.Context, r *pb.AuthenticateRequest) (*pb.AuthenticateResponse, error) {
var result *applyResult
for {
checkedRevision, err := s.AuthStore().CheckPassword(r.Name, r.Password)
if err != nil {
plog.Errorf("invalid authentication request to user %s was issued", r.Name)
return nil, err
}
st, err := s.AuthStore().GenSimpleToken() st, err := s.AuthStore().GenSimpleToken()
if err != nil { if err != nil {
return nil, err return nil, err
@ -430,13 +439,22 @@ func (s *EtcdServer) Authenticate(ctx context.Context, r *pb.AuthenticateRequest
SimpleToken: st, SimpleToken: st,
} }
result, err := s.processInternalRaftRequestOnce(ctx, pb.InternalRaftRequest{Authenticate: internalReq}) result, err = s.processInternalRaftRequestOnce(ctx, pb.InternalRaftRequest{Authenticate: internalReq})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if result.err != nil { if result.err != nil {
return nil, result.err return nil, result.err
} }
if checkedRevision != s.AuthStore().Revision() {
plog.Infof("revision when password checked is obsolete, retrying")
continue
}
break
}
return result.resp.(*pb.AuthenticateResponse), nil return result.resp.(*pb.AuthenticateResponse), nil
} }