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] <support@github.com>
This commit is contained in:
dependabot[bot]
2023-09-05 11:49:37 +00:00
committed by Ralf Haferkamp
parent 3f40a42bb2
commit 71e548189d
11 changed files with 342 additions and 54 deletions

2
go.mod
View File

@@ -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

4
go.sum
View File

@@ -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=

View File

@@ -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/)

View File

@@ -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.

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

69
vendor/github.com/MicahParks/keyfunc/multiple.go generated vendored Normal file
View File

@@ -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)
}

View File

@@ -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) {

2
vendor/modules.txt vendored
View File

@@ -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