Codebase list golang-github-go-kit-kit / b411556
Merge pull request #335 from briankassouf/auth/jwt auth/jwt -- JWT service to service authentication Peter Bourgon authored 7 years ago GitHub committed 7 years ago
5 changed file(s) with 565 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 # package auth/jwt
1
2 `package auth/jwt` provides a set of interfaces for service authorization
3 through [JSON Web Tokens](https://jwt.io/).
4
5 ## Usage
6
7 NewParser takes a key function and an expected signing method and returns an
8 `endpoint.Middleware`. The middleware will parse a token passed into the
9 context via the `jwt.JWTTokenContextKey`. If the token is valid, any claims
10 will be added to the context via the `jwt.JWTClaimsContextKey`.
11
12 ```go
13 import (
14 stdjwt "github.com/dgrijalva/jwt-go"
15
16 "github.com/go-kit/kit/auth/jwt"
17 "github.com/go-kit/kit/endpoint"
18 )
19
20 func main() {
21 var exampleEndpoint endpoint.Endpoint
22 {
23 kf := func(token *stdjwt.Token) (interface{}, error) { return []byte("SigningString"), nil }
24 exampleEndpoint = MakeExampleEndpoint(service)
25 exampleEndpoint = jwt.NewParser(kf, stdjwt.SigningMethodHS256)(exampleEndpoint)
26 }
27 }
28 ```
29
30 NewSigner takes a JWT key ID header, the signing key, signing method, and a
31 claims object. It returns an `endpoint.Middleware`. The middleware will build
32 the token string and add it to the context via the `jwt.JWTTokenContextKey`.
33
34 ```go
35 import (
36 stdjwt "github.com/dgrijalva/jwt-go"
37
38 "github.com/go-kit/kit/auth/jwt"
39 "github.com/go-kit/kit/endpoint"
40 )
41
42 func main() {
43 var exampleEndpoint endpoint.Endpoint
44 {
45 exampleEndpoint = grpctransport.NewClient(...).Endpoint()
46 exampleEndpoint = jwt.NewSigner(
47 "kid-header",
48 []byte("SigningString"),
49 stdjwt.SigningMethodHS256,
50 jwt.Claims{},
51 )(exampleEndpoint)
52 }
53 }
54 ```
55
56 In order for the parser and the signer to work, the authorization headers need
57 to be passed between the request and the context. `ToHTTPContext()`,
58 `FromHTTPContext()`, `ToGRPCContext()`, and `FromGRPCContext()` are given as
59 helpers to do this. These functions implement the correlating transport's
60 RequestFunc interface and can be passed as ClientBefore or ServerBefore
61 options.
62
63 Example of use in a client:
64
65 ```go
66 import (
67 stdjwt "github.com/dgrijalva/jwt-go"
68
69 grpctransport "github.com/go-kit/kit/transport/grpc"
70 "github.com/go-kit/kit/auth/jwt"
71 "github.com/go-kit/kit/endpoint"
72 )
73
74 func main() {
75
76 options := []httptransport.ClientOption{}
77 var exampleEndpoint endpoint.Endpoint
78 {
79 exampleEndpoint = grpctransport.NewClient(..., grpctransport.ClientBefore(jwt.FromGRPCContext())).Endpoint()
80 exampleEndpoint = jwt.NewSigner(
81 "kid-header",
82 []byte("SigningString"),
83 stdjwt.SigningMethodHS256,
84 jwt.Claims{},
85 )(exampleEndpoint)
86 }
87 }
88 ```
89
90 Example of use in a server:
91
92 ```go
93 import (
94 "golang.org/x/net/context"
95
96 "github.com/go-kit/kit/auth/jwt"
97 "github.com/go-kit/kit/log"
98 grpctransport "github.com/go-kit/kit/transport/grpc"
99 )
100
101 func MakeGRPCServer(ctx context.Context, endpoints Endpoints, logger log.Logger) pb.ExampleServer {
102 options := []grpctransport.ServerOption{grpctransport.ServerErrorLogger(logger)}
103
104 return &grpcServer{
105 createUser: grpctransport.NewServer(
106 ctx,
107 endpoints.CreateUserEndpoint,
108 DecodeGRPCCreateUserRequest,
109 EncodeGRPCCreateUserResponse,
110 append(options, grpctransport.ServerBefore(jwt.ToGRPCContext()))...,
111 ),
112 getUser: grpctransport.NewServer(
113 ctx,
114 endpoints.GetUserEndpoint,
115 DecodeGRPCGetUserRequest,
116 EncodeGRPCGetUserResponse,
117 options...,
118 ),
119 }
120 }
121 ```
0 package jwt
1
2 import (
3 "errors"
4
5 jwt "github.com/dgrijalva/jwt-go"
6 "golang.org/x/net/context"
7
8 "github.com/go-kit/kit/endpoint"
9 )
10
11 type contextKey string
12
13 const (
14 // JWTTokenContextKey holds the key used to store a JWT Token in the
15 // context.
16 JWTTokenContextKey contextKey = "JWTToken"
17 // JWTClaimsContxtKey holds the key used to store the JWT Claims in the
18 // context.
19 JWTClaimsContextKey contextKey = "JWTClaims"
20 )
21
22 var (
23 // ErrTokenContextMissing denotes a token was not passed into the parsing
24 // middleware's context.
25 ErrTokenContextMissing = errors.New("token up for parsing was not passed through the context")
26 // ErrTokenInvalid denotes a token was not able to be validated.
27 ErrTokenInvalid = errors.New("JWT Token was invalid")
28 // ErrTokenExpired denotes a token's expire header (exp) has since passed.
29 ErrTokenExpired = errors.New("JWT Token is expired")
30 // ErrTokenMalformed denotes a token was not formatted as a JWT token.
31 ErrTokenMalformed = errors.New("JWT Token is malformed")
32 // ErrTokenNotActive denotes a token's not before header (nbf) is in the
33 // future.
34 ErrTokenNotActive = errors.New("token is not valid yet")
35 // ErrUncesptedSigningMethod denotes a token was signed with an unexpected
36 // signing method.
37 ErrUnexpectedSigningMethod = errors.New("unexpected signing method")
38 )
39
40 type Claims map[string]interface{}
41
42 // NewSigner creates a new JWT token generating middleware, specifying key ID,
43 // signing string, signing method and the claims you would like it to contain.
44 // Tokens are signed with a Key ID header (kid) which is useful for determining
45 // the key to use for parsing. Particularly useful for clients.
46 func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims Claims) endpoint.Middleware {
47 return func(next endpoint.Endpoint) endpoint.Endpoint {
48 return func(ctx context.Context, request interface{}) (response interface{}, err error) {
49 token := jwt.NewWithClaims(method, jwt.MapClaims(claims))
50 token.Header["kid"] = kid
51
52 // Sign and get the complete encoded token as a string using the secret
53 tokenString, err := token.SignedString(key)
54 if err != nil {
55 return nil, err
56 }
57 ctx = context.WithValue(ctx, JWTTokenContextKey, tokenString)
58
59 return next(ctx, request)
60 }
61 }
62 }
63
64 // NewParser creates a new JWT token parsing middleware, specifying a
65 // jwt.Keyfunc interface and the signing method. NewParser adds the resulting
66 // claims to endpoint context or returns error on invalid token. Particularly
67 // useful for servers.
68 func NewParser(keyFunc jwt.Keyfunc, method jwt.SigningMethod) endpoint.Middleware {
69 return func(next endpoint.Endpoint) endpoint.Endpoint {
70 return func(ctx context.Context, request interface{}) (response interface{}, err error) {
71 // tokenString is stored in the context from the transport handlers.
72 tokenString, ok := ctx.Value(JWTTokenContextKey).(string)
73 if !ok {
74 return nil, ErrTokenContextMissing
75 }
76
77 // Parse takes the token string and a function for looking up the
78 // key. The latter is especially useful if you use multiple keys
79 // for your application. The standard is to use 'kid' in the head
80 // of the token to identify which key to use, but the parsed token
81 // (head and claims) is provided to the callback, providing
82 // flexibility.
83 token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
84 // Don't forget to validate the alg is what you expect:
85 if token.Method != method {
86 return nil, ErrUnexpectedSigningMethod
87 }
88
89 return keyFunc(token)
90 })
91 if err != nil {
92 if e, ok := err.(*jwt.ValidationError); ok && e.Inner != nil {
93 if e.Errors&jwt.ValidationErrorMalformed != 0 {
94 // Token is malformed
95 return nil, ErrTokenMalformed
96 } else if e.Errors&jwt.ValidationErrorExpired != 0 {
97 // Token is expired
98 return nil, ErrTokenExpired
99 } else if e.Errors&jwt.ValidationErrorNotValidYet != 0 {
100 // Token is not active yet
101 return nil, ErrTokenNotActive
102 }
103
104 return nil, e.Inner
105 }
106
107 return nil, err
108 }
109
110 if !token.Valid {
111 return nil, ErrTokenInvalid
112 }
113
114 if claims, ok := token.Claims.(jwt.MapClaims); ok {
115 ctx = context.WithValue(ctx, JWTClaimsContextKey, Claims(claims))
116 }
117
118 return next(ctx, request)
119 }
120 }
121 }
0 package jwt
1
2 import (
3 "testing"
4
5 jwt "github.com/dgrijalva/jwt-go"
6
7 "golang.org/x/net/context"
8 )
9
10 var (
11 kid = "kid"
12 key = []byte("test_signing_key")
13 method = jwt.SigningMethodHS256
14 invalidMethod = jwt.SigningMethodRS256
15 claims = Claims{"user": "go-kit"}
16 // Signed tokens generated at https://jwt.io/
17 signedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ28ta2l0In0.14M2VmYyApdSlV_LZ88ajjwuaLeIFplB8JpyNy0A19E"
18 invalidKey = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.e30.vKVCKto-Wn6rgz3vBdaZaCBGfCBDTXOENSo_X2Gq7qA"
19 )
20
21 func TestSigner(t *testing.T) {
22 e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil }
23
24 signer := NewSigner(kid, key, method, claims)(e)
25 ctx, err := signer(context.Background(), struct{}{})
26 if err != nil {
27 t.Fatalf("Signer returned error: %s", err)
28 }
29
30 token, ok := ctx.(context.Context).Value(JWTTokenContextKey).(string)
31 if !ok {
32 t.Fatal("Token did not exist in context")
33 }
34
35 if token != signedKey {
36 t.Fatalf("JWT tokens did not match: expecting %s got %s", signedKey, token)
37 }
38 }
39
40 func TestJWTParser(t *testing.T) {
41 e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil }
42
43 keys := func(token *jwt.Token) (interface{}, error) {
44 return key, nil
45 }
46
47 parser := NewParser(keys, method)(e)
48
49 // No Token is passed into the parser
50 _, err := parser(context.Background(), struct{}{})
51 if err == nil {
52 t.Error("Parser should have returned an error")
53 }
54
55 if err != ErrTokenContextMissing {
56 t.Errorf("unexpected error returned, expected: %s got: %s", ErrTokenContextMissing, err)
57 }
58
59 // Invalid Token is passed into the parser
60 ctx := context.WithValue(context.Background(), JWTTokenContextKey, invalidKey)
61 _, err = parser(ctx, struct{}{})
62 if err == nil {
63 t.Error("Parser should have returned an error")
64 }
65
66 // Invalid Method is used in the parser
67 badParser := NewParser(keys, invalidMethod)(e)
68 ctx = context.WithValue(context.Background(), JWTTokenContextKey, signedKey)
69 _, err = badParser(ctx, struct{}{})
70 if err == nil {
71 t.Error("Parser should have returned an error")
72 }
73
74 if err != ErrUnexpectedSigningMethod {
75 t.Errorf("unexpected error returned, expected: %s got: %s", ErrUnexpectedSigningMethod, err)
76 }
77
78 // Invalid key is used in the parser
79 invalidKeys := func(token *jwt.Token) (interface{}, error) {
80 return []byte("bad"), nil
81 }
82
83 badParser = NewParser(invalidKeys, method)(e)
84 ctx = context.WithValue(context.Background(), JWTTokenContextKey, signedKey)
85 _, err = badParser(ctx, struct{}{})
86 if err == nil {
87 t.Error("Parser should have returned an error")
88 }
89
90 // Correct token is passed into the parser
91 ctx = context.WithValue(context.Background(), JWTTokenContextKey, signedKey)
92 ctx1, err := parser(ctx, struct{}{})
93 if err != nil {
94 t.Fatalf("Parser returned error: %s", err)
95 }
96
97 cl, ok := ctx1.(context.Context).Value(JWTClaimsContextKey).(Claims)
98 if !ok {
99 t.Fatal("Claims were not passed into context correctly")
100 }
101
102 if cl["user"] != claims["user"] {
103 t.Fatalf("JWT Claims.user did not match: expecting %s got %s", claims["user"], cl["user"])
104 }
105 }
0 package jwt
1
2 import (
3 "fmt"
4 stdhttp "net/http"
5 "strings"
6
7 "golang.org/x/net/context"
8 "google.golang.org/grpc/metadata"
9
10 "github.com/go-kit/kit/transport/grpc"
11 "github.com/go-kit/kit/transport/http"
12 )
13
14 const (
15 bearer string = "bearer"
16 bearerFormat string = "Bearer %s"
17 )
18
19 // ToHTTPContext moves JWT token from request header to contexti. Particularly
20 // useful for servers
21 func ToHTTPContext() http.RequestFunc {
22 return func(ctx context.Context, r *stdhttp.Request) context.Context {
23 token, ok := extractTokenFromAuthHeader(r.Header.Get("Authorization"))
24 if !ok {
25 return ctx
26 }
27
28 return context.WithValue(ctx, JWTTokenContextKey, token)
29 }
30 }
31
32 // FromHTTPContext moves JWT token from context to request header. Particularly
33 // useful for clients
34 func FromHTTPContext() http.RequestFunc {
35 return func(ctx context.Context, r *stdhttp.Request) context.Context {
36 token, ok := ctx.Value(JWTTokenContextKey).(string)
37 if ok {
38 r.Header.Add("Authorization", generateAuthHeaderFromToken(token))
39 }
40 return ctx
41 }
42 }
43
44 // ToGRPCContext moves JWT token from grpc metadata to context. Particularly
45 // userful for servers
46 func ToGRPCContext() grpc.RequestFunc {
47 return func(ctx context.Context, md *metadata.MD) context.Context {
48 // capital "Key" is illegal in HTTP/2.
49 authHeader, ok := (*md)["authorization"]
50 if !ok {
51 return ctx
52 }
53
54 token, ok := extractTokenFromAuthHeader(authHeader[0])
55 if ok {
56 ctx = context.WithValue(ctx, JWTTokenContextKey, token)
57 }
58
59 return ctx
60 }
61 }
62
63 // FromGRPCContext moves JWT token from context to grpc metadata. Particularly
64 // useful for clients
65 func FromGRPCContext() grpc.RequestFunc {
66 return func(ctx context.Context, md *metadata.MD) context.Context {
67 token, ok := ctx.Value(JWTTokenContextKey).(string)
68 if ok {
69 // capital "Key" is illegal in HTTP/2.
70 (*md)["authorization"] = []string{generateAuthHeaderFromToken(token)}
71 }
72
73 return ctx
74 }
75 }
76
77 func extractTokenFromAuthHeader(val string) (token string, ok bool) {
78 authHeaderParts := strings.Split(val, " ")
79 if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != bearer {
80 return "", false
81 }
82
83 return authHeaderParts[1], true
84 }
85
86 func generateAuthHeaderFromToken(token string) string {
87 return fmt.Sprintf(bearerFormat, token)
88 }
0 package jwt
1
2 import (
3 "fmt"
4 "net/http"
5 "testing"
6
7 "google.golang.org/grpc/metadata"
8
9 "golang.org/x/net/context"
10 )
11
12 func TestToHTTPContext(t *testing.T) {
13 reqFunc := ToHTTPContext()
14
15 // When the header doesn't exist
16 ctx := reqFunc(context.Background(), &http.Request{})
17
18 if ctx.Value(JWTTokenContextKey) != nil {
19 t.Error("Context shouldn't contain the encoded JWT")
20 }
21
22 // Authorization header value has invalid format
23 header := http.Header{}
24 header.Set("Authorization", "no expected auth header format value")
25 ctx = reqFunc(context.Background(), &http.Request{Header: header})
26
27 if ctx.Value(JWTTokenContextKey) != nil {
28 t.Error("Context shouldn't contain the encoded JWT")
29 }
30
31 // Authorization header is correct
32 header.Set("Authorization", generateAuthHeaderFromToken(signedKey))
33 ctx = reqFunc(context.Background(), &http.Request{Header: header})
34
35 token := ctx.Value(JWTTokenContextKey).(string)
36 if token != signedKey {
37 t.Errorf("Context doesn't contain the expected encoded token value; expected: %s, got: %s", signedKey, token)
38 }
39 }
40
41 func TestFromHTTPContext(t *testing.T) {
42 reqFunc := FromHTTPContext()
43
44 // No JWT Token is passed in the context
45 ctx := context.Background()
46 r := http.Request{}
47 reqFunc(ctx, &r)
48
49 token := r.Header.Get("Authorization")
50 if token != "" {
51 t.Error("authorization key should not exist in metadata")
52 }
53
54 // Correct JWT Token is passed in the context
55 ctx = context.WithValue(context.Background(), JWTTokenContextKey, signedKey)
56 r = http.Request{Header: http.Header{}}
57 reqFunc(ctx, &r)
58
59 token = r.Header.Get("Authorization")
60 expected := generateAuthHeaderFromToken(signedKey)
61
62 if token != expected {
63 t.Errorf("Authorization header does not contain the expected JWT token; expected %s, got %s", expected, token)
64 }
65 }
66
67 func TestToGRPCContext(t *testing.T) {
68 md := metadata.MD{}
69 reqFunc := ToGRPCContext()
70
71 // No Authorization header is passed
72 ctx := reqFunc(context.Background(), &md)
73 token := ctx.Value(JWTTokenContextKey)
74 if token != nil {
75 t.Error("Context should not contain a JWT Token")
76 }
77
78 // Invalid Authorization header is passed
79 md["authorization"] = []string{fmt.Sprintf("%s", signedKey)}
80 ctx = reqFunc(context.Background(), &md)
81 token = ctx.Value(JWTTokenContextKey)
82 if token != nil {
83 t.Error("Context should not contain a JWT Token")
84 }
85
86 // Authorization header is correct
87 md["authorization"] = []string{fmt.Sprintf("Bearer %s", signedKey)}
88 ctx = reqFunc(context.Background(), &md)
89 token, ok := ctx.Value(JWTTokenContextKey).(string)
90 if !ok {
91 t.Fatal("JWT Token not passed to context correctly")
92 }
93
94 if token != signedKey {
95 t.Errorf("JWT tokens did not match: expecting %s got %s", signedKey, token)
96 }
97 }
98
99 func TestFromGRPCContext(t *testing.T) {
100 reqFunc := FromGRPCContext()
101
102 // No JWT Token is passed in the context
103 ctx := context.Background()
104 md := metadata.MD{}
105 reqFunc(ctx, &md)
106
107 _, ok := md["authorization"]
108 if ok {
109 t.Error("authorization key should not exist in metadata")
110 }
111
112 // Correct JWT Token is passed in the context
113 ctx = context.WithValue(context.Background(), JWTTokenContextKey, signedKey)
114 md = metadata.MD{}
115 reqFunc(ctx, &md)
116
117 token, ok := md["authorization"]
118 if !ok {
119 t.Fatal("JWT Token not passed to metadata correctly")
120 }
121
122 if token[0] != generateAuthHeaderFromToken(signedKey) {
123 t.Errorf("JWT tokens did not match: expecting %s got %s", signedKey, token[0])
124 }
125 }