blob: 1088725345584a442d8d8b24e1c6045b1af04eb7 [file] [log] [blame]
/*
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
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/apid/istioApigeeAdapter/common"
"istio.io/mixer/pkg/adapter"
)
const (
defaultAppLifetime = 5 * time.Second
)
type application struct {
clientID string
valid bool
expiration time.Time
name string
apiProducts []string
}
type applicationJWT struct {
ClientID string `json:"client_id"`
ApplicationName string `json:"application_name"`
APIProducts []string `json:"api_product_list"`
}
type applicationManager struct {
env adapter.Env
lifetime time.Duration
cache map[string]*application
checkURL string
latch *sync.Mutex
}
func newApplicationManager(env adapter.Env, lifetime time.Duration, checkURL string) *applicationManager {
return &applicationManager{
env: env,
lifetime: lifetime,
checkURL: checkURL,
cache: make(map[string]*application),
latch: &sync.Mutex{},
}
}
func (a *applicationManager) get(key string) (*application, error) {
app := a.cacheGet(key)
if app != nil {
return app, nil
}
app, err := a.lookupKey(key)
if err != nil {
return nil, err
}
a.cachePut(key, app)
return app, nil
}
func (a *applicationManager) cacheGet(key string) *application {
a.latch.Lock()
app := a.cache[key]
if app != nil && app.expiration.Before(time.Now()) {
delete(a.cache, key)
app = nil
}
a.latch.Unlock()
return app
}
func (a *applicationManager) cachePut(key string, app *application) {
a.latch.Lock()
a.cache[key] = app
a.latch.Unlock()
}
func (a *applicationManager) lookupKey(key string) (*application, error) {
apiKeyBody := common.VerifyAPIKeyRequest{
Key: key,
}
requestBody, _ := json.Marshal(&apiKeyBody)
resp, err := http.DefaultClient.Post(a.checkURL, "application/json",
bytes.NewBuffer(requestBody))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
return a.parseApplication(resp.Body)
} else if resp.StatusCode == 401 {
return &application{
clientID: key,
valid: false,
expiration: time.Now().Add(a.lifetime),
}, nil
} else {
return nil, fmt.Errorf("HTTP error looking up application: %d", resp.StatusCode)
}
}
func (a *applicationManager) parseApplication(r io.Reader) (*application, error) {
var keyResponse common.VerifyAPIKeyResponse
jr := json.NewDecoder(r)
err := jr.Decode(&keyResponse)
if err != nil {
return nil, fmt.Errorf("Invalid JSON in API key response: %s", err)
}
// We got back a JWT, but we don't need to verify it -- just parse the JSON we need
parts := strings.Split(keyResponse.Token, ".")
if len(parts) != 3 {
return nil, errors.New("Invalid JWT returned")
}
// base64 from Apigee is unpadded, so use "raw" encoding here.
rawJWT, err := base64.RawStdEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("Error decoding JWT base 64: from \"%s\", %s", keyResponse.Token, err)
}
var jwt applicationJWT
err = json.Unmarshal(rawJWT, &jwt)
if err != nil {
return nil, fmt.Errorf("Error decoding JWT json: %s", err)
}
a.env.Logger().Infof("Looked up data for application %s", jwt.ApplicationName)
return &application{
clientID: jwt.ClientID,
valid: true,
expiration: time.Now().Add(a.lifetime),
name: jwt.ApplicationName,
apiProducts: jwt.APIProducts,
}, nil
}