From 71e548189dad80824fd5834e2a382d07cf8a2967 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 11:49:37 +0000 Subject: [PATCH] Bump github.com/MicahParks/keyfunc from 1.5.1 to 1.9.0 Bumps [github.com/MicahParks/keyfunc](https://github.com/MicahParks/keyfunc) from 1.5.1 to 1.9.0. - [Release notes](https://github.com/MicahParks/keyfunc/releases) - [Commits](https://github.com/MicahParks/keyfunc/compare/v1.5.1...v1.9.0) --- updated-dependencies: - dependency-name: github.com/MicahParks/keyfunc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 +- .../github.com/MicahParks/keyfunc/README.md | 18 ++- vendor/github.com/MicahParks/keyfunc/ecdsa.go | 8 ++ vendor/github.com/MicahParks/keyfunc/get.go | 86 ++++++++++---- vendor/github.com/MicahParks/keyfunc/given.go | 109 +++++++++++++++++- vendor/github.com/MicahParks/keyfunc/jwks.go | 46 +++++--- .../github.com/MicahParks/keyfunc/keyfunc.go | 27 ++++- .../github.com/MicahParks/keyfunc/multiple.go | 69 +++++++++++ .../github.com/MicahParks/keyfunc/options.go | 25 ++++ vendor/modules.txt | 2 +- 11 files changed, 342 insertions(+), 54 deletions(-) create mode 100644 vendor/github.com/MicahParks/keyfunc/multiple.go diff --git a/go.mod b/go.mod index 75b41c8de..e0c745929 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/CiscoM31/godata v1.0.8 github.com/KimMachineGun/automemlimit v0.3.0 github.com/Masterminds/semver v1.5.0 - github.com/MicahParks/keyfunc v1.5.1 + github.com/MicahParks/keyfunc v1.9.0 github.com/Nerzal/gocloak/v13 v13.8.0 github.com/bbalet/stopwords v1.0.0 github.com/blevesearch/bleve/v2 v2.3.9 diff --git a/go.sum b/go.sum index b33674caa..1cd15cead 100644 --- a/go.sum +++ b/go.sum @@ -651,8 +651,8 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/MicahParks/keyfunc v1.5.1 h1:RlyyYgKQI/adkIw1yXYtPvTAOb7hBhSX42aH23d8N0Q= -github.com/MicahParks/keyfunc v1.5.1/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= +github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= +github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= diff --git a/vendor/github.com/MicahParks/keyfunc/README.md b/vendor/github.com/MicahParks/keyfunc/README.md index 2fb1a216f..ed07eaa7f 100644 --- a/vendor/github.com/MicahParks/keyfunc/README.md +++ b/vendor/github.com/MicahParks/keyfunc/README.md @@ -70,7 +70,7 @@ jwksURL := os.Getenv("JWKS_URL") // Confirm the environment variable is not empty. if jwksURL == "" { -log.Fatalln("JWKS_URL environment variable must be populated.") + log.Fatalln("JWKS_URL environment variable must be populated.") } ``` @@ -81,7 +81,7 @@ Via HTTP: // Create the JWKS from the resource at the given URL. jwks, err := keyfunc.Get(jwksURL, keyfunc.Options{}) // See recommended options in the examples directory. if err != nil { -log.Fatalf("Failed to get the JWKS from the given URL.\nError: %s", err) + log.Fatalf("Failed to get the JWKS from the given URL.\nError: %s", err) } ``` Via JSON: @@ -92,7 +92,7 @@ var jwksJSON = json.RawMessage(`{"keys":[{"kid":"zXew0UJ1h6Q4CCcd_9wxMzvcp5cEBif // Create the JWKS from the resource at the given URL. jwks, err := keyfunc.NewJSON(jwksJSON) if err != nil { -log.Fatalf("Failed to create JWKS from JSON.\nError: %s", err) + log.Fatalf("Failed to create JWKS from JSON.\nError: %s", err) } ``` Via a given key: @@ -103,7 +103,7 @@ uniqueKeyID := "myKeyID" // Create the JWKS from the HMAC key. jwks := keyfunc.NewGiven(map[string]keyfunc.GivenKey{ -uniqueKeyID: keyfunc.NewGivenHMAC(key), + uniqueKeyID: keyfunc.NewGivenHMAC(key), }) ``` @@ -117,7 +117,7 @@ features mentioned at the bottom of this `README.md`. // Parse the JWT. token, err := jwt.Parse(jwtB64, jwks.Keyfunc) if err != nil { -return nil, fmt.Errorf("failed to parse token: %w", err) + return nil, fmt.Errorf("failed to parse token: %w", err) } ``` @@ -170,6 +170,9 @@ These features can be configured by populating fields in the * Custom cryptographic algorithms can be used. Make sure to use [`jwt.RegisterSigningMethod`](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#RegisterSigningMethod) before parsing JWTs. For an example, see the `examples/custom` directory. +* The remote JWKS resource can be refreshed manually using the `.Refresh` method. This can bypass the rate limit, if the + option is set. +* There is support for creating one `jwt.Keyfunc` from multiple JWK Sets through the use of the `keyfunc.GetMultiple`. ## Notes Trailing padding is required to be removed from base64url encoded keys inside a JWKS. This is because RFC 7517 defines @@ -180,6 +183,11 @@ base64url the same as RFC 7515 Section 2: However, this package will remove trailing padding on base64url encoded keys to account for improper implementations of JWKS. +This package will check the `alg` in each JWK. If present, it will confirm the same `alg` is in a given JWT's header +before returning the key for signature verification. If the `alg`s do not match, `keyfunc.ErrJWKAlgMismatch` will +prevent the key being used for signature verification. If the `alg` is not present in the JWK, this check will not +occur. + ## References This project was built and tested using various RFCs and services. The services are listed below: * [Keycloak](https://www.keycloak.org/) diff --git a/vendor/github.com/MicahParks/keyfunc/ecdsa.go b/vendor/github.com/MicahParks/keyfunc/ecdsa.go index 110562331..ca0566dd7 100644 --- a/vendor/github.com/MicahParks/keyfunc/ecdsa.go +++ b/vendor/github.com/MicahParks/keyfunc/ecdsa.go @@ -3,6 +3,7 @@ package keyfunc import ( "crypto/ecdsa" "crypto/elliptic" + "errors" "fmt" "math/big" ) @@ -21,6 +22,11 @@ const ( p521 = "P-521" ) +var ( + // ErrECDSACurve indicates an error with the ECDSA curve. + ErrECDSACurve = errors.New("invalid ECDSA curve") +) + // ECDSA parses a jsonWebKey and turns it into an ECDSA public key. func (j *jsonWebKey) ECDSA() (publicKey *ecdsa.PublicKey, err error) { if j.X == "" || j.Y == "" || j.Curve == "" { @@ -48,6 +54,8 @@ func (j *jsonWebKey) ECDSA() (publicKey *ecdsa.PublicKey, err error) { publicKey.Curve = elliptic.P384() case p521: publicKey.Curve = elliptic.P521() + default: + return nil, fmt.Errorf("%w: unknown curve: %s", ErrECDSACurve, j.Curve) } // Turn the X coordinate into *big.Int. diff --git a/vendor/github.com/MicahParks/keyfunc/get.go b/vendor/github.com/MicahParks/keyfunc/get.go index c196af2df..5dd754b74 100644 --- a/vendor/github.com/MicahParks/keyfunc/get.go +++ b/vendor/github.com/MicahParks/keyfunc/get.go @@ -3,6 +3,7 @@ package keyfunc import ( "bytes" "context" + "errors" "fmt" "net/http" "sync" @@ -10,6 +11,10 @@ import ( ) var ( + // ErrRefreshImpossible is returned when a refresh is attempted on a JWKS that was not created from a remote + // resource. + ErrRefreshImpossible = errors.New("refresh impossible: JWKS was not created from a remote resource") + // defaultRefreshTimeout is the default duration for the context used to create the HTTP request for a refresh of // the JWKS. defaultRefreshTimeout = time.Minute @@ -49,13 +54,53 @@ func Get(jwksURL string, options Options) (jwks *JWKS, err error) { if jwks.refreshInterval != 0 || jwks.refreshUnknownKID { jwks.ctx, jwks.cancel = context.WithCancel(context.Background()) - jwks.refreshRequests = make(chan context.CancelFunc, 1) + jwks.refreshRequests = make(chan refreshRequest, 1) go jwks.backgroundRefresh() } return jwks, nil } +// Refresh manually refreshes the JWKS with the remote resource. It can bypass the rate limit if configured to do so. +// This function will return an ErrRefreshImpossible if the JWKS was created from a static source like given keys or raw +// JSON, because there is no remote resource to refresh from. +// +// This function will block until the refresh is finished or an error occurs. +func (j *JWKS) Refresh(ctx context.Context, options RefreshOptions) error { + if j.jwksURL == "" { + return ErrRefreshImpossible + } + + // Check if the background goroutine was launched. + if j.refreshInterval != 0 || j.refreshUnknownKID { + ctx, cancel := context.WithCancel(ctx) + + req := refreshRequest{ + cancel: cancel, + ignoreRateLimit: options.IgnoreRateLimit, + } + + select { + case <-ctx.Done(): + return fmt.Errorf("failed to send request refresh to background goroutine: %w", j.ctx.Err()) + case j.refreshRequests <- req: + } + + <-ctx.Done() + + if !errors.Is(ctx.Err(), context.Canceled) { + return fmt.Errorf("unexpected keyfunc background refresh context error: %w", ctx.Err()) + } + } else { + err := j.refresh() + if err != nil { + return fmt.Errorf("failed to refresh JWKS: %w", err) + } + } + + return nil +} + // backgroundRefresh is meant to be a separate goroutine that will update the keys in a JWKS over a given interval of // time. func (j *JWKS) backgroundRefresh() { @@ -69,6 +114,14 @@ func (j *JWKS) backgroundRefresh() { // Create a channel that will never send anything unless there is a refresh interval. refreshInterval := make(<-chan time.Time) + refresh := func() { + err := j.refresh() + if err != nil && j.refreshErrorHandler != nil { + j.refreshErrorHandler(err) + } + lastRefresh = time.Now() + } + // Enter an infinite loop that ends when the background ends. for { if j.refreshInterval != 0 { @@ -80,16 +133,15 @@ func (j *JWKS) backgroundRefresh() { select { case <-j.ctx.Done(): return - case j.refreshRequests <- func() {}: + case j.refreshRequests <- refreshRequest{}: default: // If the j.refreshRequests channel is full, don't send another request. } - case cancel := <-j.refreshRequests: + case req := <-j.refreshRequests: refreshMux.Lock() - if j.refreshRateLimit != 0 && lastRefresh.Add(j.refreshRateLimit).After(time.Now()) { - // Don't make the JWT parsing goroutine wait for the JWKS to refresh. - cancel() - + if req.ignoreRateLimit { + refresh() + } else if j.refreshRateLimit != 0 && lastRefresh.Add(j.refreshRateLimit).After(time.Now()) { // Launch a goroutine that will get a reservation for a JWKS refresh or fail to and immediately return. queueOnce.Do(func() { go func() { @@ -104,25 +156,15 @@ func (j *JWKS) backgroundRefresh() { refreshMux.Lock() defer refreshMux.Unlock() - err := j.refresh() - if err != nil && j.refreshErrorHandler != nil { - j.refreshErrorHandler(err) - } - - lastRefresh = time.Now() + refresh() queueOnce = sync.Once{} }() }) } else { - err := j.refresh() - if err != nil && j.refreshErrorHandler != nil { - j.refreshErrorHandler(err) - } - - lastRefresh = time.Now() - - // Allow the JWT parsing goroutine to continue with the refreshed JWKS. - cancel() + refresh() + } + if req.cancel != nil { + req.cancel() } refreshMux.Unlock() diff --git a/vendor/github.com/MicahParks/keyfunc/given.go b/vendor/github.com/MicahParks/keyfunc/given.go index 649843110..68c8abd7f 100644 --- a/vendor/github.com/MicahParks/keyfunc/given.go +++ b/vendor/github.com/MicahParks/keyfunc/given.go @@ -4,11 +4,26 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" + "encoding/json" ) // GivenKey represents a cryptographic key that resides in a JWKS. In conjuncture with Options. type GivenKey struct { - inter interface{} + algorithm string + inter interface{} +} + +// GivenKeyOptions represents the configuration options for a GivenKey. +type GivenKeyOptions struct { + // Algorithm is the given key's signing algorithm. Its value will be compared to unverified tokens' "alg" header. + // + // See RFC 8725 Section 3.1 for details. + // https://www.rfc-editor.org/rfc/rfc8725#section-3.1 + // + // For a list of possible values, please see: + // https://www.rfc-editor.org/rfc/rfc7518#section-3.1 + // https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms + Algorithm string } // NewGiven creates a JWKS from a map of given keys. @@ -16,7 +31,10 @@ func NewGiven(givenKeys map[string]GivenKey) (jwks *JWKS) { keys := make(map[string]parsedJWK) for kid, given := range givenKeys { - keys[kid] = parsedJWK{public: given.inter} + keys[kid] = parsedJWK{ + algorithm: given.algorithm, + public: given.inter, + } } return &JWKS{ @@ -29,36 +47,123 @@ func NewGiven(givenKeys map[string]GivenKey) (jwks *JWKS) { // // See the https://pkg.go.dev/github.com/golang-jwt/jwt/v4#RegisterSigningMethod function for registering an unsupported // signing method. +// +// Deprecated: This function does not allow the user to specify the JWT's signing algorithm. Use +// NewGivenCustomWithOptions instead. func NewGivenCustom(key interface{}) (givenKey GivenKey) { return GivenKey{ inter: key, } } +// NewGivenCustomWithOptions creates a new GivenKey given an untyped variable. The key argument is expected to be a type +// supported by the jwt package used. +// +// Consider the options carefully as each field may have a security implication. +// +// See the https://pkg.go.dev/github.com/golang-jwt/jwt/v4#RegisterSigningMethod function for registering an unsupported +// signing method. +func NewGivenCustomWithOptions(key interface{}, options GivenKeyOptions) (givenKey GivenKey) { + return GivenKey{ + algorithm: options.Algorithm, + inter: key, + } +} + // NewGivenECDSA creates a new GivenKey given an ECDSA public key. +// +// Deprecated: This function does not allow the user to specify the JWT's signing algorithm. Use +// NewGivenECDSACustomWithOptions instead. func NewGivenECDSA(key *ecdsa.PublicKey) (givenKey GivenKey) { return GivenKey{ inter: key, } } +// NewGivenECDSACustomWithOptions creates a new GivenKey given an ECDSA public key. +// +// Consider the options carefully as each field may have a security implication. +func NewGivenECDSACustomWithOptions(key *ecdsa.PublicKey, options GivenKeyOptions) (givenKey GivenKey) { + return GivenKey{ + algorithm: options.Algorithm, + inter: key, + } +} + // NewGivenEdDSA creates a new GivenKey given an EdDSA public key. +// +// Deprecated: This function does not allow the user to specify the JWT's signing algorithm. Use +// NewGivenEdDSACustomWithOptions instead. func NewGivenEdDSA(key ed25519.PublicKey) (givenKey GivenKey) { return GivenKey{ inter: key, } } +// NewGivenEdDSACustomWithOptions creates a new GivenKey given an EdDSA public key. +// +// Consider the options carefully as each field may have a security implication. +func NewGivenEdDSACustomWithOptions(key ed25519.PublicKey, options GivenKeyOptions) (givenKey GivenKey) { + return GivenKey{ + algorithm: options.Algorithm, + inter: key, + } +} + // NewGivenHMAC creates a new GivenKey given an HMAC key in a byte slice. +// +// Deprecated: This function does not allow the user to specify the JWT's signing algorithm. Use +// NewGivenHMACCustomWithOptions instead. func NewGivenHMAC(key []byte) (givenKey GivenKey) { return GivenKey{ inter: key, } } +// NewGivenHMACCustomWithOptions creates a new GivenKey given an HMAC key in a byte slice. +// +// Consider the options carefully as each field may have a security implication. +func NewGivenHMACCustomWithOptions(key []byte, options GivenKeyOptions) (givenKey GivenKey) { + return GivenKey{ + algorithm: options.Algorithm, + inter: key, + } +} + // NewGivenRSA creates a new GivenKey given an RSA public key. +// +// Deprecated: This function does not allow the user to specify the JWT's signing algorithm. Use +// NewGivenRSACustomWithOptions instead. func NewGivenRSA(key *rsa.PublicKey) (givenKey GivenKey) { return GivenKey{ inter: key, } } + +// NewGivenRSACustomWithOptions creates a new GivenKey given an RSA public key. +// +// Consider the options carefully as each field may have a security implication. +func NewGivenRSACustomWithOptions(key *rsa.PublicKey, options GivenKeyOptions) (givenKey GivenKey) { + return GivenKey{ + algorithm: options.Algorithm, + inter: key, + } +} + +// NewGivenKeysFromJSON parses a raw JSON message into a map of key IDs (`kid`) to GivenKeys. The returned map is +// suitable for passing to `NewGiven()` or as `Options.GivenKeys` to `Get()` +func NewGivenKeysFromJSON(jwksBytes json.RawMessage) (map[string]GivenKey, error) { + // Parse by making a temporary JWKS instance. No need to lock its map since it doesn't escape this function. + j, err := NewJSON(jwksBytes) + if err != nil { + return nil, err + } + keys := make(map[string]GivenKey, len(j.keys)) + for kid, cryptoKey := range j.keys { + keys[kid] = GivenKey{ + algorithm: cryptoKey.algorithm, + inter: cryptoKey.public, + } + } + return keys, nil +} diff --git a/vendor/github.com/MicahParks/keyfunc/jwks.go b/vendor/github.com/MicahParks/keyfunc/jwks.go index 9a429bfe4..3c75eda76 100644 --- a/vendor/github.com/MicahParks/keyfunc/jwks.go +++ b/vendor/github.com/MicahParks/keyfunc/jwks.go @@ -11,6 +11,10 @@ import ( ) var ( + // ErrJWKAlgMismatch indicates that the given JWK was found, but its "alg" parameter's value did not match that of + // the JWT. + ErrJWKAlgMismatch = errors.New(`the given JWK was found, but its "alg" parameter's value did not match the expected algorithm`) + // ErrJWKUseWhitelist indicates that the given JWK was found, but its "use" parameter's value was not whitelisted. ErrJWKUseWhitelist = errors.New(`the given JWK was found, but its "use" parameter's value was not whitelisted`) @@ -39,21 +43,23 @@ type JWKUse string // jsonWebKey represents a JSON Web Key inside a JWKS. type jsonWebKey struct { - Curve string `json:"crv"` - Exponent string `json:"e"` - K string `json:"k"` - ID string `json:"kid"` - Modulus string `json:"n"` - Type string `json:"kty"` - Use string `json:"use"` - X string `json:"x"` - Y string `json:"y"` + Algorithm string `json:"alg"` + Curve string `json:"crv"` + Exponent string `json:"e"` + K string `json:"k"` + ID string `json:"kid"` + Modulus string `json:"n"` + Type string `json:"kty"` + Use string `json:"use"` + X string `json:"x"` + Y string `json:"y"` } // parsedJWK represents a JSON Web Key parsed with fields as the correct Go types. type parsedJWK struct { - use JWKUse - public interface{} + algorithm string + public interface{} + use JWKUse } // JWKS represents a JSON Web Key Set (JWK Set). @@ -71,7 +77,7 @@ type JWKS struct { refreshErrorHandler ErrorHandler refreshInterval time.Duration refreshRateLimit time.Duration - refreshRequests chan context.CancelFunc + refreshRequests chan refreshRequest refreshTimeout time.Duration refreshUnknownKID bool requestFactory func(ctx context.Context, url string) (*http.Request, error) @@ -124,8 +130,9 @@ func NewJSON(jwksBytes json.RawMessage) (jwks *JWKS, err error) { } jwks.keys[key.ID] = parsedJWK{ - use: JWKUse(key.Use), - public: keyInter, + algorithm: key.Algorithm, + use: JWKUse(key.Use), + public: keyInter, } } @@ -181,7 +188,7 @@ func (j *JWKS) ReadOnlyKeys() map[string]interface{} { } // getKey gets the jsonWebKey from the given KID from the JWKS. It may refresh the JWKS if configured to. -func (j *JWKS) getKey(kid string) (jsonKey interface{}, err error) { +func (j *JWKS) getKey(alg, kid string) (jsonKey interface{}, err error) { j.mux.RLock() pubKey, ok := j.keys[kid] j.mux.RUnlock() @@ -192,12 +199,15 @@ func (j *JWKS) getKey(kid string) (jsonKey interface{}, err error) { } ctx, cancel := context.WithCancel(j.ctx) + req := refreshRequest{ + cancel: cancel, + } // Refresh the JWKS. select { case <-j.ctx.Done(): return - case j.refreshRequests <- cancel: + case j.refreshRequests <- req: default: // If the j.refreshRequests channel is full, return the error early. return nil, ErrKIDNotFound @@ -221,5 +231,9 @@ func (j *JWKS) getKey(kid string) (jsonKey interface{}, err error) { } } + if pubKey.algorithm != "" && pubKey.algorithm != alg { + return nil, fmt.Errorf(`%w: JWK "alg" parameter value %q does not match token "alg" parameter value %q`, ErrJWKAlgMismatch, pubKey.algorithm, alg) + } + return pubKey.public, nil } diff --git a/vendor/github.com/MicahParks/keyfunc/keyfunc.go b/vendor/github.com/MicahParks/keyfunc/keyfunc.go index f74f2fa27..1f082bda0 100644 --- a/vendor/github.com/MicahParks/keyfunc/keyfunc.go +++ b/vendor/github.com/MicahParks/keyfunc/keyfunc.go @@ -16,16 +16,33 @@ var ( // Keyfunc matches the signature of github.com/golang-jwt/jwt/v4's jwt.Keyfunc function. func (j *JWKS) Keyfunc(token *jwt.Token) (interface{}, error) { + kid, alg, err := kidAlg(token) + if err != nil { + return nil, err + } + return j.getKey(alg, kid) +} + +func (m *MultipleJWKS) Keyfunc(token *jwt.Token) (interface{}, error) { + return m.keySelector(m, token) +} + +func kidAlg(token *jwt.Token) (kid, alg string, err error) { kidInter, ok := token.Header["kid"] if !ok { - return nil, fmt.Errorf("%w: could not find kid in JWT header", ErrKID) + return "", "", fmt.Errorf("%w: could not find kid in JWT header", ErrKID) } - kid, ok := kidInter.(string) + kid, ok = kidInter.(string) if !ok { - return nil, fmt.Errorf("%w: could not convert kid in JWT header to string", ErrKID) + return "", "", fmt.Errorf("%w: could not convert kid in JWT header to string", ErrKID) } - - return j.getKey(kid) + alg, ok = token.Header["alg"].(string) + if !ok { + // For test coverage purposes, this should be impossible to reach because the JWT package rejects a token + // without an alg parameter in the header before calling jwt.Keyfunc. + return "", "", fmt.Errorf(`%w: the JWT header did not contain the "alg" parameter, which is required by RFC 7515 section 4.1.1`, ErrJWKAlgMismatch) + } + return kid, alg, nil } // base64urlTrailingPadding removes trailing padding before decoding a string from base64url. Some non-RFC compliant diff --git a/vendor/github.com/MicahParks/keyfunc/multiple.go b/vendor/github.com/MicahParks/keyfunc/multiple.go new file mode 100644 index 000000000..61ea30b80 --- /dev/null +++ b/vendor/github.com/MicahParks/keyfunc/multiple.go @@ -0,0 +1,69 @@ +package keyfunc + +import ( + "errors" + "fmt" + + "github.com/golang-jwt/jwt/v4" +) + +// ErrMultipleJWKSSize is returned when the number of JWKS given are not enough to make a MultipleJWKS. +var ErrMultipleJWKSSize = errors.New("multiple JWKS must have two or more remote JWK Set resources") + +// MultipleJWKS manages multiple JWKS and has a field for jwt.Keyfunc. +type MultipleJWKS struct { + keySelector func(multiJWKS *MultipleJWKS, token *jwt.Token) (key interface{}, err error) + sets map[string]*JWKS // No lock is required because this map is read-only after initialization. +} + +// GetMultiple creates a new MultipleJWKS. A map of length two or more JWKS URLs to Options is required. +// +// Be careful when choosing Options for each JWKS in the map. If RefreshUnknownKID is set to true for all JWKS in the +// map then many refresh requests would take place each time a JWT is processed, this should be rate limited by +// RefreshRateLimit. +func GetMultiple(multiple map[string]Options, options MultipleOptions) (multiJWKS *MultipleJWKS, err error) { + if multiple == nil || len(multiple) < 2 { + return nil, fmt.Errorf("multiple JWKS must have two or more remote JWK Set resources: %w", ErrMultipleJWKSSize) + } + + if options.KeySelector == nil { + options.KeySelector = KeySelectorFirst + } + + multiJWKS = &MultipleJWKS{ + sets: make(map[string]*JWKS, len(multiple)), + keySelector: options.KeySelector, + } + + for u, opts := range multiple { + jwks, err := Get(u, opts) + if err != nil { + return nil, fmt.Errorf("failed to get JWKS from %q: %w", u, err) + } + multiJWKS.sets[u] = jwks + } + + return multiJWKS, nil +} + +func (m *MultipleJWKS) JWKSets() map[string]*JWKS { + sets := make(map[string]*JWKS, len(m.sets)) + for u, jwks := range m.sets { + sets[u] = jwks + } + return sets +} + +func KeySelectorFirst(multiJWKS *MultipleJWKS, token *jwt.Token) (key interface{}, err error) { + kid, alg, err := kidAlg(token) + if err != nil { + return nil, err + } + for _, jwks := range multiJWKS.sets { + key, err = jwks.getKey(alg, kid) + if err == nil { + return key, nil + } + } + return nil, fmt.Errorf("failed to find key ID in multiple JWKS: %w", ErrKIDNotFound) +} diff --git a/vendor/github.com/MicahParks/keyfunc/options.go b/vendor/github.com/MicahParks/keyfunc/options.go index a4b77a733..cc4cf5efe 100644 --- a/vendor/github.com/MicahParks/keyfunc/options.go +++ b/vendor/github.com/MicahParks/keyfunc/options.go @@ -8,6 +8,8 @@ import ( "io" "net/http" "time" + + "github.com/golang-jwt/jwt/v4" ) // ErrInvalidHTTPStatusCode indicates that the HTTP status code is invalid. @@ -70,6 +72,9 @@ type Options struct { // This is done through a background goroutine. Without specifying a RefreshInterval a malicious client could // self-sign X JWTs, send them to this service, then cause potentially high network usage proportional to X. Make // sure to call the JWKS.EndBackground method to end this goroutine when it's no longer needed. + // + // It is recommended this option is not used when in MultipleJWKS. This is because KID collisions SHOULD be uncommon + // meaning nearly any JWT SHOULD trigger a refresh for the number of JWKS in the MultipleJWKS minus one. RefreshUnknownKID bool // RequestFactory creates HTTP requests for the remote JWKS resource located at the given url. For example, an @@ -81,6 +86,26 @@ type Options struct { ResponseExtractor func(ctx context.Context, resp *http.Response) (json.RawMessage, error) } +// MultipleOptions is used to configure the behavior when multiple JWKS are used by MultipleJWKS. +type MultipleOptions struct { + // KeySelector is a function that selects the key to use for a given token. It will be used in the implementation + // for jwt.Keyfunc. If implementing this custom selector extract the key ID and algorithm from the token's header. + // Use the key ID to select a token and confirm the key's algorithm before returning it. + // + // This value defaults to KeySelectorFirst. + KeySelector func(multiJWKS *MultipleJWKS, token *jwt.Token) (key interface{}, err error) +} + +// RefreshOptions are used to specify manual refresh behavior. +type RefreshOptions struct { + IgnoreRateLimit bool +} + +type refreshRequest struct { + cancel context.CancelFunc + ignoreRateLimit bool +} + // ResponseExtractorStatusOK is meant to be used as the ResponseExtractor field for Options. It confirms that response // status code is 200 OK and returns the raw JSON from the response body. func ResponseExtractorStatusOK(ctx context.Context, resp *http.Response) (json.RawMessage, error) { diff --git a/vendor/modules.txt b/vendor/modules.txt index 59ec18677..c84d411c4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -24,7 +24,7 @@ github.com/Masterminds/semver # github.com/Masterminds/sprig v2.22.0+incompatible ## explicit github.com/Masterminds/sprig -# github.com/MicahParks/keyfunc v1.5.1 +# github.com/MicahParks/keyfunc v1.9.0 ## explicit; go 1.16 github.com/MicahParks/keyfunc # github.com/Microsoft/go-winio v0.6.0