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:
committed by
Ralf Haferkamp
parent
3f40a42bb2
commit
71e548189d
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||||
|
|||||||
Vendored
+1
-1
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user