From 224083c45ab72e82faf6f1d80bdb79f6b1bf2916 Mon Sep 17 00:00:00 2001 From: Philipp Hug Date: Tue, 8 Nov 2016 16:08:11 +0100 Subject: [PATCH 1/4] add support for json web key set URL. --- cmd/jwtproxy/main.go | 1 + jwt/keyserver/jwks/jwks.go | 175 +++++++++++++++++++ jwt/keyserver/jwks/keycache/keycache.go | 51 ++++++ jwt/keyserver/jwks/keycache/memory/memory.go | 44 +++++ 4 files changed, 271 insertions(+) create mode 100644 jwt/keyserver/jwks/jwks.go create mode 100644 jwt/keyserver/jwks/keycache/keycache.go create mode 100644 jwt/keyserver/jwks/keycache/memory/memory.go diff --git a/cmd/jwtproxy/main.go b/cmd/jwtproxy/main.go index 7e7755e..1759830 100644 --- a/cmd/jwtproxy/main.go +++ b/cmd/jwtproxy/main.go @@ -29,6 +29,7 @@ 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/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..b571e01 --- /dev/null +++ b/jwt/keyserver/jwks/jwks.go @@ -0,0 +1,175 @@ +// 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" + "fmt" + "io" + "net/http" + "net/url" + "path" + "sync" + + "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 pk key.PublicKey + jsonDecoder := json.NewDecoder(resp.Body) + err = jsonDecoder.Decode(&pk) + if err != nil { + return nil, err + } + + return &pk, 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 +} From 70107692a8657703874a22f50f87927b7a21e5d1 Mon Sep 17 00:00:00 2001 From: Philipp Hug Date: Mon, 21 Nov 2016 14:14:14 +0100 Subject: [PATCH 2/4] Expose 8080 --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) 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 From 33d43792bab8754f6b4f6f28881b295622be9221 Mon Sep 17 00:00:00 2001 From: Philipp Hug Date: Mon, 21 Nov 2016 14:33:42 +0100 Subject: [PATCH 3/4] add missing import --- cmd/jwtproxy/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/jwtproxy/main.go b/cmd/jwtproxy/main.go index 1759830..22e40a1 100644 --- a/cmd/jwtproxy/main.go +++ b/cmd/jwtproxy/main.go @@ -30,6 +30,7 @@ import ( _ "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" From ede3096fbc0ce4d546dc284209be12797170fdf5 Mon Sep 17 00:00:00 2001 From: Philipp Hug Date: Fri, 9 Dec 2016 18:29:12 +0100 Subject: [PATCH 4/4] JWKS is a set, parse accordingly --- jwt/keyserver/jwks/jwks.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/jwt/keyserver/jwks/jwks.go b/jwt/keyserver/jwks/jwks.go index b571e01..9b65b9d 100644 --- a/jwt/keyserver/jwks/jwks.go +++ b/jwt/keyserver/jwks/jwks.go @@ -16,13 +16,16 @@ 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" @@ -78,14 +81,20 @@ func (krc *client) GetPublicKey(issuer string, keyID string) (*key.PublicKey, er } // Decode the public key we received as a JSON-encoded JWK. - var pk key.PublicKey + var d struct { + Keys []jose.JWK `json:"keys"` + } jsonDecoder := json.NewDecoder(resp.Body) - err = jsonDecoder.Decode(&pk) + 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 &pk, nil + return ks.Key(keyID), nil } func (krc *client) Stop() <-chan struct{} {