From ddce5d383ad40bfa5a0b7a083129ae8fc0b636e7 Mon Sep 17 00:00:00 2001 From: George Date: Thu, 19 Sep 2019 12:31:40 +0100 Subject: [PATCH] feat(auth): add new jsonweb package (#15151) --- CHANGELOG.md | 2 + jsonweb/token.go | 115 ++++++++++++++++++++++++++++++++++++++++++ jsonweb/token_test.go | 85 +++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 jsonweb/token.go create mode 100644 jsonweb/token_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f95b6dee974..06928df7c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +1. [15151](https://github.com/influxdata/influxdb/pull/15151): Add jsonweb package for future JWT support + ### UI Improvements 1. [15099](https://github.com/influxdata/influxdb/pull/15099): Add viewport scaling to html meta for responsive mobile scaling diff --git a/jsonweb/token.go b/jsonweb/token.go new file mode 100644 index 00000000000..e78382508f7 --- /dev/null +++ b/jsonweb/token.go @@ -0,0 +1,115 @@ +package jsonweb + +import ( + "errors" + + "github.com/dgrijalva/jwt-go" + "github.com/influxdata/influxdb" +) + +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) + +// KeyStore is a type which holds a set of keys accessed +// via an id +type KeyStore interface { + Key(string) ([]byte, error) +} + +// KeyStoreFunc is a function which can be used as a KeyStore +type KeyStoreFunc func(string) ([]byte, error) + +// Key delegates to the receiver KeyStoreFunc +func (k KeyStoreFunc) Key(v string) ([]byte, error) { return k(v) } + +// TokenParser is a type which can parse and validate tokens +type TokenParser struct { + keyStore KeyStore + parser *jwt.Parser +} + +// NewTokenParser returns a configured token parser used to +// parse Token types from strings +func NewTokenParser(keyStore KeyStore) TokenParser { + return TokenParser{ + keyStore: keyStore, + parser: &jwt.Parser{ + ValidMethods: []string{jwt.SigningMethodHS256.Alg()}, + }, + } +} + +// Parse takes a string then parses and validates it as a jwt based on +// the key described within the token +func (t *TokenParser) Parse(v string) (*Token, error) { + jwt, err := t.parser.ParseWithClaims(v, &Token{}, func(jwt *jwt.Token) (interface{}, error) { + token, ok := jwt.Claims.(*Token) + if !ok { + return nil, errors.New("missing kid in token claims") + } + + // fetch key for "kid" from key store + return t.keyStore.Key(token.KeyID) + }) + + if err != nil { + return nil, err + } + + token, ok := jwt.Claims.(*Token) + if !ok { + return nil, errors.New("token is unexpected type") + } + + return token, nil +} + +// Token is a structure which is serialized as a json web token +// It contains the necessary claims required to authorize +type Token struct { + jwt.StandardClaims + // KeyID is the identifier of the key used to sign the token + KeyID string `json:"kid"` + // Permissions is the set of authorized permissions for the token + Permissions []influxdb.Permission `json:"permissions"` +} + +// Allowed returns whether or not a permission is allowed based +// on the set of permissions within the Token +func (t *Token) Allowed(p influxdb.Permission) bool { + if err := p.Valid(); err != nil { + return false + } + + for _, perm := range t.Permissions { + if perm.Matches(p) { + return true + } + } + + return false +} + +// Identifier returns the identifier for this Token +// as found in the standard claims +func (t *Token) Identifier() influxdb.ID { + id, _ := influxdb.IDFromString(t.Id) + return *id +} + +// GetUserID returns an invalid id as tokens are generated +// with permissions rather than for or by a particular user +func (t *Token) GetUserID() influxdb.ID { + return influxdb.InvalidID() +} + +// Kind returns the string "jwt" which is used for auditing +func (t *Token) Kind() string { + return kind +} diff --git a/jsonweb/token_test.go b/jsonweb/token_test.go new file mode 100644 index 00000000000..350509405a6 --- /dev/null +++ b/jsonweb/token_test.go @@ -0,0 +1,85 @@ +package jsonweb + +import ( + "reflect" + "testing" + + "github.com/dgrijalva/jwt-go" + "github.com/google/go-cmp/cmp" + "github.com/influxdata/influxdb" +) + +var ( + one = influxdb.ID(1) + two = influxdb.ID(2) + keyStore = KeyStoreFunc(func(kid string) ([]byte, error) { + if kid != "some-key" { + return nil, ErrKeyNotFound + } + + return []byte("correct-key"), nil + }) +) + +func Test_TokenParser(t *testing.T) { + for _, test := range []struct { + name string + keyStore KeyStore + input string + // expectations + token *Token + err error + }{ + { + name: "happy path", + input: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjbG91ZDIuaW5mbHV4ZGF0YS5jb20iLCJhdWQiOiJnYXRld2F5LmluZmx1eGRhdGEuY29tIiwiaWF0IjoxNTY4NjI4OTgwLCJraWQiOiJzb21lLWtleSIsInBlcm1pc3Npb25zIjpbeyJhY3Rpb24iOiJ3cml0ZSIsInJlc291cmNlIjp7InR5cGUiOiJidWNrZXRzIiwiaWQiOiIwMDAwMDAwMDAwMDAwMDAxIiwib3JnSUQiOiIwMDAwMDAwMDAwMDAwMDAyIn19XX0.74vjbExiOd702VSIMmQWaDT_GFvUI0-_P-SfQ_OOHB0", + token: &Token{ + StandardClaims: jwt.StandardClaims{ + Issuer: "cloud2.influxdata.com", + Audience: "gateway.influxdata.com", + IssuedAt: 1568628980, + }, + KeyID: "some-key", + Permissions: []influxdb.Permission{ + { + Action: influxdb.WriteAction, + Resource: influxdb.Resource{ + Type: influxdb.BucketsResourceType, + ID: &one, + OrgID: &two, + }, + }, + }, + }, + }, + { + name: "key not found", + input: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjbG91ZDIuaW5mbHV4ZGF0YS5jb20iLCJhdWQiOiJnYXRld2F5LmluZmx1eGRhdGEuY29tIiwiaWF0IjoxNTY4NjMxMTQ0LCJraWQiOiJzb21lLW90aGVyLWtleSIsInBlcm1pc3Npb25zIjpbeyJhY3Rpb24iOiJyZWFkIiwicmVzb3VyY2UiOnsidHlwZSI6InRhc2tzIiwiaWQiOiIwMDAwMDAwMDAwMDAwMDAzIiwib3JnSUQiOiIwMDAwMDAwMDAwMDAwMDA0In19XX0.QVXJ3kGP1gsxisNZe7QmphXox-vjZr6MAMbd00CQlfA", + err: &jwt.ValidationError{ + Inner: ErrKeyNotFound, + Errors: jwt.ValidationErrorUnverifiable, + }, + }, + { + name: "invalid signature", + input: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjbG91ZDIuaW5mbHV4ZGF0YS5jb20iLCJhdWQiOiJnYXRld2F5LmluZmx1eGRhdGEuY29tIiwiaWF0IjoxNTY4NjMxMTQ0LCJraWQiOiJzb21lLWtleSIsInBlcm1pc3Npb25zIjpbeyJhY3Rpb24iOiJyZWFkIiwicmVzb3VyY2UiOnsidHlwZSI6InRhc2tzIiwiaWQiOiIwMDAwMDAwMDAwMDAwMDAzIiwib3JnSUQiOiIwMDAwMDAwMDAwMDAwMDA0In19XX0.RwmNs5u6NnjNq9xTdAIERFrI5ow-6lJpND3jRrTwkaE", + err: &jwt.ValidationError{ + Inner: jwt.ErrSignatureInvalid, + Errors: jwt.ValidationErrorSignatureInvalid, + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + parser := NewTokenParser(keyStore) + + token, err := parser.Parse(test.input) + if !reflect.DeepEqual(test.err, err) { + t.Errorf("expected %[1]s (%#[1]v), got %[2]s (%#[2]v)", test.err, err) + } + + if diff := cmp.Diff(test.token, token); diff != "" { + t.Errorf("unexpected token:\n%s", diff) + } + }) + } +}