Skip to content

Commit

Permalink
feat(auth): add new jsonweb package
Browse files Browse the repository at this point in the history
  • Loading branch information
GeorgeMac committed Sep 19, 2019
1 parent ff7d13a commit e5c8b69
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
115 changes: 115 additions & 0 deletions jsonweb/token.go
Original file line number Diff line number Diff line change
@@ -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
}
85 changes: 85 additions & 0 deletions jsonweb/token_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit e5c8b69

Please sign in to comment.