From 4e2605e98d2fb146c18e9aa256c678ae4bfa3e41 Mon Sep 17 00:00:00 2001 From: David Norton Date: Wed, 11 May 2016 21:42:45 -0400 Subject: [PATCH 1/6] feat #4448: add support for JWT tokens --- services/httpd/config.go | 1 + services/httpd/config_test.go | 2 +- services/httpd/handler.go | 171 +++++++++++++++++++++++--------- services/httpd/handler_test.go | 174 ++++++++++++++++++++++++++++++++- services/httpd/service.go | 20 ++-- 5 files changed, 307 insertions(+), 61 deletions(-) diff --git a/services/httpd/config.go b/services/httpd/config.go index 530cb7b7d65..82199a43f0d 100644 --- a/services/httpd/config.go +++ b/services/httpd/config.go @@ -14,6 +14,7 @@ type Config struct { HTTPSEnabled bool `toml:"https-enabled"` HTTPSCertificate string `toml:"https-certificate"` MaxRowLimit int `toml:"max-row-limit"` + SharedSecret string `toml:"shared-secret"` } // NewConfig returns a new Config with default settings. diff --git a/services/httpd/config_test.go b/services/httpd/config_test.go index 4e9103afe70..7e014b3ca6a 100644 --- a/services/httpd/config_test.go +++ b/services/httpd/config_test.go @@ -46,7 +46,7 @@ https-certificate = "/dev/null" func TestConfig_WriteTracing(t *testing.T) { c := httpd.Config{WriteTracing: true} s := httpd.NewService(c) - if !s.Handler.WriteTrace { + if !s.Handler.Config.WriteTracing { t.Fatalf("write tracing was not set") } } diff --git a/services/httpd/handler.go b/services/httpd/handler.go index a7c80aa6fe2..707f6db8a88 100644 --- a/services/httpd/handler.go +++ b/services/httpd/handler.go @@ -17,6 +17,7 @@ import ( "time" "github.com/bmizerany/pat" + "github.com/dgrijalva/jwt-go" "github.com/influxdata/influxdb" "github.com/influxdata/influxdb/influxql" "github.com/influxdata/influxdb/models" @@ -33,6 +34,13 @@ const ( DefaultChunkSize = 10000 ) +type AuthenticationMethod int + +const ( + UserAuthentication AuthenticationMethod = iota + BearerAuthentication +) + // TODO: Standard response headers (see: HeaderHandler) // TODO: Compression (see: CompressionHeaderHandler) @@ -49,14 +57,14 @@ type Route struct { // Handler represents an HTTP handler for the InfluxDB server. type Handler struct { - mux *pat.PatternServeMux - requireAuthentication bool - Version string + mux *pat.PatternServeMux + Version string MetaClient interface { Database(name string) *meta.DatabaseInfo Authenticate(username, password string) (ui *meta.UserInfo, err error) Users() []meta.UserInfo + User(username string) (*meta.UserInfo, error) } QueryAuthorizer interface { @@ -75,23 +83,18 @@ type Handler struct { ContinuousQuerier continuous_querier.ContinuousQuerier - Logger *log.Logger - loggingEnabled bool // Log every HTTP access. - WriteTrace bool // Detailed logging of write path - rowLimit int - statMap *expvar.Map + Config *Config + Logger *log.Logger + statMap *expvar.Map } // NewHandler returns a new instance of handler with routes. -func NewHandler(requireAuthentication, loggingEnabled, writeTrace bool, rowLimit int, statMap *expvar.Map) *Handler { +func NewHandler(c Config, statMap *expvar.Map) *Handler { h := &Handler{ - mux: pat.New(), - requireAuthentication: requireAuthentication, - Logger: log.New(os.Stderr, "[http] ", log.LstdFlags), - loggingEnabled: loggingEnabled, - WriteTrace: writeTrace, - rowLimit: rowLimit, - statMap: statMap, + mux: pat.New(), + Config: &c, + Logger: log.New(os.Stderr, "[http] ", log.LstdFlags), + statMap: statMap, } h.AddRoutes([]Route{ @@ -148,7 +151,7 @@ func (h *Handler) AddRoutes(routes ...Route) { // If it's a handler func that requires authorization, wrap it in authorization if hf, ok := r.HandlerFunc.(func(http.ResponseWriter, *http.Request, *meta.UserInfo)); ok { - handler = authenticate(hf, h, h.requireAuthentication) + handler = authenticate(hf, h, h.Config.AuthEnabled) } // This is a normal handler signature and does not require authorization if hf, ok := r.HandlerFunc.(func(http.ResponseWriter, *http.Request)); ok { @@ -161,7 +164,7 @@ func (h *Handler) AddRoutes(routes ...Route) { handler = versionHeader(handler, h) handler = cors(handler) handler = requestID(handler) - if h.loggingEnabled && r.LoggingEnabled { + if h.Config.LogEnabled && r.LoggingEnabled { handler = h.logging(handler, r.Name) } handler = h.recovery(handler, r.Name) // make sure recovery is always last @@ -272,7 +275,7 @@ func (h *Handler) serveQuery(w http.ResponseWriter, r *http.Request, user *meta. } // Check authorization. - if h.requireAuthentication { + if h.Config.AuthEnabled { if err := h.QueryAuthorizer.AuthorizeQuery(user, query, db); err != nil { if err, ok := err.(meta.ErrAuthorize); ok { h.Logger.Printf("unauthorized request | user: %q | query: %q | database %q\n", err.User, err.Query.String(), err.Database) @@ -357,7 +360,7 @@ func (h *Handler) serveQuery(w http.ResponseWriter, r *http.Request, user *meta. // If you want to return more than the default chunk size, then use chunking // to process multiple blobs. rows += len(r.Series) - if h.rowLimit > 0 && rows > h.rowLimit { + if h.Config.MaxRowLimit > 0 && rows > h.Config.MaxRowLimit { break } @@ -424,12 +427,12 @@ func (h *Handler) serveWrite(w http.ResponseWriter, r *http.Request, user *meta. return } - if h.requireAuthentication && user == nil { + if h.Config.AuthEnabled && user == nil { resultError(w, influxql.Result{Err: fmt.Errorf("user is required to write to database %q", database)}, http.StatusUnauthorized) return } - if h.requireAuthentication { + if h.Config.AuthEnabled { if err := h.WriteAuthorizer.AuthorizeWrite(user.Name, database); err != nil { resultError(w, influxql.Result{Err: fmt.Errorf("%q user is not authorized to write to database %q", user.Name, database)}, http.StatusUnauthorized) return @@ -460,7 +463,7 @@ func (h *Handler) serveWrite(w http.ResponseWriter, r *http.Request, user *meta. _, err := buf.ReadFrom(body) if err != nil { - if h.WriteTrace { + if h.Config.WriteTracing { h.Logger.Print("write handler unable to read bytes from request body") } resultError(w, influxql.Result{Err: err}, http.StatusBadRequest) @@ -468,7 +471,7 @@ func (h *Handler) serveWrite(w http.ResponseWriter, r *http.Request, user *meta. } h.statMap.Add(statWriteRequestBytesReceived, int64(buf.Len())) - if h.WriteTrace { + if h.Config.WriteTracing { h.Logger.Printf("write body received by handler: %s", buf.Bytes()) } @@ -615,21 +618,53 @@ func resultError(w http.ResponseWriter, result influxql.Result, code int) { // Filters and filter helpers -// parseCredentials returns the username and password encoded in -// a request. The credentials may be present as URL query params, or as -// a Basic Authentication header. -// as params: http://127.0.0.1/query?u=username&p=password -// as basic auth: http://username:password@127.0.0.1 -func parseCredentials(r *http.Request) (string, string, error) { +type credentials struct { + Method AuthenticationMethod + Username string + Password string + Token string +} + +// parseCredentials parses a request and returns the authentication credentials. +// The credentials may be present as URL query params, or as a Basic +// Authentication header. +// As params: http://127.0.0.1/query?u=username&p=password +// As basic auth: http://username:password@127.0.0.1 +// As Bearer token in Authorization header: Bearer +func parseCredentials(r *http.Request) (*credentials, error) { q := r.URL.Query() - if u, p := q.Get("u"), q.Get("p"); u != "" && p != "" { - return u, p, nil + // Check for the HTTP Authorization header. + if s := r.Header.Get("Authorization"); s != "" { + // Check for Bearer token. + strs := strings.Split(s, " ") + if len(strs) == 2 && strs[0] == "Bearer" { + return &credentials{ + Method: BearerAuthentication, + Token: strs[1], + }, nil + } + + // Check for basic auth. + if u, p, ok := r.BasicAuth(); ok { + return &credentials{ + Method: UserAuthentication, + Username: u, + Password: p, + }, nil + } } - if u, p, ok := r.BasicAuth(); ok { - return u, p, nil + + // Check for username and password in URL params. + if u, p := q.Get("u"), q.Get("p"); u != "" && p != "" { + return &credentials{ + Method: UserAuthentication, + Username: u, + Password: p, + }, nil } - return "", "", fmt.Errorf("unable to parse Basic Auth credentials") + + return nil, fmt.Errorf("unable to parse Basic Auth credentials") } // authenticate wraps a handler and ensures that if user credentials are passed in @@ -651,24 +686,68 @@ func authenticate(inner func(http.ResponseWriter, *http.Request, *meta.UserInfo) // TODO corylanou: never allow this in the future without users if requireAuthentication && len(uis) > 0 { - username, password, err := parseCredentials(r) + creds, err := parseCredentials(r) if err != nil { h.statMap.Add(statAuthFail, 1) httpError(w, err.Error(), false, http.StatusUnauthorized) return } - if username == "" { - h.statMap.Add(statAuthFail, 1) - httpError(w, "username required", false, http.StatusUnauthorized) - return - } - user, err = h.MetaClient.Authenticate(username, password) - if err != nil { - h.statMap.Add(statAuthFail, 1) - httpError(w, err.Error(), false, http.StatusUnauthorized) - return + switch creds.Method { + case UserAuthentication: + if creds.Username == "" { + h.statMap.Add(statAuthFail, 1) + httpError(w, "username required", false, http.StatusUnauthorized) + return + } + + user, err = h.MetaClient.Authenticate(creds.Username, creds.Password) + if err != nil { + h.statMap.Add(statAuthFail, 1) + httpError(w, err.Error(), false, http.StatusUnauthorized) + return + } + case BearerAuthentication: + keyLookupFn := func(token *jwt.Token) (interface{}, error) { + // Check for expected signing method. + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(h.Config.SharedSecret), nil + } + + // Parse and validate the token. + token, err := jwt.Parse(creds.Token, keyLookupFn) + if err != nil { + httpError(w, err.Error(), false, http.StatusUnauthorized) + return + } else if !token.Valid { + httpError(w, "invalid token", false, http.StatusUnauthorized) + return + } + + // Get the username from the token. + username, ok := token.Claims["username"].(string) + if !ok { + httpError(w, "username in token must be a string", false, http.StatusUnauthorized) + return + } else if username == "" { + httpError(w, "token must contain a username", false, http.StatusUnauthorized) + return + } + + // Lookup user in the metastore. + if user, err = h.MetaClient.User(username); err != nil { + httpError(w, err.Error(), false, http.StatusUnauthorized) + return + } else if user == nil { + httpError(w, meta.ErrUserNotFound.Error(), false, http.StatusUnauthorized) + return + } + default: + httpError(w, "unsupported authentication", false, http.StatusUnauthorized) } + } inner(w, r, user) }) diff --git a/services/httpd/handler_test.go b/services/httpd/handler_test.go index fe608937921..eac10fbc0fe 100644 --- a/services/httpd/handler_test.go +++ b/services/httpd/handler_test.go @@ -3,6 +3,7 @@ package httpd_test import ( "bytes" "errors" + "fmt" "io" "net/http" "net/http/httptest" @@ -10,6 +11,7 @@ import ( "testing" "time" + "github.com/dgrijalva/jwt-go" "github.com/influxdata/influxdb" "github.com/influxdata/influxdb/influxql" "github.com/influxdata/influxdb/models" @@ -40,6 +42,139 @@ func TestHandler_Query(t *testing.T) { } } +// Test query with user authentication. +func TestHandler_Query_Auth(t *testing.T) { + // Create the handler to be tested. + h := NewHandler(true) + + // Set mock meta client functions for the handler to use. + h.MetaClient.UsersFn = func() []meta.UserInfo { + return []meta.UserInfo{ + { + Name: "user1", + Hash: "abcd", + Admin: true, + Privileges: make(map[string]influxql.Privilege), + }, + } + } + + h.MetaClient.UserFn = func(username string) (*meta.UserInfo, error) { + if username != "user1" { + return nil, meta.ErrUserNotFound + } + return &meta.UserInfo{ + Name: "user1", + Hash: "abcd", + Admin: true, + }, nil + } + + h.MetaClient.AuthenticateFn = func(u, p string) (*meta.UserInfo, error) { + if u != "user1" { + return nil, fmt.Errorf("unexpected user: exp: user1, got: %s", u) + } else if p != "abcd" { + return nil, fmt.Errorf("unexpected password: exp: abcd, got: %s", p) + } + return h.MetaClient.User(u) + } + + // Set mock query authorizer for handler to use. + h.QueryAuthorizer.AuthorizeQueryFn = func(u *meta.UserInfo, query *influxql.Query, database string) error { + return nil + } + + // Set mock statement executor for handler to use. + h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx *influxql.ExecutionContext) error { + if stmt.String() != `SELECT * FROM bar` { + t.Fatalf("unexpected query: %s", stmt.String()) + } else if ctx.Database != `foo` { + t.Fatalf("unexpected db: %s", ctx.Database) + } + ctx.Results <- &influxql.Result{StatementID: 1, Series: models.Rows([]*models.Row{{Name: "series0"}})} + ctx.Results <- &influxql.Result{StatementID: 2, Series: models.Rows([]*models.Row{{Name: "series1"}})} + return nil + } + + // Test the handler with valid user and password in the URL parameters. + w := httptest.NewRecorder() + h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?u=user1&p=abcd&db=foo&q=SELECT+*+FROM+bar", nil)) + if w.Code != http.StatusOK { + t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String()) + } else if w.Body.String() != `{"results":[{"series":[{"name":"series0"}]},{"series":[{"name":"series1"}]}]}` { + t.Fatalf("unexpected body: %s", w.Body.String()) + } + + // Test the handler with valid JWT bearer token. + req := MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar", nil) + // Create a signed JWT token string and add it to the request header. + _, signedToken := MustJWTToken("user1", h.Config.SharedSecret, false) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedToken)) + + w = httptest.NewRecorder() + h.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String()) + } else if w.Body.String() != `{"results":[{"series":[{"name":"series0"}]},{"series":[{"name":"series1"}]}]}` { + t.Fatalf("unexpected body: %s", w.Body.String()) + } + + // Test the handler with JWT token signed with invalid key. + req = MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar", nil) + // Create a signed JWT token string and add it to the request header. + _, signedToken = MustJWTToken("user1", "invalid key", false) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedToken)) + + w = httptest.NewRecorder() + h.ServeHTTP(w, req) + if w.Code != http.StatusUnauthorized { + t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String()) + } else if w.Body.String() != `{"error":"signature is invalid"}` { + t.Fatalf("unexpected body: %s", w.Body.String()) + } + + // Test handler with valid JWT token carrying non-existant user. + _, signedToken = MustJWTToken("bad_user", h.Config.SharedSecret, false) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedToken)) + + w = httptest.NewRecorder() + h.ServeHTTP(w, req) + if w.Code != http.StatusUnauthorized { + t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String()) + } else if w.Body.String() != `{"error":"user not found"}` { + t.Fatalf("unexpected body: %s", w.Body.String()) + } + + // Test handler with expired JWT token. + _, signedToken = MustJWTToken("user1", h.Config.SharedSecret, true) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedToken)) + + w = httptest.NewRecorder() + h.ServeHTTP(w, req) + if w.Code != http.StatusUnauthorized { + t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String()) + } else if w.Body.String() != `{"error":"token is expired by 1s"}` { + t.Fatalf("unexpected body: %s", w.Body.String()) + } + + // Test handler with JWT token that has no expiration set. + token, _ := MustJWTToken("user1", h.Config.SharedSecret, false) + delete(token.Claims, "exp") + signedToken, err := token.SignedString([]byte(h.Config.SharedSecret)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedToken)) + + w = httptest.NewRecorder() + h.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String()) + } else if w.Body.String() != `{"results":[{"series":[{"name":"series0"}]},{"series":[{"name":"series1"}]}]}` { + t.Fatalf("unexpected body: %s", w.Body.String()) + } +} + // Ensure the handler returns results from a query (including nil results). func TestHandler_QueryRegex(t *testing.T) { h := NewHandler(false) @@ -282,17 +417,24 @@ type Handler struct { *httpd.Handler MetaClient HandlerMetaStore StatementExecutor HandlerStatementExecutor + QueryAuthorizer HandlerQueryAuthorizer } // NewHandler returns a new instance of Handler. func NewHandler(requireAuthentication bool) *Handler { statMap := influxdb.NewStatistics("httpd", "httpd", nil) + + config := httpd.NewConfig() + config.AuthEnabled = requireAuthentication + config.SharedSecret = "super secret key" + h := &Handler{ - Handler: httpd.NewHandler(requireAuthentication, true, false, 0, statMap), + Handler: httpd.NewHandler(config, statMap), } h.Handler.MetaClient = &h.MetaClient h.Handler.QueryExecutor = influxql.NewQueryExecutor() h.Handler.QueryExecutor.StatementExecutor = &h.StatementExecutor + h.Handler.QueryAuthorizer = &h.QueryAuthorizer h.Handler.Version = "0.0.0" return h } @@ -303,6 +445,7 @@ type HandlerMetaStore struct { DatabaseFn func(name string) *meta.DatabaseInfo AuthenticateFn func(username, password string) (ui *meta.UserInfo, err error) UsersFn func() []meta.UserInfo + UserFn func(username string) (*meta.UserInfo, error) } func (s *HandlerMetaStore) Ping(b bool) error { @@ -325,6 +468,10 @@ func (s *HandlerMetaStore) Users() []meta.UserInfo { return s.UsersFn() } +func (s *HandlerMetaStore) User(username string) (*meta.UserInfo, error) { + return s.UserFn(username) +} + // HandlerStatementExecutor is a mock implementation of Handler.StatementExecutor. type HandlerStatementExecutor struct { ExecuteStatementFn func(stmt influxql.Statement, ctx *influxql.ExecutionContext) error @@ -338,6 +485,15 @@ func (e *HandlerStatementExecutor) NormalizeStatement(stmt influxql.Statement, d return nil } +// HandlerQueryAuthorizer is a mock implementation of Handler.QueryAuthorizer. +type HandlerQueryAuthorizer struct { + AuthorizeQueryFn func(u *meta.UserInfo, query *influxql.Query, database string) error +} + +func (a *HandlerQueryAuthorizer) AuthorizeQuery(u *meta.UserInfo, query *influxql.Query, database string) error { + return a.AuthorizeQueryFn(u, query, database) +} + // MustNewRequest returns a new HTTP request. Panic on error. func MustNewRequest(method, urlStr string, body io.Reader) *http.Request { r, err := http.NewRequest(method, urlStr, body) @@ -368,3 +524,19 @@ func NewResultChan(results ...*influxql.Result) <-chan *influxql.Result { close(ch) return ch } + +// MustJWTToken returns a new JWT token and signed string or panics trying. +func MustJWTToken(username, secret string, expired bool) (*jwt.Token, string) { + token := jwt.New(jwt.GetSigningMethod("HS512")) + token.Claims["username"] = username + if expired { + token.Claims["exp"] = time.Now().Add(-time.Second).Unix() + } else { + token.Claims["exp"] = time.Now().Add(time.Second * 10).Unix() + } + signed, err := token.SignedString([]byte(secret)) + if err != nil { + panic(err) + } + return token, signed +} diff --git a/services/httpd/service.go b/services/httpd/service.go index 55f5b5887d9..b228d6bb923 100644 --- a/services/httpd/service.go +++ b/services/httpd/service.go @@ -57,18 +57,12 @@ func NewService(c Config) *Service { statMap := influxdb.NewStatistics(key, "httpd", tags) s := &Service{ - addr: c.BindAddress, - https: c.HTTPSEnabled, - cert: c.HTTPSCertificate, - err: make(chan error), - Handler: NewHandler( - c.AuthEnabled, - c.LogEnabled, - c.WriteTracing, - c.MaxRowLimit, - statMap, - ), - Logger: log.New(os.Stderr, "[httpd] ", log.LstdFlags), + addr: c.BindAddress, + https: c.HTTPSEnabled, + cert: c.HTTPSCertificate, + err: make(chan error), + Handler: NewHandler(c, statMap), + Logger: log.New(os.Stderr, "[httpd] ", log.LstdFlags), } s.Handler.Logger = s.Logger return s @@ -77,7 +71,7 @@ func NewService(c Config) *Service { // Open starts the service func (s *Service) Open() error { s.Logger.Println("Starting HTTP service") - s.Logger.Println("Authentication enabled:", s.Handler.requireAuthentication) + s.Logger.Println("Authentication enabled:", s.Handler.Config.AuthEnabled) // Open listener. if s.https { From efba07993add8c97b706de058806e7faa39e4c95 Mon Sep 17 00:00:00 2001 From: David Norton Date: Wed, 11 May 2016 22:54:52 -0400 Subject: [PATCH 2/6] update Godeps: add jwt-go --- Godeps | 1 + 1 file changed, 1 insertion(+) diff --git a/Godeps b/Godeps index 1c9f6221077..c9bfd49bf97 100644 --- a/Godeps +++ b/Godeps @@ -4,6 +4,7 @@ github.com/armon/go-metrics 345426c77237ece5dab0e1605c3e4b35c3f54757 github.com/bmizerany/pat b8a35001b773c267eb260a691f4e5499a3531600 github.com/boltdb/bolt 2f846c3551b76d7710f159be840d66c3d064abbe github.com/davecgh/go-spew fc32781af5e85e548d3f1abaf0fa3dbe8a72495c +github.com/dgrijalva/jwt-go a2c85815a77d0f951e33ba4db5ae93629a1530af github.com/dgryski/go-bits 86c69b3c986f9d40065df5bd8f765796549eef2e github.com/dgryski/go-bitstream 27cd5973303fde7d914860be1ea4b927a6be0c92 github.com/gogo/protobuf 74b6e9deaff6ba6da1389ec97351d337f0d08b06 From 5842e206ae810f695c7d8ef8c6ab383fa8c832cb Mon Sep 17 00:00:00 2001 From: David Norton Date: Wed, 11 May 2016 23:05:47 -0400 Subject: [PATCH 3/6] bump token expiration time up in test --- services/httpd/handler_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/httpd/handler_test.go b/services/httpd/handler_test.go index eac10fbc0fe..8dfb980a7e9 100644 --- a/services/httpd/handler_test.go +++ b/services/httpd/handler_test.go @@ -532,7 +532,7 @@ func MustJWTToken(username, secret string, expired bool) (*jwt.Token, string) { if expired { token.Claims["exp"] = time.Now().Add(-time.Second).Unix() } else { - token.Claims["exp"] = time.Now().Add(time.Second * 10).Unix() + token.Claims["exp"] = time.Now().Add(time.Minute * 10).Unix() } signed, err := token.SignedString([]byte(secret)) if err != nil { From d42f5f8062a062ff910a5e04fa4c7139570d1220 Mon Sep 17 00:00:00 2001 From: David Norton Date: Wed, 11 May 2016 23:26:58 -0400 Subject: [PATCH 4/6] require token expiration --- services/httpd/handler.go | 6 ++++++ services/httpd/handler_test.go | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/services/httpd/handler.go b/services/httpd/handler.go index 707f6db8a88..243cd17d38c 100644 --- a/services/httpd/handler.go +++ b/services/httpd/handler.go @@ -726,6 +726,12 @@ func authenticate(inner func(http.ResponseWriter, *http.Request, *meta.UserInfo) return } + // Make sure an expiration was set on the token. + if exp, ok := token.Claims["exp"].(float64); !ok || exp <= 0.0 { + httpError(w, "token expiration required", false, http.StatusUnauthorized) + return + } + // Get the username from the token. username, ok := token.Claims["username"].(string) if !ok { diff --git a/services/httpd/handler_test.go b/services/httpd/handler_test.go index 8dfb980a7e9..5758fda4e1f 100644 --- a/services/httpd/handler_test.go +++ b/services/httpd/handler_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "regexp" + "strings" "testing" "time" @@ -153,7 +154,7 @@ func TestHandler_Query_Auth(t *testing.T) { h.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String()) - } else if w.Body.String() != `{"error":"token is expired by 1s"}` { + } else if !strings.Contains(w.Body.String(), `{"error":"token is expired`) { t.Fatalf("unexpected body: %s", w.Body.String()) } @@ -165,12 +166,11 @@ func TestHandler_Query_Auth(t *testing.T) { t.Fatal(err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signedToken)) - w = httptest.NewRecorder() h.ServeHTTP(w, req) - if w.Code != http.StatusOK { + if w.Code != http.StatusUnauthorized { t.Fatalf("unexpected status: %d: %s", w.Code, w.Body.String()) - } else if w.Body.String() != `{"results":[{"series":[{"name":"series0"}]},{"series":[{"name":"series1"}]}]}` { + } else if w.Body.String() != `{"error":"token expiration required"}` { t.Fatalf("unexpected body: %s", w.Body.String()) } } From 2080cfb35df7706d0f351e547ed9d12f10f4e6b4 Mon Sep 17 00:00:00 2001 From: David Norton Date: Thu, 12 May 2016 08:05:37 -0400 Subject: [PATCH 5/6] improve error message --- services/httpd/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/httpd/handler.go b/services/httpd/handler.go index 243cd17d38c..297e579088d 100644 --- a/services/httpd/handler.go +++ b/services/httpd/handler.go @@ -664,7 +664,7 @@ func parseCredentials(r *http.Request) (*credentials, error) { }, nil } - return nil, fmt.Errorf("unable to parse Basic Auth credentials") + return nil, fmt.Errorf("unable to parse authentication credentials") } // authenticate wraps a handler and ensures that if user credentials are passed in From c0a772c068e48da4105a972200fc1ba97520588e Mon Sep 17 00:00:00 2001 From: David Norton Date: Thu, 12 May 2016 08:19:49 -0400 Subject: [PATCH 6/6] update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d264a64789..5f92a09123a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Features - [#3541](https://github.com/influxdata/influxdb/issues/3451): Update SHOW FIELD KEYS to return the field type with the field key. +- [#6609](https://github.com/influxdata/influxdb/pull/6609): Add support for JWT token authentication. ### Bugfixes