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 14:51:01 +02:00
committed by Ralf Haferkamp
parent 3f40a42bb2
commit 71e548189d
11 changed files with 342 additions and 54 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ require (
github.com/CiscoM31/godata v1.0.8 github.com/CiscoM31/godata v1.0.8
github.com/KimMachineGun/automemlimit v0.3.0 github.com/KimMachineGun/automemlimit v0.3.0
github.com/Masterminds/semver v1.5.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/Nerzal/gocloak/v13 v13.8.0
github.com/bbalet/stopwords v1.0.0 github.com/bbalet/stopwords v1.0.0
github.com/blevesearch/bleve/v2 v2.3.9 github.com/blevesearch/bleve/v2 v2.3.9
+2 -2
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/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 h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 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.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.5.1/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= 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.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.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+13 -5
View File
@@ -70,7 +70,7 @@ jwksURL := os.Getenv("JWKS_URL")
// Confirm the environment variable is not empty. // Confirm the environment variable is not empty.
if jwksURL == "" { 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. // Create the JWKS from the resource at the given URL.
jwks, err := keyfunc.Get(jwksURL, keyfunc.Options{}) // See recommended options in the examples directory. jwks, err := keyfunc.Get(jwksURL, keyfunc.Options{}) // See recommended options in the examples directory.
if err != nil { 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: Via JSON:
@@ -92,7 +92,7 @@ var jwksJSON = json.RawMessage(`{"keys":[{"kid":"zXew0UJ1h6Q4CCcd_9wxMzvcp5cEBif
// Create the JWKS from the resource at the given URL. // Create the JWKS from the resource at the given URL.
jwks, err := keyfunc.NewJSON(jwksJSON) jwks, err := keyfunc.NewJSON(jwksJSON)
if err != nil { 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: Via a given key:
@@ -103,7 +103,7 @@ uniqueKeyID := "myKeyID"
// Create the JWKS from the HMAC key. // Create the JWKS from the HMAC key.
jwks := keyfunc.NewGiven(map[string]keyfunc.GivenKey{ 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. // Parse the JWT.
token, err := jwt.Parse(jwtB64, jwks.Keyfunc) token, err := jwt.Parse(jwtB64, jwks.Keyfunc)
if err != nil { 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 * Custom cryptographic algorithms can be used. Make sure to
use [`jwt.RegisterSigningMethod`](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#RegisterSigningMethod) before 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. 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 ## Notes
Trailing padding is required to be removed from base64url encoded keys inside a JWKS. This is because RFC 7517 defines 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 However, this package will remove trailing padding on base64url encoded keys to account for improper implementations of
JWKS. 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 ## References
This project was built and tested using various RFCs and services. The services are listed below: This project was built and tested using various RFCs and services. The services are listed below:
* [Keycloak](https://www.keycloak.org/) * [Keycloak](https://www.keycloak.org/)
+8
View File
@@ -3,6 +3,7 @@ package keyfunc
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"errors"
"fmt" "fmt"
"math/big" "math/big"
) )
@@ -21,6 +22,11 @@ const (
p521 = "P-521" 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. // ECDSA parses a jsonWebKey and turns it into an ECDSA public key.
func (j *jsonWebKey) ECDSA() (publicKey *ecdsa.PublicKey, err error) { func (j *jsonWebKey) ECDSA() (publicKey *ecdsa.PublicKey, err error) {
if j.X == "" || j.Y == "" || j.Curve == "" { if j.X == "" || j.Y == "" || j.Curve == "" {
@@ -48,6 +54,8 @@ func (j *jsonWebKey) ECDSA() (publicKey *ecdsa.PublicKey, err error) {
publicKey.Curve = elliptic.P384() publicKey.Curve = elliptic.P384()
case p521: case p521:
publicKey.Curve = elliptic.P521() publicKey.Curve = elliptic.P521()
default:
return nil, fmt.Errorf("%w: unknown curve: %s", ErrECDSACurve, j.Curve)
} }
// Turn the X coordinate into *big.Int. // Turn the X coordinate into *big.Int.
+64 -22
View File
@@ -3,6 +3,7 @@ package keyfunc
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"sync" "sync"
@@ -10,6 +11,10 @@ import (
) )
var ( 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 // defaultRefreshTimeout is the default duration for the context used to create the HTTP request for a refresh of
// the JWKS. // the JWKS.
defaultRefreshTimeout = time.Minute defaultRefreshTimeout = time.Minute
@@ -49,13 +54,53 @@ func Get(jwksURL string, options Options) (jwks *JWKS, err error) {
if jwks.refreshInterval != 0 || jwks.refreshUnknownKID { if jwks.refreshInterval != 0 || jwks.refreshUnknownKID {
jwks.ctx, jwks.cancel = context.WithCancel(context.Background()) jwks.ctx, jwks.cancel = context.WithCancel(context.Background())
jwks.refreshRequests = make(chan context.CancelFunc, 1) jwks.refreshRequests = make(chan refreshRequest, 1)
go jwks.backgroundRefresh() go jwks.backgroundRefresh()
} }
return jwks, nil 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 // backgroundRefresh is meant to be a separate goroutine that will update the keys in a JWKS over a given interval of
// time. // time.
func (j *JWKS) backgroundRefresh() { 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. // Create a channel that will never send anything unless there is a refresh interval.
refreshInterval := make(<-chan time.Time) 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. // Enter an infinite loop that ends when the background ends.
for { for {
if j.refreshInterval != 0 { if j.refreshInterval != 0 {
@@ -80,16 +133,15 @@ func (j *JWKS) backgroundRefresh() {
select { select {
case <-j.ctx.Done(): case <-j.ctx.Done():
return return
case j.refreshRequests <- func() {}: case j.refreshRequests <- refreshRequest{}:
default: // If the j.refreshRequests channel is full, don't send another request. default: // If the j.refreshRequests channel is full, don't send another request.
} }
case cancel := <-j.refreshRequests: case req := <-j.refreshRequests:
refreshMux.Lock() refreshMux.Lock()
if j.refreshRateLimit != 0 && lastRefresh.Add(j.refreshRateLimit).After(time.Now()) { if req.ignoreRateLimit {
// Don't make the JWT parsing goroutine wait for the JWKS to refresh. refresh()
cancel() } 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. // Launch a goroutine that will get a reservation for a JWKS refresh or fail to and immediately return.
queueOnce.Do(func() { queueOnce.Do(func() {
go func() { go func() {
@@ -104,25 +156,15 @@ func (j *JWKS) backgroundRefresh() {
refreshMux.Lock() refreshMux.Lock()
defer refreshMux.Unlock() defer refreshMux.Unlock()
err := j.refresh() refresh()
if err != nil && j.refreshErrorHandler != nil {
j.refreshErrorHandler(err)
}
lastRefresh = time.Now()
queueOnce = sync.Once{} queueOnce = sync.Once{}
}() }()
}) })
} else { } else {
err := j.refresh() refresh()
if err != nil && j.refreshErrorHandler != nil { }
j.refreshErrorHandler(err) if req.cancel != nil {
} req.cancel()
lastRefresh = time.Now()
// Allow the JWT parsing goroutine to continue with the refreshed JWKS.
cancel()
} }
refreshMux.Unlock() refreshMux.Unlock()
+107 -2
View File
@@ -4,11 +4,26 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/ed25519" "crypto/ed25519"
"crypto/rsa" "crypto/rsa"
"encoding/json"
) )
// GivenKey represents a cryptographic key that resides in a JWKS. In conjuncture with Options. // GivenKey represents a cryptographic key that resides in a JWKS. In conjuncture with Options.
type GivenKey struct { 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. // 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) keys := make(map[string]parsedJWK)
for kid, given := range givenKeys { for kid, given := range givenKeys {
keys[kid] = parsedJWK{public: given.inter} keys[kid] = parsedJWK{
algorithm: given.algorithm,
public: given.inter,
}
} }
return &JWKS{ 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 // See the https://pkg.go.dev/github.com/golang-jwt/jwt/v4#RegisterSigningMethod function for registering an unsupported
// signing method. // 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) { func NewGivenCustom(key interface{}) (givenKey GivenKey) {
return GivenKey{ return GivenKey{
inter: key, 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. // 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) { func NewGivenECDSA(key *ecdsa.PublicKey) (givenKey GivenKey) {
return GivenKey{ return GivenKey{
inter: key, 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. // 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) { func NewGivenEdDSA(key ed25519.PublicKey) (givenKey GivenKey) {
return GivenKey{ return GivenKey{
inter: key, 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. // 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) { func NewGivenHMAC(key []byte) (givenKey GivenKey) {
return GivenKey{ return GivenKey{
inter: key, 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. // 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) { func NewGivenRSA(key *rsa.PublicKey) (givenKey GivenKey) {
return GivenKey{ return GivenKey{
inter: key, 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
}
+30 -16
View File
@@ -11,6 +11,10 @@ import (
) )
var ( 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 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`) 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. // jsonWebKey represents a JSON Web Key inside a JWKS.
type jsonWebKey struct { type jsonWebKey struct {
Curve string `json:"crv"` Algorithm string `json:"alg"`
Exponent string `json:"e"` Curve string `json:"crv"`
K string `json:"k"` Exponent string `json:"e"`
ID string `json:"kid"` K string `json:"k"`
Modulus string `json:"n"` ID string `json:"kid"`
Type string `json:"kty"` Modulus string `json:"n"`
Use string `json:"use"` Type string `json:"kty"`
X string `json:"x"` Use string `json:"use"`
Y string `json:"y"` X string `json:"x"`
Y string `json:"y"`
} }
// parsedJWK represents a JSON Web Key parsed with fields as the correct Go types. // parsedJWK represents a JSON Web Key parsed with fields as the correct Go types.
type parsedJWK struct { type parsedJWK struct {
use JWKUse algorithm string
public interface{} public interface{}
use JWKUse
} }
// JWKS represents a JSON Web Key Set (JWK Set). // JWKS represents a JSON Web Key Set (JWK Set).
@@ -71,7 +77,7 @@ type JWKS struct {
refreshErrorHandler ErrorHandler refreshErrorHandler ErrorHandler
refreshInterval time.Duration refreshInterval time.Duration
refreshRateLimit time.Duration refreshRateLimit time.Duration
refreshRequests chan context.CancelFunc refreshRequests chan refreshRequest
refreshTimeout time.Duration refreshTimeout time.Duration
refreshUnknownKID bool refreshUnknownKID bool
requestFactory func(ctx context.Context, url string) (*http.Request, error) 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{ jwks.keys[key.ID] = parsedJWK{
use: JWKUse(key.Use), algorithm: key.Algorithm,
public: keyInter, 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. // 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() j.mux.RLock()
pubKey, ok := j.keys[kid] pubKey, ok := j.keys[kid]
j.mux.RUnlock() j.mux.RUnlock()
@@ -192,12 +199,15 @@ func (j *JWKS) getKey(kid string) (jsonKey interface{}, err error) {
} }
ctx, cancel := context.WithCancel(j.ctx) ctx, cancel := context.WithCancel(j.ctx)
req := refreshRequest{
cancel: cancel,
}
// Refresh the JWKS. // Refresh the JWKS.
select { select {
case <-j.ctx.Done(): case <-j.ctx.Done():
return return
case j.refreshRequests <- cancel: case j.refreshRequests <- req:
default: default:
// If the j.refreshRequests channel is full, return the error early. // If the j.refreshRequests channel is full, return the error early.
return nil, ErrKIDNotFound 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 return pubKey.public, nil
} }
+22 -5
View File
@@ -16,16 +16,33 @@ var (
// Keyfunc matches the signature of github.com/golang-jwt/jwt/v4's jwt.Keyfunc function. // Keyfunc matches the signature of github.com/golang-jwt/jwt/v4's jwt.Keyfunc function.
func (j *JWKS) Keyfunc(token *jwt.Token) (interface{}, error) { 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"] kidInter, ok := token.Header["kid"]
if !ok { 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 { 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)
} }
alg, ok = token.Header["alg"].(string)
return j.getKey(kid) 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 // base64urlTrailingPadding removes trailing padding before decoding a string from base64url. Some non-RFC compliant
+69
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)
}
+25
View File
@@ -8,6 +8,8 @@ import (
"io" "io"
"net/http" "net/http"
"time" "time"
"github.com/golang-jwt/jwt/v4"
) )
// ErrInvalidHTTPStatusCode indicates that the HTTP status code is invalid. // 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 // 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 // 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. // 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 RefreshUnknownKID bool
// RequestFactory creates HTTP requests for the remote JWKS resource located at the given url. For example, an // 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) 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 // 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. // 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) { func ResponseExtractorStatusOK(ctx context.Context, resp *http.Response) (json.RawMessage, error) {
+1 -1
View File
@@ -24,7 +24,7 @@ github.com/Masterminds/semver
# github.com/Masterminds/sprig v2.22.0+incompatible # github.com/Masterminds/sprig v2.22.0+incompatible
## explicit ## explicit
github.com/Masterminds/sprig github.com/Masterminds/sprig
# github.com/MicahParks/keyfunc v1.5.1 # github.com/MicahParks/keyfunc v1.9.0
## explicit; go 1.16 ## explicit; go 1.16
github.com/MicahParks/keyfunc github.com/MicahParks/keyfunc
# github.com/Microsoft/go-winio v0.6.0 # github.com/Microsoft/go-winio v0.6.0