jwt support
diff --git a/adapter/BUILD b/adapter/BUILD index f42b913..a864253 100644 --- a/adapter/BUILD +++ b/adapter/BUILD
@@ -12,12 +12,14 @@ "applications.go", "products.go", "rejecter.go", + "jwt.go", ], deps = [ "//adapter/config:go_default_library", "//common:go_default_library", "//external:mixer_adapter", "@com_github_hashicorp_go_multierror//:go_default_library", + "@com_github_dgrijalva_jwtgo//:go_default_library", ], ) @@ -33,5 +35,6 @@ library = ":go_default_library", deps = [ "//mock:go_default_library", + ], )
diff --git a/adapter/apigeeKeyAttributes.go b/adapter/apigeeKeyAttributes.go index 6971f98..5c5047c 100644 --- a/adapter/apigeeKeyAttributes.go +++ b/adapter/apigeeKeyAttributes.go
@@ -19,7 +19,7 @@ import ( "fmt" "strconv" - + "strings" "github.com/apid/istioApigeeAdapter/adapter/config" "istio.io/mixer/pkg/adapter" ) @@ -29,6 +29,7 @@ keyAttrsDesc = "Set attributes based on an Apigee API key" keyParam = "apiKey" + jwtParam = "jwtToken" pathParam = "requestPath" successParam = "success" successStringParam = "successString" @@ -47,6 +48,7 @@ env adapter.Env applications *applicationManager products *productManager + jwtVerifier *jwtVerifier } func newKeyAttrsBuilder() adapter.AttributesGeneratorBuilder { @@ -62,10 +64,13 @@ func (b keyAttrsBuilder) BuildAttributesGenerator(env adapter.Env, c adapter.Config) (adapter.AttributesGenerator, error) { cfg := c.(*config.VerifyKeyParams) verifyPath, productsPath := getPaths(cfg) + publicKeyUrl := getPulicKeyUrl(cfg) + g := &keyAttrsGenerator{ env: env, applications: newApplicationManager(env, defaultAppLifetime, verifyPath), products: newProductManager(productsPath, defaultProductsFetch), + jwtVerifier: newVerifier(defaultKeyFetch, publicKeyUrl), } env.Logger().Infof("Created Apigee attributes generator to invoke \"%s\"", verifyPath) env.Logger().Infof("Checking API products using \"%s\"", productsPath) @@ -80,14 +85,48 @@ out[successStringParam] = "false" key := getString(in, keyParam) - if key == "" { + jwt_token := getString(in, jwtParam) + + if key == "" && jwt_token == "" { return out, nil } + path := getString(in, pathParam) if path == "" { return out, nil } + if jwt_token != "" { + if strings.HasPrefix(jwt_token, "Bearer") { + token := strings.Split(jwt_token, " ")[1] + jwt_token = token + } else { + g.env.Logger().Errorf("Cannot verify jwt token, Bearer missing") + return out, nil + } + + claims, err := g.jwtVerifier.Verify(jwt_token) + + if err != nil { + g.env.Logger().Errorf("Cannot verify jwt token : %s", err) + return out, nil + } + + // considers token as valid if exp, iat, iss is not present + err = claims.Valid() + + // invalid token + if err != nil { + g.env.Logger().Errorf("jwt token invalid : %s", err) + return out, nil + } + + // TODO: check scopes + + // prioritizing jwt over apikey (if user passes both jwt and apikey) + key = claims.(*apigeeClaims).ClientId + } + // Look up API key from cache, making HTTP request if necessary app, err := g.applications.get(key) if err != nil { @@ -116,9 +155,14 @@ if success { out[productNameParam] = products[0].Name } + return out, nil } func (g *keyAttrsGenerator) Close() error { return nil } + +func getPulicKeyUrl(cfg *config.VerifyKeyParams) (string) { + return fmt.Sprintf(defaultPublicKeyURL, cfg.Organization, cfg.Environment) +} \ No newline at end of file
diff --git a/adapter/jwt.go b/adapter/jwt.go new file mode 100644 index 0000000..290ba62 --- /dev/null +++ b/adapter/jwt.go
@@ -0,0 +1,112 @@ +/* +Copyright 2017 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package adapter + +// TODO: key rotation + +import( + // ref: https://godoc.org/github.com/dgrijalva/jwt-go , https://github.com/dgrijalva/jwt-go + jwt "github.com/dgrijalva/jwt-go" + "fmt" + "regexp" + "net/http" + "io/ioutil" + "time" +) + +const ( + defaultKeyFetch = time.Hour + defaultPublicKeyURL = "https://%s-%s.apigee.net/edgemicro-auth/publicKey" +) + +type apigeeClaims struct { + jwt.StandardClaims + ClientId string `json:"client_id,omitempty"` + ApplicationName string `json:"application_name,omitempty"` + Scopes []string `json:"scopes,omitempty"` +} + +type jwtVerifier struct { + parser *jwt.Parser + refresh time.Duration + public_key_rsa256 []byte + // TODO add support for other algorithms +} + +// return: verifier object that can be used to verify JWT +// input: keys +// TODO: input HMAC and ECDSA keys +func newVerifier(refresh time.Duration, public_key_url string) (*jwtVerifier){ + verifier_obj_pointer := new(jwtVerifier) + + // TODO: handle error + public_key_rsa256_raw_data, _ := getHttp(public_key_url) + + // TODO: store parsed key + verifier_obj_pointer.public_key_rsa256 = public_key_rsa256_raw_data + verifier_obj_pointer.parser = new(jwt.Parser) + // TODO: add more methods based on keys supplied to this function, while adding support for other algorithms + verifier_obj_pointer.parser.ValidMethods = []string{"RS256"} + + return verifier_obj_pointer +} + +// Verify a token and output the claims +func (this *jwtVerifier) Verify(to_verify_token string) (jwt.Claims, error) { + + // get byte stream of token + to_verify_token_in_bytes := []byte(to_verify_token) + + // trim possible whitespace from token + to_verify_token_in_bytes = regexp.MustCompile(`\s*$`).ReplaceAll(to_verify_token_in_bytes, []byte{}) + + // Parse the token + token, err := this.parser.ParseWithClaims(string(to_verify_token), &apigeeClaims{}, func(t *jwt.Token) (interface{}, error) { + // this function should return right key to parse the token + if(t.Method.Alg() == "RS256") { + return jwt.ParseRSAPublicKeyFromPEM(this.public_key_rsa256) + } else { + return nil, fmt.Errorf("signing method not supported") + } + }) + + if err != nil { + return nil, fmt.Errorf("Couldn't parse token: %v", err) + } + + if !token.Valid { + return nil, fmt.Errorf("Token is invalid") + } + + // return jwt.Claim object (https://github.com/dgrijalva/jwt-go/blob/master/claims.go#L11) (http://godoc.org/github.com/dgrijalva/jwt-go#Claims) + return token.Claims, err +} + +func getHttp(public_key_url string) ([]byte, error) { + resp, err := http.Get(public_key_url) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("HTTP error fetching from url", public_key_url, " with response code ", resp.StatusCode) + } + + return ioutil.ReadAll(resp.Body) +} \ No newline at end of file