From d51e546463208e79246aa8d64701b7f07d0680fc Mon Sep 17 00:00:00 2001 From: Cameron Stitt Date: Wed, 8 Mar 2017 16:40:16 +1000 Subject: [PATCH 1/4] Add *WithClaims methods to jwt middleware for more advanced usage. --- auth/jwt/middleware.go | 40 ++++++++++++++++++--------- auth/jwt/middleware_test.go | 54 ++++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/auth/jwt/middleware.go b/auth/jwt/middleware.go index b5ccf0b4c..84863bf47 100644 --- a/auth/jwt/middleware.go +++ b/auth/jwt/middleware.go @@ -47,14 +47,14 @@ var ( // Claims is a map of arbitrary claim data. type Claims map[string]interface{} -// NewSigner creates a new JWT token generating middleware, specifying key ID, -// signing string, signing method and the claims you would like it to contain. +// NewSignerWithClaims creates a new JWT token generating middleware, specifying key ID, +// signing string, signing method and the jwt.Claims you would like it to contain. // Tokens are signed with a Key ID header (kid) which is useful for determining // the key to use for parsing. Particularly useful for clients. -func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims Claims) endpoint.Middleware { +func NewSignerWithClaims(kid string, key []byte, method jwt.SigningMethod, claims jwt.Claims) endpoint.Middleware { return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { - token := jwt.NewWithClaims(method, jwt.MapClaims(claims)) + token := jwt.NewWithClaims(method, claims) token.Header["kid"] = kid // Sign and get the complete encoded token as a string using the secret @@ -69,11 +69,18 @@ func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims Claims) } } -// NewParser creates a new JWT token parsing middleware, specifying a -// jwt.Keyfunc interface and the signing method. NewParser adds the resulting -// claims to endpoint context or returns error on invalid token. Particularly -// useful for servers. -func NewParser(keyFunc jwt.Keyfunc, method jwt.SigningMethod) endpoint.Middleware { +// NewSigner creates a new JWT token generating middleware, specifying key ID, +// signing string, signing method and the claims you would like it to contain. +// It passes these values onto NewSignerWithClaims to handle the signing process. +func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims Claims) endpoint.Middleware { + return NewSignerWithClaims(kid, key, method, jwt.MapClaims(claims)) +} + +// NewParserWithClaims creates a new JWT token parsing middleware, specifying a +// jwt.Keyfunc interface, the signing method as well as the claims to parse into. +// NewParserWithClaims adds the resulting claims to endpoint context or returns error on invalid token. +// Particularly useful for servers. +func NewParserWithClaims(keyFunc jwt.Keyfunc, method jwt.SigningMethod, claims jwt.Claims) endpoint.Middleware { return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { // tokenString is stored in the context from the transport handlers. @@ -88,7 +95,7 @@ func NewParser(keyFunc jwt.Keyfunc, method jwt.SigningMethod) endpoint.Middlewar // of the token to identify which key to use, but the parsed token // (head and claims) is provided to the callback, providing // flexibility. - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { // Don't forget to validate the alg is what you expect: if token.Method != method { return nil, ErrUnexpectedSigningMethod @@ -119,11 +126,20 @@ func NewParser(keyFunc jwt.Keyfunc, method jwt.SigningMethod) endpoint.Middlewar return nil, ErrTokenInvalid } - if claims, ok := token.Claims.(jwt.MapClaims); ok { - ctx = context.WithValue(ctx, JWTClaimsContextKey, Claims(claims)) + if tokenClaims, ok := token.Claims.(jwt.MapClaims); ok { + ctx = context.WithValue(ctx, JWTClaimsContextKey, Claims(tokenClaims)) + } else { + ctx = context.WithValue(ctx, JWTClaimsContextKey, token.Claims) } return next(ctx, request) } } } + +// NewParser creates a new JWT token parsing middleware, specifying a +// jwt.KeyFunc interface and the signing method. It will utilize NewParserWithClaims +// and fall back to implementing the jwt.MapClaims type. +func NewParser(keyFunc jwt.Keyfunc, method jwt.SigningMethod) endpoint.Middleware { + return NewParserWithClaims(keyFunc, method, jwt.MapClaims{}) +} diff --git a/auth/jwt/middleware_test.go b/auth/jwt/middleware_test.go index 99b943c59..0ba299951 100644 --- a/auth/jwt/middleware_test.go +++ b/auth/jwt/middleware_test.go @@ -5,23 +5,24 @@ import ( "testing" jwt "github.com/dgrijalva/jwt-go" + "github.com/go-kit/kit/endpoint" ) var ( - kid = "kid" - key = []byte("test_signing_key") - method = jwt.SigningMethodHS256 - invalidMethod = jwt.SigningMethodRS256 - claims = Claims{"user": "go-kit"} + kid = "kid" + key = []byte("test_signing_key") + method = jwt.SigningMethodHS256 + invalidMethod = jwt.SigningMethodRS256 + claims = Claims{"user": "go-kit"} + mapClaims = jwt.MapClaims{"user": "go-kit"} + standardClaims = jwt.StandardClaims{Audience: "go-kit"} // Signed tokens generated at https://jwt.io/ - signedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ28ta2l0In0.14M2VmYyApdSlV_LZ88ajjwuaLeIFplB8JpyNy0A19E" - invalidKey = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.e30.vKVCKto-Wn6rgz3vBdaZaCBGfCBDTXOENSo_X2Gq7qA" + signedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ28ta2l0In0.14M2VmYyApdSlV_LZ88ajjwuaLeIFplB8JpyNy0A19E" + standardSignedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJnby1raXQifQ.L5ypIJjCOOv3jJ8G5SelaHvR04UJuxmcBN5QW3m_aoY" + invalidKey = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.e30.vKVCKto-Wn6rgz3vBdaZaCBGfCBDTXOENSo_X2Gq7qA" ) -func TestSigner(t *testing.T) { - e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil } - - signer := NewSigner(kid, key, method, claims)(e) +func signingValidator(t *testing.T, signer endpoint.Endpoint, expectedKey string) { ctx, err := signer(context.Background(), struct{}{}) if err != nil { t.Fatalf("Signer returned error: %s", err) @@ -32,11 +33,24 @@ func TestSigner(t *testing.T) { t.Fatal("Token did not exist in context") } - if token != signedKey { - t.Fatalf("JWT tokens did not match: expecting %s got %s", signedKey, token) + if token != expectedKey { + t.Fatalf("JWT tokens did not match: expecting %s got %s", expectedKey, token) } } +func TestNewSigner(t *testing.T) { + e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil } + + signer := NewSigner(kid, key, method, claims)(e) + signingValidator(t, signer, signedKey) + + signer = NewSignerWithClaims(kid, key, method, mapClaims)(e) + signingValidator(t, signer, signedKey) + + signer = NewSignerWithClaims(kid, key, method, standardClaims)(e) + signingValidator(t, signer, standardSignedKey) +} + func TestJWTParser(t *testing.T) { e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil } @@ -102,4 +116,18 @@ func TestJWTParser(t *testing.T) { if cl["user"] != claims["user"] { t.Fatalf("JWT Claims.user did not match: expecting %s got %s", claims["user"], cl["user"]) } + + parser = NewParserWithClaims(keys, method, &jwt.StandardClaims{})(e) + ctx = context.WithValue(context.Background(), JWTTokenContextKey, standardSignedKey) + ctx1, err = parser(ctx, struct{}{}) + if err != nil { + t.Fatalf("Parser returned error: %s", err) + } + stdCl, ok := ctx1.(context.Context).Value(JWTClaimsContextKey).(*jwt.StandardClaims) + if !ok { + t.Fatal("Claims were not passed into context correctly") + } + if !stdCl.VerifyAudience("go-kit", true) { + t.Fatal("JWT jwt.StandardClaims.Audience did not match: expecting %s got %s", standardClaims.Audience, stdCl.Audience) + } } From 8147bc1a8162bc63d1ab4d719c15dc24b13d2e6f Mon Sep 17 00:00:00 2001 From: Cameron Stitt Date: Thu, 9 Mar 2017 07:58:53 +1000 Subject: [PATCH 2/4] Remove ambiguous Claims type. --- auth/jwt/middleware.go | 31 +++++-------------------------- auth/jwt/middleware_test.go | 22 +++++++++------------- 2 files changed, 14 insertions(+), 39 deletions(-) diff --git a/auth/jwt/middleware.go b/auth/jwt/middleware.go index 84863bf47..090078c08 100644 --- a/auth/jwt/middleware.go +++ b/auth/jwt/middleware.go @@ -44,14 +44,11 @@ var ( ErrUnexpectedSigningMethod = errors.New("unexpected signing method") ) -// Claims is a map of arbitrary claim data. -type Claims map[string]interface{} - -// NewSignerWithClaims creates a new JWT token generating middleware, specifying key ID, +// NewSigner creates a new JWT token generating middleware, specifying key ID, // signing string, signing method and the jwt.Claims you would like it to contain. // Tokens are signed with a Key ID header (kid) which is useful for determining // the key to use for parsing. Particularly useful for clients. -func NewSignerWithClaims(kid string, key []byte, method jwt.SigningMethod, claims jwt.Claims) endpoint.Middleware { +func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims jwt.Claims) endpoint.Middleware { return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { token := jwt.NewWithClaims(method, claims) @@ -69,18 +66,11 @@ func NewSignerWithClaims(kid string, key []byte, method jwt.SigningMethod, claim } } -// NewSigner creates a new JWT token generating middleware, specifying key ID, -// signing string, signing method and the claims you would like it to contain. -// It passes these values onto NewSignerWithClaims to handle the signing process. -func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims Claims) endpoint.Middleware { - return NewSignerWithClaims(kid, key, method, jwt.MapClaims(claims)) -} - -// NewParserWithClaims creates a new JWT token parsing middleware, specifying a +// NewParser creates a new JWT token parsing middleware, specifying a // jwt.Keyfunc interface, the signing method as well as the claims to parse into. // NewParserWithClaims adds the resulting claims to endpoint context or returns error on invalid token. // Particularly useful for servers. -func NewParserWithClaims(keyFunc jwt.Keyfunc, method jwt.SigningMethod, claims jwt.Claims) endpoint.Middleware { +func NewParser(keyFunc jwt.Keyfunc, method jwt.SigningMethod, claims jwt.Claims) endpoint.Middleware { return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { // tokenString is stored in the context from the transport handlers. @@ -126,20 +116,9 @@ func NewParserWithClaims(keyFunc jwt.Keyfunc, method jwt.SigningMethod, claims j return nil, ErrTokenInvalid } - if tokenClaims, ok := token.Claims.(jwt.MapClaims); ok { - ctx = context.WithValue(ctx, JWTClaimsContextKey, Claims(tokenClaims)) - } else { - ctx = context.WithValue(ctx, JWTClaimsContextKey, token.Claims) - } + ctx = context.WithValue(ctx, JWTClaimsContextKey, token.Claims) return next(ctx, request) } } } - -// NewParser creates a new JWT token parsing middleware, specifying a -// jwt.KeyFunc interface and the signing method. It will utilize NewParserWithClaims -// and fall back to implementing the jwt.MapClaims type. -func NewParser(keyFunc jwt.Keyfunc, method jwt.SigningMethod) endpoint.Middleware { - return NewParserWithClaims(keyFunc, method, jwt.MapClaims{}) -} diff --git a/auth/jwt/middleware_test.go b/auth/jwt/middleware_test.go index 0ba299951..cc3406623 100644 --- a/auth/jwt/middleware_test.go +++ b/auth/jwt/middleware_test.go @@ -13,7 +13,6 @@ var ( key = []byte("test_signing_key") method = jwt.SigningMethodHS256 invalidMethod = jwt.SigningMethodRS256 - claims = Claims{"user": "go-kit"} mapClaims = jwt.MapClaims{"user": "go-kit"} standardClaims = jwt.StandardClaims{Audience: "go-kit"} // Signed tokens generated at https://jwt.io/ @@ -41,13 +40,10 @@ func signingValidator(t *testing.T, signer endpoint.Endpoint, expectedKey string func TestNewSigner(t *testing.T) { e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil } - signer := NewSigner(kid, key, method, claims)(e) + signer := NewSigner(kid, key, method, mapClaims)(e) signingValidator(t, signer, signedKey) - signer = NewSignerWithClaims(kid, key, method, mapClaims)(e) - signingValidator(t, signer, signedKey) - - signer = NewSignerWithClaims(kid, key, method, standardClaims)(e) + signer = NewSigner(kid, key, method, standardClaims)(e) signingValidator(t, signer, standardSignedKey) } @@ -58,7 +54,7 @@ func TestJWTParser(t *testing.T) { return key, nil } - parser := NewParser(keys, method)(e) + parser := NewParser(keys, method, jwt.MapClaims{})(e) // No Token is passed into the parser _, err := parser(context.Background(), struct{}{}) @@ -78,7 +74,7 @@ func TestJWTParser(t *testing.T) { } // Invalid Method is used in the parser - badParser := NewParser(keys, invalidMethod)(e) + badParser := NewParser(keys, invalidMethod, jwt.MapClaims{})(e) ctx = context.WithValue(context.Background(), JWTTokenContextKey, signedKey) _, err = badParser(ctx, struct{}{}) if err == nil { @@ -94,7 +90,7 @@ func TestJWTParser(t *testing.T) { return []byte("bad"), nil } - badParser = NewParser(invalidKeys, method)(e) + badParser = NewParser(invalidKeys, method, jwt.MapClaims{})(e) ctx = context.WithValue(context.Background(), JWTTokenContextKey, signedKey) _, err = badParser(ctx, struct{}{}) if err == nil { @@ -108,16 +104,16 @@ func TestJWTParser(t *testing.T) { t.Fatalf("Parser returned error: %s", err) } - cl, ok := ctx1.(context.Context).Value(JWTClaimsContextKey).(Claims) + cl, ok := ctx1.(context.Context).Value(JWTClaimsContextKey).(jwt.MapClaims) if !ok { t.Fatal("Claims were not passed into context correctly") } - if cl["user"] != claims["user"] { - t.Fatalf("JWT Claims.user did not match: expecting %s got %s", claims["user"], cl["user"]) + if cl["user"] != mapClaims["user"] { + t.Fatalf("JWT Claims.user did not match: expecting %s got %s", mapClaims["user"], cl["user"]) } - parser = NewParserWithClaims(keys, method, &jwt.StandardClaims{})(e) + parser = NewParser(keys, method, &jwt.StandardClaims{})(e) ctx = context.WithValue(context.Background(), JWTTokenContextKey, standardSignedKey) ctx1, err = parser(ctx, struct{}{}) if err != nil { From c7b8e100a429cb7a4bb102f71e4550c4b9d0158e Mon Sep 17 00:00:00 2001 From: Cameron Stitt Date: Thu, 9 Mar 2017 08:01:24 +1000 Subject: [PATCH 3/4] Fix comments for middlewares. --- auth/jwt/middleware.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auth/jwt/middleware.go b/auth/jwt/middleware.go index 090078c08..cce469506 100644 --- a/auth/jwt/middleware.go +++ b/auth/jwt/middleware.go @@ -45,7 +45,7 @@ var ( ) // NewSigner creates a new JWT token generating middleware, specifying key ID, -// signing string, signing method and the jwt.Claims you would like it to contain. +// signing string, signing method and the claims you would like it to contain. // Tokens are signed with a Key ID header (kid) which is useful for determining // the key to use for parsing. Particularly useful for clients. func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims jwt.Claims) endpoint.Middleware { @@ -67,8 +67,8 @@ func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims jwt.Clai } // NewParser creates a new JWT token parsing middleware, specifying a -// jwt.Keyfunc interface, the signing method as well as the claims to parse into. -// NewParserWithClaims adds the resulting claims to endpoint context or returns error on invalid token. +// jwt.Keyfunc interface, the signing method and the claims type to be used. NewParser +// adds the resulting claims to endpoint context or returns error on invalid token. // Particularly useful for servers. func NewParser(keyFunc jwt.Keyfunc, method jwt.SigningMethod, claims jwt.Claims) endpoint.Middleware { return func(next endpoint.Endpoint) endpoint.Endpoint { From b2b278c6464997f28538a66a6d754a53ef901022 Mon Sep 17 00:00:00 2001 From: Cameron Stitt Date: Thu, 16 Mar 2017 20:17:13 +1000 Subject: [PATCH 4/4] Fixes from PR requests. --- auth/jwt/middleware_test.go | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/auth/jwt/middleware_test.go b/auth/jwt/middleware_test.go index cc3406623..76889d6f4 100644 --- a/auth/jwt/middleware_test.go +++ b/auth/jwt/middleware_test.go @@ -4,20 +4,34 @@ import ( "context" "testing" + "crypto/subtle" + jwt "github.com/dgrijalva/jwt-go" "github.com/go-kit/kit/endpoint" ) +type customClaims struct { + MyProperty string `json:"my_property"` + jwt.StandardClaims +} + +func (c customClaims) VerifyMyProperty(p string) bool { + return subtle.ConstantTimeCompare([]byte(c.MyProperty), []byte(p)) != 0 +} + var ( kid = "kid" key = []byte("test_signing_key") + myProperty = "some value" method = jwt.SigningMethodHS256 invalidMethod = jwt.SigningMethodRS256 mapClaims = jwt.MapClaims{"user": "go-kit"} standardClaims = jwt.StandardClaims{Audience: "go-kit"} + myCustomClaims = customClaims{MyProperty: myProperty, StandardClaims: standardClaims} // Signed tokens generated at https://jwt.io/ signedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ28ta2l0In0.14M2VmYyApdSlV_LZ88ajjwuaLeIFplB8JpyNy0A19E" standardSignedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJnby1raXQifQ.L5ypIJjCOOv3jJ8G5SelaHvR04UJuxmcBN5QW3m_aoY" + customSignedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJteV9wcm9wZXJ0eSI6InNvbWUgdmFsdWUiLCJhdWQiOiJnby1raXQifQ.s8F-IDrV4WPJUsqr7qfDi-3GRlcKR0SRnkTeUT_U-i0" invalidKey = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.e30.vKVCKto-Wn6rgz3vBdaZaCBGfCBDTXOENSo_X2Gq7qA" ) @@ -45,6 +59,9 @@ func TestNewSigner(t *testing.T) { signer = NewSigner(kid, key, method, standardClaims)(e) signingValidator(t, signer, standardSignedKey) + + signer = NewSigner(kid, key, method, myCustomClaims)(e) + signingValidator(t, signer, customSignedKey) } func TestJWTParser(t *testing.T) { @@ -124,6 +141,23 @@ func TestJWTParser(t *testing.T) { t.Fatal("Claims were not passed into context correctly") } if !stdCl.VerifyAudience("go-kit", true) { - t.Fatal("JWT jwt.StandardClaims.Audience did not match: expecting %s got %s", standardClaims.Audience, stdCl.Audience) + t.Fatalf("JWT jwt.StandardClaims.Audience did not match: expecting %s got %s", standardClaims.Audience, stdCl.Audience) + } + + parser = NewParser(keys, method, &customClaims{})(e) + ctx = context.WithValue(context.Background(), JWTTokenContextKey, customSignedKey) + ctx1, err = parser(ctx, struct{}{}) + if err != nil { + t.Fatalf("Parser returned error: %s", err) + } + custCl, ok := ctx1.(context.Context).Value(JWTClaimsContextKey).(*customClaims) + if !ok { + t.Fatal("Claims were not passed into context correctly") + } + if !custCl.VerifyAudience("go-kit", true) { + t.Fatalf("JWT customClaims.Audience did not match: expecting %s got %s", standardClaims.Audience, custCl.Audience) + } + if !custCl.VerifyMyProperty(myProperty) { + t.Fatalf("JWT customClaims.MyProperty did not match: expecting %s got %s", myProperty, custCl.MyProperty) } }