From 211016b05e924e565314d3966997621d577b0cba Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 8 Feb 2019 01:26:45 +0000 Subject: [PATCH] Fetch JWKs from OIDC URLs --- fetch.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++ fetch_test.go | 34 +++++++++++++++++ keypairs.go | 49 +++++++++++++++++++----- 3 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 fetch.go create mode 100644 fetch_test.go diff --git a/fetch.go b/fetch.go new file mode 100644 index 0000000..4f66e02 --- /dev/null +++ b/fetch.go @@ -0,0 +1,101 @@ +package keypairs + +import ( + "crypto" + "encoding/json" + "errors" + "io" + "net" + "net/http" + "time" +) + +var EInvalidJWKURL = errors.New("url does not lead to valid JWKs") + +func FetchOIDCPublicKeys(host string) ([]crypto.PublicKey, error) { + oidcConf := struct { + JWKSURI string `json:"jwks_uri"` + }{} + // must come in as https:/// + url := host + ".well-known/openid-configuration" + err := safeFetch(url, func(body io.Reader) error { + return json.NewDecoder(body).Decode(&oidcConf) + }) + if nil != err { + return nil, err + } + + return FetchPublicKeys(oidcConf.JWKSURI) +} + +func FetchPublicKeys(jwksurl string) ([]crypto.PublicKey, error) { + var keys []crypto.PublicKey + resp := struct { + Keys []map[string]interface{} `json:"keys"` + }{ + Keys: make([]map[string]interface{}, 0, 1), + } + + if err := safeFetch(jwksurl, func(body io.Reader) error { + return json.NewDecoder(body).Decode(&resp) + }); nil != err { + return nil, err + } + + for i := range resp.Keys { + n := map[string]string{} + k := resp.Keys[i] + + // convert map[string]interface{} to map[string]string + for j := range k { + switch s := k[j].(type) { + case string: + n[j] = s + default: + // safely ignore + } + } + + if key, err := NewJWKPublicKey(n); nil != err { + return nil, err + } else { + keys = append(keys, key) + } + } + + return keys, nil +} + +func FetchPublicKey(url string) (crypto.PublicKey, error) { + var m map[string]string + if err := safeFetch(url, func(body io.Reader) error { + return json.NewDecoder(body).Decode(&m) + }); nil != err { + return nil, err + } + + return NewJWKPublicKey(m) +} + +type decodeFunc func(io.Reader) error + +func safeFetch(url string, decoder decodeFunc) error { + var netTransport = &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 5 * time.Second, + }).Dial, + TLSHandshakeTimeout: 5 * time.Second, + } + var netClient = &http.Client{ + Timeout: time.Second * 10, + Transport: netTransport, + } + + res, err := netClient.Get(url) + if nil != err { + return err + } + defer res.Body.Close() + + return decoder(res.Body) +} diff --git a/fetch_test.go b/fetch_test.go new file mode 100644 index 0000000..d1ba9da --- /dev/null +++ b/fetch_test.go @@ -0,0 +1,34 @@ +package keypairs + +import ( + "crypto/ecdsa" + "crypto/rsa" + "errors" + "testing" +) + +func TestFetchOIDCPublicKeys(t *testing.T) { + urls := []string{ + //"https://bigsquid.auth0.com/.well-known/jwks.json", + "https://bigsquid.auth0.com/", + "https://api-dev.bigsquid.com/", + } + for i := range urls { + url := urls[i] + keys, err := FetchOIDCPublicKeys(url) + if nil != err { + t.Fatal(url, err) + } + + for i := range keys { + switch key := keys[i].(type) { + case *rsa.PublicKey: + _ = ThumbprintRSAPublicKey(key) + case *ecdsa.PublicKey: + _ = ThumbprintECPublicKey(key) + default: + t.Fatal(errors.New("unsupported interface type")) + } + } + } +} diff --git a/keypairs.go b/keypairs.go index 1ac0c1a..9710716 100644 --- a/keypairs.go +++ b/keypairs.go @@ -13,6 +13,7 @@ import ( "encoding/pem" "errors" "fmt" + "io" "math/big" ) @@ -166,23 +167,53 @@ func parsePrivateKey(der []byte) (PrivateKey, error) { return key, nil } -func ParseJWKPublicKey(b []byte) (crypto.PublicKey, error) { - m := make(map[string]string) - err := json.Unmarshal(b, &m) - if nil != err { - return nil, err - } - +func NewJWKPublicKey(m map[string]string) (crypto.PublicKey, error) { switch m["kty"] { case "RSA": return parseRSAPublicKey(m) case "EC": return parseECPublicKey(m) default: - err = EInvalidKeyType + return nil, EInvalidKeyType + } +} + +func ParseJWKPublicKey(b []byte) (crypto.PublicKey, error) { + return newJWKPublicKey(b) +} + +func ParseJWKPublicKeyString(s string) (crypto.PublicKey, error) { + return newJWKPublicKey(s) +} + +func DecodeJWKPublicKey(r io.Reader) (crypto.PublicKey, error) { + return newJWKPublicKey(r) +} + +func newJWKPublicKey(data interface{}) (crypto.PublicKey, error) { + var m map[string]string + + switch d := data.(type) { + case map[string]string: + m = d + case io.Reader: + m = make(map[string]string) + if err := json.NewDecoder(d).Decode(&m); nil != err { + return nil, err + } + case string: + if err := json.Unmarshal([]byte(d), &m); nil != err { + return nil, err + } + case []byte: + if err := json.Unmarshal(d, &m); nil != err { + return nil, err + } + default: + panic("Developer Error: unsupported interface type") } - return nil, err + return NewJWKPublicKey(m) } func ParseJWKPrivateKey(b []byte) (PrivateKey, error) {