Improvements based off PR comments
Brian Kassouf
6 years ago
0 | 0 | # package auth/jwt |
1 | 1 | |
2 | `package auth/jwt` provides a set of interfaces for service authorization through [JSON Web Tokens](https://jwt.io/). | |
2 | `package auth/jwt` provides a set of interfaces for service authorization | |
3 | through [JSON Web Tokens](https://jwt.io/). | |
3 | 4 | |
4 | 5 | ## Usage |
5 | 6 | |
6 | NewParser takes a key function and an expected signing method and returns an `endpoint.Middleware`. | |
7 | The middleware will parse a token passed into the context via the `jwt.JWTTokenContextKey`. | |
8 | If the token is valid, any claims will be added to the context via the `jwt.JWTClaimsContextKey`. | |
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`. | |
9 | 11 | |
10 | 12 | ```go |
11 | 13 | import ( |
12 | 14 | stdjwt "github.com/dgrijalva/jwt-go" |
13 | ||
14 | "github.com/go-kit/kit/auth/jwt" | |
15 | "github.com/go-kit/kit/endpoint" | |
15 | ||
16 | "github.com/go-kit/kit/auth/jwt" | |
17 | "github.com/go-kit/kit/endpoint" | |
16 | 18 | ) |
17 | 19 | |
18 | 20 | func main() { |
19 | 21 | var exampleEndpoint endpoint.Endpoint |
20 | 22 | { |
21 | keyFunc := func(token *stdjwt.Token) (interface{}, error) { return []byte("SigningString"), nil } | |
22 | jwtParser := jwt.NewParser(keyFunc, stdjwt.SigningMethodHS256) | |
23 | ||
23 | kf := func(token *stdjwt.Token) (interface{}, error) { return []byte("SigningString"), nil } | |
24 | 24 | exampleEndpoint = MakeExampleEndpoint(service) |
25 | exampleEndpoint = jwtParser(exampleEndpoint) | |
25 | exampleEndpoint = jwt.NewParser(kf, stdjwt.SigningMethodHS256)(exampleEndpoint) | |
26 | 26 | } |
27 | 27 | } |
28 | 28 | ``` |
29 | 29 | |
30 | NewSigner takes a JWT key id header, the signing key, signing method, and a claims object. It returns an `endpoint.Middleware`. | |
31 | The middleware will build the token string and add it to the context via the `jwt.JWTTokenContextKey`. | |
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`. | |
32 | 33 | |
33 | 34 | ```go |
34 | 35 | import ( |
35 | 36 | stdjwt "github.com/dgrijalva/jwt-go" |
36 | ||
37 | "github.com/go-kit/kit/auth/jwt" | |
38 | "github.com/go-kit/kit/endpoint" | |
37 | ||
38 | "github.com/go-kit/kit/auth/jwt" | |
39 | "github.com/go-kit/kit/endpoint" | |
39 | 40 | ) |
40 | 41 | |
41 | 42 | func main() { |
42 | 43 | var exampleEndpoint endpoint.Endpoint |
43 | 44 | { |
44 | jwtSigner := jwt.NewSigner("kid-header", []byte("SigningString"), stdjwt.SigningMethodHS256, jwt.Claims{}) | |
45 | ||
46 | exampleEndpoint = grpctransport.NewClient( | |
47 | . // build client endpoint here | |
48 | . | |
49 | . | |
50 | ).Endpoint() | |
51 | ||
52 | exampleEndpoint = jwtSigner(exampleEndpoint) | |
45 | exampleEndpoint = grpctransport.NewClient(...).Endpoint() | |
46 | exampleEndpoint = jwt.NewSigner( | |
47 | "kid-header", | |
48 | []byte("SigningString"), | |
49 | stdjwt.SigningMethodHS256, | |
50 | jwt.Claims{}, | |
51 | )(exampleEndpoint) | |
53 | 52 | } |
54 | 53 | } |
55 | 54 | ``` |
56 | 55 | |
57 | In order for the parser and the signer to work, the authorization headers need to be passed between the request and the context. | |
58 | `ToHTTPContext()`, `FromHTTPContext()`, `ToGRPCContext()`, and `FromGRPCContext()` are given as helpers to do this. | |
59 | These functions implement the correlating transport's RequestFunc interface and can be passed as ClientBefore or ServerBefore options. | |
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. | |
60 | 62 | |
61 | 63 | Example of use in a client: |
62 | 64 | |
63 | 65 | ```go |
64 | 66 | import ( |
65 | stdjwt "github.com/dgrijalva/jwt-go" | |
66 | grpctransport "github.com/go-kit/kit/transport/grpc" | |
67 | ||
68 | "github.com/go-kit/kit/auth/jwt" | |
69 | "github.com/go-kit/kit/endpoint" | |
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" | |
70 | 72 | ) |
71 | 73 | |
72 | 74 | func main() { |
73 | 75 | |
74 | options := []httptransport.ClientOption{} | |
76 | options := []httptransport.ClientOption{} | |
75 | 77 | var exampleEndpoint endpoint.Endpoint |
76 | 78 | { |
77 | jwtSigner := jwt.NewSigner("kid-header", []byte("SigningString"), stdjwt.SigningMethodHS256, jwt.Claims{}) | |
78 | ||
79 | options = append(options, grpctransport.ClientBefore(jwt.FromGRPCContext())) | |
80 | exampleEndpoint = grpctransport.NewClient( | |
81 | . // build client endpoint here | |
82 | . | |
83 | options.... | |
84 | ).Endpoint() | |
85 | ||
86 | exampleEndpoint = jwtSigner(exampleEndpoint) | |
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) | |
87 | 86 | } |
88 | 87 | } |
89 | 88 | ``` |
2 | 2 | import ( |
3 | 3 | "errors" |
4 | 4 | |
5 | jwt "github.com/dgrijalva/jwt-go" | |
5 | 6 | "golang.org/x/net/context" |
6 | 7 | |
7 | jwt "github.com/dgrijalva/jwt-go" | |
8 | 8 | "github.com/go-kit/kit/endpoint" |
9 | 9 | ) |
10 | 10 | |
11 | 11 | type contextKey string |
12 | 12 | |
13 | 13 | const ( |
14 | // JWTTokenContextKey holds the key used to store a JWT Token in the context | |
14 | // JWTTokenContextKey holds the key used to store a JWT Token in the | |
15 | // context. | |
15 | 16 | JWTTokenContextKey contextKey = "JWTToken" |
16 | // JWTClaimsContxtKey holds the key used to store the JWT Claims in the context | |
17 | // JWTClaimsContxtKey holds the key used to store the JWT Claims in the | |
18 | // context. | |
17 | 19 | JWTClaimsContextKey contextKey = "JWTClaims" |
18 | 20 | ) |
19 | 21 | |
20 | 22 | var ( |
21 | ErrTokenContextMissing = errors.New("Token up for parsing was not passed through the context") | |
22 | ErrTokenInvalid = errors.New("JWT Token was invalid") | |
23 | ErrTokenExpired = errors.New("JWT Token is expired") | |
24 | ErrTokenMalformed = errors.New("JWT Token is malformed") | |
25 | ErrTokenNotActive = errors.New("Token is not valid yet") | |
26 | ErrUnexpectedSigningMethod = errors.New("Unexpected signing method") | |
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") | |
27 | 38 | ) |
28 | 39 | |
29 | 40 | type Claims map[string]interface{} |
30 | 41 | |
31 | // Create a new JWT token generating middleware, specifying signing method and the claims | |
32 | // you would like it to contain. Particularly useful for clients. | |
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. | |
33 | 46 | func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims Claims) endpoint.Middleware { |
34 | 47 | return func(next endpoint.Endpoint) endpoint.Endpoint { |
35 | 48 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { |
48 | 61 | } |
49 | 62 | } |
50 | 63 | |
51 | // Create a new JWT token parsing middleware, specifying a jwt.Keyfunc interface and the | |
52 | // signing method. Adds the resulting claims to endpoint context or returns error on invalid | |
53 | // token. Particularly useful for servers. | |
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. | |
54 | 68 | func NewParser(keyFunc jwt.Keyfunc, method jwt.SigningMethod) endpoint.Middleware { |
55 | 69 | return func(next endpoint.Endpoint) endpoint.Endpoint { |
56 | 70 | return func(ctx context.Context, request interface{}) (response interface{}, err error) { |
57 | // tokenString is stored in the context from the transport handlers | |
71 | // tokenString is stored in the context from the transport handlers. | |
58 | 72 | tokenString, ok := ctx.Value(JWTTokenContextKey).(string) |
59 | 73 | if !ok { |
60 | 74 | return nil, ErrTokenContextMissing |
61 | 75 | } |
62 | 76 | |
63 | // Parse takes the token string and a function for looking up the key. The latter is especially | |
64 | // useful if you use multiple keys for your application. The standard is to use 'kid' in the | |
65 | // head of the token to identify which key to use, but the parsed token (head and claims) is provided | |
66 | // to the callback, providing flexibility. | |
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. | |
67 | 83 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { |
68 | 84 | // Don't forget to validate the alg is what you expect: |
69 | 85 | if token.Method != method { |
13 | 13 | method = jwt.SigningMethodHS256 |
14 | 14 | invalidMethod = jwt.SigningMethodRS256 |
15 | 15 | claims = Claims{"user": "go-kit"} |
16 | signedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ28ta2l0In0.14M2VmYyApdSlV_LZ88ajjwuaLeIFplB8JpyNy0A19E" | |
17 | invalidKey = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.e30.vKVCKto-Wn6rgz3vBdaZaCBGfCBDTXOENSo_X2Gq7qA" | |
16 | // Signed tokens generated at https://jwt.io/ | |
17 | signedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ28ta2l0In0.14M2VmYyApdSlV_LZ88ajjwuaLeIFplB8JpyNy0A19E" | |
18 | invalidKey = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.e30.vKVCKto-Wn6rgz3vBdaZaCBGfCBDTXOENSo_X2Gq7qA" | |
18 | 19 | ) |
19 | 20 | |
20 | 21 | func TestSigner(t *testing.T) { |
4 | 4 | stdhttp "net/http" |
5 | 5 | "strings" |
6 | 6 | |
7 | "golang.org/x/net/context" | |
8 | "google.golang.org/grpc/metadata" | |
9 | ||
7 | 10 | "github.com/go-kit/kit/transport/grpc" |
8 | 11 | "github.com/go-kit/kit/transport/http" |
9 | "golang.org/x/net/context" | |
10 | "google.golang.org/grpc/metadata" | |
11 | 12 | ) |
12 | 13 | |
13 | 14 | const ( |
14 | BEARER string = "bearer" | |
15 | BEARER_FORMAT string = "Bearer %s" | |
15 | bearer string = "bearer" | |
16 | bearerFormat string = "Bearer %s" | |
16 | 17 | ) |
17 | 18 | |
18 | // moves JWT token from request header to context | |
19 | // particularly useful for servers | |
19 | // ToHTTPContext moves JWT token from request header to contexti. Particularly | |
20 | // useful for servers | |
20 | 21 | func ToHTTPContext() http.RequestFunc { |
21 | 22 | return func(ctx context.Context, r *stdhttp.Request) context.Context { |
22 | 23 | token, ok := extractTokenFromAuthHeader(r.Header.Get("Authorization")) |
28 | 29 | } |
29 | 30 | } |
30 | 31 | |
31 | // moves JWT token from context to request header | |
32 | // particularly useful for clients | |
32 | // FromHTTPContext moves JWT token from context to request header. Particularly | |
33 | // useful for clients | |
33 | 34 | func FromHTTPContext() http.RequestFunc { |
34 | 35 | return func(ctx context.Context, r *stdhttp.Request) context.Context { |
35 | 36 | token, ok := ctx.Value(JWTTokenContextKey).(string) |
40 | 41 | } |
41 | 42 | } |
42 | 43 | |
43 | // moves JWT token from grpc metadata to context | |
44 | // particularly userful for servers | |
44 | // ToGRPCContext moves JWT token from grpc metadata to context. Particularly | |
45 | // userful for servers | |
45 | 46 | func ToGRPCContext() grpc.RequestFunc { |
46 | 47 | return func(ctx context.Context, md *metadata.MD) context.Context { |
47 | 48 | // capital "Key" is illegal in HTTP/2. |
59 | 60 | } |
60 | 61 | } |
61 | 62 | |
62 | // moves JWT token from context to grpc metadata | |
63 | // particularly useful for clients | |
63 | // FromGRPCContext moves JWT token from context to grpc metadata. Particularly | |
64 | // useful for clients | |
64 | 65 | func FromGRPCContext() grpc.RequestFunc { |
65 | 66 | return func(ctx context.Context, md *metadata.MD) context.Context { |
66 | 67 | token, ok := ctx.Value(JWTTokenContextKey).(string) |
73 | 74 | } |
74 | 75 | } |
75 | 76 | |
76 | // extractTokenFromAuthHeader returns the token from the value of the Authorzation header | |
77 | 77 | func extractTokenFromAuthHeader(val string) (token string, ok bool) { |
78 | 78 | authHeaderParts := strings.Split(val, " ") |
79 | if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != BEARER { | |
79 | if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != bearer { | |
80 | 80 | return "", false |
81 | 81 | } |
82 | 82 | |
84 | 84 | } |
85 | 85 | |
86 | 86 | func generateAuthHeaderFromToken(token string) string { |
87 | return fmt.Sprintf(BEARER_FORMAT, token) | |
87 | return fmt.Sprintf(bearerFormat, token) | |
88 | 88 | } |