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