diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf3f04880d..fb58feac31c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ 1. [15151](https://github.com/influxdata/influxdb/pull/15151): Add jsonweb package for future JWT support 1. [15168](https://github.com/influxdata/influxdb/pull/15168): Added the JMeter Template dashboard +1. [15152](https://github.com/influxdata/influxdb/pull/15152): Add JWT support to http auth middleware ### UI Improvements diff --git a/auth.go b/auth.go index 296fac1be5a..3e3650aaf6d 100644 --- a/auth.go +++ b/auth.go @@ -8,13 +8,11 @@ import ( // AuthorizationKind is returned by (*Authorization).Kind(). const AuthorizationKind = "authorization" -var ( - // ErrUnableToCreateToken sanitized error message for all errors when a user cannot create a token - ErrUnableToCreateToken = &Error{ - Msg: "unable to create token", - Code: EInvalid, - } -) +// ErrUnableToCreateToken sanitized error message for all errors when a user cannot create a token +var ErrUnableToCreateToken = &Error{ + Msg: "unable to create token", + Code: EInvalid, +} // Authorization is an authorization. 🎉 type Authorization struct { diff --git a/http/authentication_middleware.go b/http/authentication_middleware.go index d75832535bf..91b64438eb4 100644 --- a/http/authentication_middleware.go +++ b/http/authentication_middleware.go @@ -8,6 +8,7 @@ import ( platform "github.com/influxdata/influxdb" platcontext "github.com/influxdata/influxdb/context" + "github.com/influxdata/influxdb/jsonweb" "github.com/julienschmidt/httprouter" "go.uber.org/zap" ) @@ -20,6 +21,7 @@ type AuthenticationHandler struct { AuthorizationService platform.AuthorizationService SessionService platform.SessionService UserService platform.UserService + TokenParser *jsonweb.TokenParser SessionRenewDisabled bool // This is only really used for it's lookup method the specific http @@ -35,6 +37,7 @@ func NewAuthenticationHandler(h platform.HTTPErrorHandler) *AuthenticationHandle Logger: zap.NewNop(), HTTPErrorHandler: h, Handler: http.DefaultServeMux, + TokenParser: jsonweb.NewTokenParser(jsonweb.EmptyKeyStore), noAuthRouter: httprouter.New(), } } @@ -100,16 +103,19 @@ func (h *AuthenticationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request return } - err = h.isUserActive(ctx, auth) - if err != nil { - InactiveUserError(ctx, h, w) - return + // jwt based auth is permission based rather than identity based + // and therefor has no associated user. if the user ID is invalid + // disregard the user active check + if auth.GetUserID().Valid() { + if err = h.isUserActive(ctx, auth); err != nil { + InactiveUserError(ctx, h, w) + return + } } ctx = platcontext.SetAuthorizer(ctx, auth) - r = r.WithContext(ctx) - h.Handler.ServeHTTP(w, r) + h.Handler.ServeHTTP(w, r.WithContext(ctx)) } func (h *AuthenticationHandler) isUserActive(ctx context.Context, auth platform.Authorizer) error { @@ -125,12 +131,25 @@ func (h *AuthenticationHandler) isUserActive(ctx context.Context, auth platform. return &platform.Error{Code: platform.EForbidden, Msg: "User is inactive"} } -func (h *AuthenticationHandler) extractAuthorization(ctx context.Context, r *http.Request) (*platform.Authorization, error) { +func (h *AuthenticationHandler) extractAuthorization(ctx context.Context, r *http.Request) (platform.Authorizer, error) { t, err := GetToken(r) if err != nil { return nil, err } + token, err := h.TokenParser.Parse(t) + if err == nil { + return token, nil + } + + // if the error returned signifies ths token is + // not a well formed JWT then use it as a lookup + // key for its associated authorization + // otherwise return the error + if !jsonweb.IsMalformedError(err) { + return nil, err + } + return h.AuthorizationService.FindAuthorizationByToken(ctx, t) } diff --git a/http/authentication_test.go b/http/authentication_test.go index 1df6ea2a391..e05e884d39f 100644 --- a/http/authentication_test.go +++ b/http/authentication_test.go @@ -2,21 +2,30 @@ package http_test import ( "context" + "errors" "fmt" "net/http" "net/http/httptest" "testing" "time" + influxdb "github.com/influxdata/influxdb" platform "github.com/influxdata/influxdb" platformhttp "github.com/influxdata/influxdb/http" + "github.com/influxdata/influxdb/jsonweb" "github.com/influxdata/influxdb/mock" ) +const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjbG91ZDIuaW5mbHV4ZGF0YS5jb20iLCJhdWQiOiJnYXRld2F5LmluZmx1eGRhdGEuY29tIiwiaWF0IjoxNTY4NjI4OTgwLCJraWQiOiJzb21lLWtleSIsInBlcm1pc3Npb25zIjpbeyJhY3Rpb24iOiJ3cml0ZSIsInJlc291cmNlIjp7InR5cGUiOiJidWNrZXRzIiwiaWQiOiIwMDAwMDAwMDAwMDAwMDAxIiwib3JnSUQiOiIwMDAwMDAwMDAwMDAwMDAyIn19XX0.74vjbExiOd702VSIMmQWaDT_GFvUI0-_P-SfQ_OOHB0" + +var one = influxdb.ID(1) + func TestAuthenticationHandler(t *testing.T) { type fields struct { AuthorizationService platform.AuthorizationService SessionService platform.SessionService + UserService platform.UserService + TokenParser *jsonweb.TokenParser } type args struct { token string @@ -103,6 +112,32 @@ func TestAuthenticationHandler(t *testing.T) { code: http.StatusUnauthorized, }, }, + { + name: "associated user is inactive", + fields: fields{ + AuthorizationService: &mock.AuthorizationService{ + FindAuthorizationByTokenFn: func(ctx context.Context, token string) (*platform.Authorization, error) { + return &platform.Authorization{UserID: one}, nil + }, + }, + SessionService: mock.NewSessionService(), + UserService: &mock.UserService{ + FindUserByIDFn: func(ctx context.Context, id platform.ID) (*platform.User, error) { + if !id.Valid() { + panic("user service should only be called with valid user ID") + } + + return &platform.User{Status: "inactive"}, nil + }, + }, + }, + args: args{ + token: "abc123", + }, + wants: wants{ + code: http.StatusForbidden, + }, + }, { name: "no auth provided", fields: fields{ @@ -114,6 +149,57 @@ func TestAuthenticationHandler(t *testing.T) { code: http.StatusUnauthorized, }, }, + { + name: "jwt provided", + fields: fields{ + AuthorizationService: &mock.AuthorizationService{ + FindAuthorizationByTokenFn: func(ctx context.Context, token string) (*platform.Authorization, error) { + return nil, fmt.Errorf("authorization not found") + }, + }, + SessionService: mock.NewSessionService(), + UserService: &mock.UserService{ + FindUserByIDFn: func(ctx context.Context, id platform.ID) (*platform.User, error) { + // ensure that this is not reached as jwt token authorizer produces + // invalid user id + if !id.Valid() { + panic("user service should only be called with valid user ID") + } + + return nil, errors.New("user not found") + }, + }, + TokenParser: jsonweb.NewTokenParser(jsonweb.KeyStoreFunc(func(string) ([]byte, error) { + return []byte("correct-key"), nil + })), + }, + args: args{ + token: token, + }, + wants: wants{ + code: http.StatusOK, + }, + }, + { + name: "jwt provided - bad signature", + fields: fields{ + AuthorizationService: &mock.AuthorizationService{ + FindAuthorizationByTokenFn: func(ctx context.Context, token string) (*platform.Authorization, error) { + panic("token lookup attempted") + }, + }, + SessionService: mock.NewSessionService(), + TokenParser: jsonweb.NewTokenParser(jsonweb.KeyStoreFunc(func(string) ([]byte, error) { + return []byte("incorrect-key"), nil + })), + }, + args: args{ + token: token, + }, + wants: wants{ + code: http.StatusUnauthorized, + }, + }, } for _, tt := range tests { @@ -130,6 +216,15 @@ func TestAuthenticationHandler(t *testing.T) { return &platform.User{}, nil }, } + + if tt.fields.UserService != nil { + h.UserService = tt.fields.UserService + } + + if tt.fields.TokenParser != nil { + h.TokenParser = tt.fields.TokenParser + } + h.Handler = handler w := httptest.NewRecorder() diff --git a/jsonweb/token.go b/jsonweb/token.go index e78382508f7..a64b292f96d 100644 --- a/jsonweb/token.go +++ b/jsonweb/token.go @@ -9,12 +9,16 @@ import ( const kind = "jwt" -// ErrKeyNotFound should be returned by a KeyStore when -// a key cannot be located for the provided key ID -var ErrKeyNotFound = errors.New("key not found") - -// ensure Token implements Authorizer -var _ influxdb.Authorizer = (*Token)(nil) +var ( + // ErrKeyNotFound should be returned by a KeyStore when + // a key cannot be located for the provided key ID + ErrKeyNotFound = errors.New("key not found") + + // EmptyKeyStore is a KeyStore implementation which contains no keys + EmptyKeyStore = KeyStoreFunc(func(string) ([]byte, error) { + return nil, ErrKeyNotFound + }) +) // KeyStore is a type which holds a set of keys accessed // via an id @@ -36,8 +40,8 @@ type TokenParser struct { // NewTokenParser returns a configured token parser used to // parse Token types from strings -func NewTokenParser(keyStore KeyStore) TokenParser { - return TokenParser{ +func NewTokenParser(keyStore KeyStore) *TokenParser { + return &TokenParser{ keyStore: keyStore, parser: &jwt.Parser{ ValidMethods: []string{jwt.SigningMethodHS256.Alg()}, @@ -70,6 +74,13 @@ func (t *TokenParser) Parse(v string) (*Token, error) { return token, nil } +// IsMalformedError returns true if the error returned represents +// a jwt malformed token error +func IsMalformedError(err error) bool { + verr, ok := err.(*jwt.ValidationError) + return ok && verr.Errors&jwt.ValidationErrorMalformed > 0 +} + // Token is a structure which is serialized as a json web token // It contains the necessary claims required to authorize type Token struct {