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
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 | } |