diff --git a/Dockerfile b/Dockerfile index 0c77a80..69fd1aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,3 +27,5 @@ WORKDIR /go/src/github.com/coreos/jwtproxy/ RUN go install -v github.com/coreos/jwtproxy/cmd/jwtproxy RUN rm -r /usr/local/go + +EXPOSE 8080 diff --git a/cmd/jwtproxy/main.go b/cmd/jwtproxy/main.go index 7e7755e..22e40a1 100644 --- a/cmd/jwtproxy/main.go +++ b/cmd/jwtproxy/main.go @@ -29,6 +29,8 @@ import ( _ "github.com/coreos/jwtproxy/jwt/keyserver/keyregistry" _ "github.com/coreos/jwtproxy/jwt/keyserver/keyregistry/keycache/memory" _ "github.com/coreos/jwtproxy/jwt/keyserver/preshared" + _ "github.com/coreos/jwtproxy/jwt/keyserver/jwks" + _ "github.com/coreos/jwtproxy/jwt/keyserver/jwks/keycache/memory" _ "github.com/coreos/jwtproxy/jwt/noncestorage/local" _ "github.com/coreos/jwtproxy/jwt/privatekey/autogenerated" _ "github.com/coreos/jwtproxy/jwt/privatekey/preshared" diff --git a/jwt/keyserver/jwks/jwks.go b/jwt/keyserver/jwks/jwks.go new file mode 100644 index 0000000..9b65b9d --- /dev/null +++ b/jwt/keyserver/jwks/jwks.go @@ -0,0 +1,184 @@ +// Copyright 2016 CoreOS, 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 jwks + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "path" + "sync" + "time" + + "github.com/coreos/go-oidc/jose" + "github.com/coreos/go-oidc/key" + "github.com/gregjones/httpcache" + "gopkg.in/yaml.v2" + + "github.com/coreos/jwtproxy/config" + "github.com/coreos/jwtproxy/jwt/keyserver" + "github.com/coreos/jwtproxy/jwt/keyserver/jwks/keycache" +) + +func init() { + keyserver.RegisterReader("jwks", constructReader) +} + +type client struct { + cache keycache.Cache + jwks *url.URL + signerParams config.SignerParams + stopping chan struct{} + inFlight *sync.WaitGroup + httpClient *http.Client +} + +type Config struct { + Jwks config.URL `yaml:"jwks"` +} + +type ReaderConfig struct { + Config `yaml:",inline"'` + Cache *config.RegistrableComponentConfig `yaml:"cache"` +} + +func (krc *client) GetPublicKey(issuer string, keyID string) (*key.PublicKey, error) { + // Query java web key set for a public key matching the given issuer and key ID. + pubkeyURL := krc.absURL(keyID) + pubkeyReq, err := krc.prepareRequest("GET", pubkeyURL, nil) + if err != nil { + return nil, err + } + resp, err := krc.httpClient.Do(pubkeyReq) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + switch resp.StatusCode { + case http.StatusNotFound: + return nil, keyserver.ErrPublicKeyNotFound + case http.StatusForbidden: + return nil, keyserver.ErrPublicKeyExpired + default: + return nil, keyserver.ErrUnkownResponse + } + } + + // Decode the public key we received as a JSON-encoded JWK. + var d struct { + Keys []jose.JWK `json:"keys"` + } + jsonDecoder := json.NewDecoder(resp.Body) + err = jsonDecoder.Decode(&d) + if err != nil { + return nil, err + } + if len(d.Keys) == 0 { + return nil, errors.New("zero keys in response") + } + ks := key.NewPublicKeySet(d.Keys, time.Now()) + + return ks.Key(keyID), nil +} + +func (krc *client) Stop() <-chan struct{} { + finished := make(chan struct{}) + // Stop the in flight requests + close(krc.stopping) + go func() { + krc.inFlight.Wait() + + // Now stop the cache + if krc.cache != nil { + <-krc.cache.Stop() + } + + close(finished) + }() + return finished +} + +func (krc *client) prepareRequest(method string, url *url.URL, body io.Reader) (*http.Request, error) { + // Create an HTTP request to the key server to publish a new key. + req, err := http.NewRequest(method, url.String(), body) + if err != nil { + return nil, err + } + + if method == "PUT" || method == "POST" { + req.Header.Add("Content-Type", "application/json") + } + + // Add our user agent. + req.Header.Set("User-Agent", "JWTProxy/0.1.0") + + return req, nil +} + +func (krc *client) absURL(pathParams ...string) *url.URL { + escaped := make([]string, 0, len(pathParams)+1) + escaped = append(escaped, krc.jwks.Path) + for _, pathParam := range pathParams { + escaped = append(escaped, url.QueryEscape(pathParam)) + } + + absPath := path.Join(escaped...) + relurl, err := url.Parse(absPath) + if err != nil { + panic(err) + } + return krc.jwks.ResolveReference(relurl) +} + +func constructReader(registrableComponentConfig config.RegistrableComponentConfig) (keyserver.Reader, error) { + bytes, err := yaml.Marshal(registrableComponentConfig.Options) + if err != nil { + return nil, err + } + var cfg ReaderConfig + err = yaml.Unmarshal(bytes, &cfg) + if err != nil { + return nil, err + } + + // Construct the public key cache. + cacheConfig := config.RegistrableComponentConfig{ + Type: "memory", + } + if cfg.Cache != nil { + cacheConfig = *cfg.Cache + } + + cache, err := keycache.NewCache(cacheConfig) + if err != nil { + return nil, fmt.Errorf("Unable to construct cache: %s", err) + } + + httpClient := &http.Client{ + Transport: httpcache.NewTransport(cache), + } + + return &client{ + jwks: cfg.Jwks.URL, + inFlight: &sync.WaitGroup{}, + stopping: make(chan struct{}), + cache: cache, + httpClient: httpClient, + }, nil +} diff --git a/jwt/keyserver/jwks/keycache/keycache.go b/jwt/keyserver/jwks/keycache/keycache.go new file mode 100644 index 0000000..b0a7c36 --- /dev/null +++ b/jwt/keyserver/jwks/keycache/keycache.go @@ -0,0 +1,51 @@ +// Copyright 2016 CoreOS, 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 keycache + +import ( + "fmt" + + "github.com/gregjones/httpcache" + + "github.com/coreos/jwtproxy/config" + "github.com/coreos/jwtproxy/stop" +) + +type Constructor func(config.RegistrableComponentConfig) (Cache, error) + +type Cache interface { + stop.Stoppable + httpcache.Cache +} + +var keycaches = make(map[string]Constructor) + +func RegisterCache(name string, c Constructor) { + if c == nil { + panic("server: could not register nil ReaderConstructor") + } + if _, dup := keycaches[name]; dup { + panic("server: could not register duplicate ReaderConstructor: " + name) + } + keycaches[name] = c +} + +func NewCache(cfg config.RegistrableComponentConfig) (Cache, error) { + c, ok := keycaches[cfg.Type] + if !ok { + return nil, fmt.Errorf("server: unknown Cache type %q (forgotten import?)", cfg.Type) + } + return c(cfg) +} diff --git a/jwt/keyserver/jwks/keycache/memory/memory.go b/jwt/keyserver/jwks/keycache/memory/memory.go new file mode 100644 index 0000000..7998f35 --- /dev/null +++ b/jwt/keyserver/jwks/keycache/memory/memory.go @@ -0,0 +1,44 @@ +// Copyright 2016 CoreOS, 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 memory + +import ( + log "github.com/Sirupsen/logrus" + "github.com/gregjones/httpcache" + + "github.com/coreos/jwtproxy/config" + "github.com/coreos/jwtproxy/jwt/keyserver/jwks/keycache" + "github.com/coreos/jwtproxy/stop" +) + +func init() { + keycache.RegisterCache("memory", constructor) +} + +type cache struct { + *httpcache.MemoryCache +} + +func constructor(registrableComponentConfig config.RegistrableComponentConfig) (keycache.Cache, error) { + log.Debug("Initializing in-memory key cache.") + + return &cache{ + MemoryCache: httpcache.NewMemoryCache(), + }, nil +} + +func (c *cache) Stop() <-chan struct{} { + return stop.AlreadyDone +}