Skip to content

Commit

Permalink
feat(auth): add jwt support in auth middleware (#15152)
Browse files Browse the repository at this point in the history
  • Loading branch information
GeorgeMac authored Sep 27, 2019
1 parent b96282d commit 9f5390e
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 22 deletions.
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

0 comments on commit 9f5390e

Please sign in to comment.