From 3b43f25b945fb8eff0f2b66d6ef1b15ea852b9c4 Mon Sep 17 00:00:00 2001 From: yahtoo Date: Mon, 27 Nov 2017 16:45:24 +0800 Subject: [PATCH] Add rpc token authenticate function (#135) --- net/http/authn/authn.go | 157 +++++++++++++++++++++++++++++++++++++++++++ net/http/authn/authn_test.go | 56 +++++++++++++++ net/http/authn/context.go | 49 ++++++++++++++ node/node.go | 22 +++++- 4 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 net/http/authn/authn.go create mode 100644 net/http/authn/authn_test.go create mode 100644 net/http/authn/context.go diff --git a/net/http/authn/authn.go b/net/http/authn/authn.go new file mode 100644 index 00000000..2bda7759 --- /dev/null +++ b/net/http/authn/authn.go @@ -0,0 +1,157 @@ +package authn + +import ( + "context" + "crypto/x509" + "encoding/hex" + "net" + "net/http" + "strings" + "sync" + "time" + + "github.com/bytom/blockchain/accesstoken" + "github.com/bytom/errors" +) + +const tokenExpiry = time.Minute * 5 + +var loopbackOn = true + +var ( + //ErrInvalidToken is returned when authenticate is called with invalide token. + ErrInvalidToken = errors.New("invalid token") + //ErrNoToken is returned when authenticate is called with no token. + ErrNoToken = errors.New("no token") +) + +//API describe the token authenticate. +type API struct { + tokens *accesstoken.CredentialStore + crosscoreRPCPrefix string + rootCAs *x509.CertPool + + tokenMu sync.Mutex // protects the following + tokenMap map[string]tokenResult +} + +type tokenResult struct { + valid bool + lastLookup time.Time +} + +//NewAPI create a token authenticate object. +func NewAPI(tokens *accesstoken.CredentialStore) *API { + return &API{ + tokens: tokens, + tokenMap: make(map[string]tokenResult), + } +} + +// Authenticate returns the request, with added tokens and/or localhost +// flags in the context, as appropriate. +func (a *API) Authenticate(req *http.Request) (*http.Request, error) { + ctx := req.Context() + + token, err := a.tokenAuthn(req) + if err == nil && token != "" { + // if this request was successfully authenticated with a token, pass the token along + ctx = newContextWithToken(ctx, token) + } + local := a.localhostAuthn(req) + if local { + ctx = newContextWithLocalhost(ctx) + } + // Temporary workaround. Dashboard is always ok. + // See loopbackOn comment above. + if strings.HasPrefix(req.URL.Path, "/dashboard/") || req.URL.Path == "/dashboard" { + return req.WithContext(ctx), nil + } + if loopbackOn && local { + return req.WithContext(ctx), nil + } + + return req.WithContext(ctx), err +} + +// checks the request for a valid client cert list. +// If found, it is added to the request's context. +// Note that an *invalid* client cert is treated the +// same as no client cert -- it is omitted from the +// returned context, but the connection may proceed. +func certAuthn(req *http.Request, rootCAs *x509.CertPool) context.Context { + if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 { + certs := req.TLS.PeerCertificates + + // Same logic as serverHandshakeState.processCertsFromClient + // in $GOROOT/src/crypto/tls/handshake_server.go. + opts := x509.VerifyOptions{ + Roots: rootCAs, + CurrentTime: time.Now(), + Intermediates: x509.NewCertPool(), + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + + if _, err := certs[0].Verify(opts); err != nil { + // crypto/tls treats this as an error: + // errors.New("tls: failed to verify client's certificate: " + err.Error()) + // For us, it is ok; we want to treat it the same as if there + // were no client cert presented. + return req.Context() + } + + return context.WithValue(req.Context(), x509CertsKey, certs) + } + return req.Context() +} + +// returns true if this request is coming from a loopback address +func (a *API) localhostAuthn(req *http.Request) bool { + h, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + return false + } + if !net.ParseIP(h).IsLoopback() { + return false + } + return true +} + +func (a *API) tokenAuthn(req *http.Request) (string, error) { + user, pw, ok := req.BasicAuth() + if !ok { + return "", ErrNoToken + } + return user, a.cachedTokenAuthnCheck(req.Context(), user, pw) +} + +func (a *API) tokenAuthnCheck(ctx context.Context, user, pw string) (bool, error) { + pwBytes, err := hex.DecodeString(pw) + if err != nil { + return false, nil + } + return a.tokens.Check(ctx, user, pwBytes) +} + +func (a *API) cachedTokenAuthnCheck(ctx context.Context, user, pw string) error { + a.tokenMu.Lock() + res, ok := a.tokenMap[user+pw] + a.tokenMu.Unlock() + if !ok || time.Now().After(res.lastLookup.Add(tokenExpiry)) { + valid, err := a.tokenAuthnCheck(ctx, user, pw) + if err != nil { + return errors.Wrap(err) + } + res = tokenResult{valid: valid, lastLookup: time.Now()} + a.tokenMu.Lock() + a.tokenMap[user+pw] = res + a.tokenMu.Unlock() + } + if !res.valid { + return ErrInvalidToken + } + return nil +} diff --git a/net/http/authn/authn_test.go b/net/http/authn/authn_test.go new file mode 100644 index 00000000..e0e7d723 --- /dev/null +++ b/net/http/authn/authn_test.go @@ -0,0 +1,56 @@ +package authn + +import ( + "context" + "net/http" + "os" + "strings" + "testing" + + dbm "github.com/tendermint/tmlibs/db" + + "github.com/bytom/blockchain/accesstoken" + "github.com/bytom/errors" +) + +func TestAuthenticate(t *testing.T) { + ctx := context.Background() + + var token *string + tokenDB := dbm.NewDB("testdb", "leveldb", "temp") + defer os.RemoveAll("temp") + accessTokens := accesstoken.NewStore(tokenDB) + token, err := accessTokens.Create(ctx, "alice", "test") + if err != nil { + t.Errorf("create token error") + } + + cases := []struct { + id, tok string + want error + }{ + {"alice", *token, nil}, + {"alice", "alice:abcsdsdfassdfsefsfsfesfesfefsefa", ErrInvalidToken}, + } + + api := NewAPI(accessTokens) + + for _, c := range cases { + var username, password string + toks := strings.SplitN(c.tok, ":", 2) + if len(toks) > 0 { + username = toks[0] + } + if len(toks) > 1 { + password = toks[1] + } + + req, _ := http.NewRequest("GET", "/", nil) + req.SetBasicAuth(username, password) + + _, err := api.Authenticate(req) + if errors.Root(err) != c.want { + t.Errorf("Authenticate(%s) error = %s want %s", c.id, err, c.want) + } + } +} diff --git a/net/http/authn/context.go b/net/http/authn/context.go new file mode 100644 index 00000000..e2b1369f --- /dev/null +++ b/net/http/authn/context.go @@ -0,0 +1,49 @@ +package authn + +import ( + "context" + "crypto/x509" +) + +type key int + +const ( + tokenKey key = iota + localhostKey + x509CertsKey +) + +// X509Certs returns the cert stored in the context, if it exists. +func X509Certs(ctx context.Context) []*x509.Certificate { + c, _ := ctx.Value(x509CertsKey).([]*x509.Certificate) + return c +} + +// newContextWithToken sets the token in a new context and returns the context. +func newContextWithToken(ctx context.Context, token string) context.Context { + return context.WithValue(ctx, tokenKey, token) +} + +// Token returns the token stored in the context, if there is one. +func Token(ctx context.Context) string { + t, ok := ctx.Value(tokenKey).(string) + if !ok { + return "" + } + return t +} + +// newContextWithLocalhost sets the localhost flag to `true` in a new context +// and returns that context. +func newContextWithLocalhost(ctx context.Context) context.Context { + return context.WithValue(ctx, localhostKey, true) +} + +// Localhost returns true if the localhost flag has been set. +func Localhost(ctx context.Context) bool { + l, ok := ctx.Value(localhostKey).(bool) + if ok && l { + return true + } + return false +} diff --git a/node/node.go b/node/node.go index de4333d0..f5546fa6 100755 --- a/node/node.go +++ b/node/node.go @@ -28,6 +28,7 @@ import ( cfg "github.com/bytom/config" "github.com/bytom/env" "github.com/bytom/errors" + "github.com/bytom/net/http/authn" "github.com/bytom/p2p" "github.com/bytom/protocol" "github.com/bytom/types" @@ -86,7 +87,23 @@ func (wh *waitHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { wh.h.ServeHTTP(w, req) } -func rpcInit(h *bc.BlockchainReactor, config *cfg.Config) { +func AuthHandler(handler http.Handler, accessTokens *accesstoken.CredentialStore) http.Handler { + + authenticator := authn.NewAPI(accessTokens) + + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // TODO(tessr): check that this path exists; return early if this path isn't legit + req, err := authenticator.Authenticate(req) + if err != nil { + log.WithField("error", errors.Wrap(err, "Serve")).Error("Authenticate fail") + + return + } + handler.ServeHTTP(rw, req) + }) +} + +func rpcInit(h *bc.BlockchainReactor, config *cfg.Config, accessTokens *accesstoken.CredentialStore) { // The waitHandler accepts incoming requests, but blocks until its underlying // handler is set, when the second phase is complete. var coreHandler waitHandler @@ -95,6 +112,7 @@ func rpcInit(h *bc.BlockchainReactor, config *cfg.Config) { mux.Handle("/", &coreHandler) var handler http.Handler = mux + handler = AuthHandler(handler, accessTokens) handler = RedirectHandler(handler) secureheader.DefaultConfig.PermitClearLoopback = true @@ -215,7 +233,7 @@ func NewNode(config *cfg.Config) *Node { sw.AddReactor("BLOCKCHAIN", bcReactor) - rpcInit(bcReactor, config) + rpcInit(bcReactor, config, accessTokens) // Optionally, start the pex reactor var addrBook *p2p.AddrBook if config.P2P.PexReactor { -- 2.11.0