Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): add jwt support in auth middleware #15152

Merged
merged 1 commit into from
Sep 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 5 additions & 7 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
33 changes: 26 additions & 7 deletions http/authentication_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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
Expand All @@ -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(),
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}

Expand Down
95 changes: 95 additions & 0 deletions http/authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand All @@ -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 {
Expand All @@ -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()
Expand Down
27 changes: 19 additions & 8 deletions jsonweb/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()},
Expand Down Expand Up @@ -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 {
Expand Down