From 1aa0e2d1982737d3e608cd2be24f8d891520d5a6 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sun, 23 Apr 2023 21:28:19 -0400 Subject: [PATCH 01/18] Upgrade to github.com/golang-jwt/v5 and use github.com/MicahParks/keyfunc/v2 for JWK Set client --- config.go | 155 ++++++++++++---------- crypto.go | 117 ----------------- go.mod | 3 +- go.sum | 6 +- jwks.go | 361 --------------------------------------------------- jwt.go | 21 --- main.go | 2 +- main_test.go | 13 +- 8 files changed, 96 insertions(+), 582 deletions(-) delete mode 100644 jwks.go diff --git a/config.go b/config.go index d4156fa..f397533 100644 --- a/config.go +++ b/config.go @@ -1,21 +1,15 @@ package jwtware import ( + "fmt" "strings" "time" + "github.com/MicahParks/keyfunc/v2" "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" ) -// KeyRefreshSuccessHandler is a function signature that consumes a set of signing key set. -// Presence of original signing key set allows to update configuration or stop background refresh. -type KeyRefreshSuccessHandler func(j *KeySet) - -// KeyRefreshErrorHandler is a function signature that consumes a set of signing key set and an error. -// Presence of original signing key set allows to update configuration or stop background refresh. -type KeyRefreshErrorHandler func(j *KeySet, err error) - // Config defines the config for JWT middleware type Config struct { // Filter defines a function to skip middleware. @@ -39,47 +33,6 @@ type Config struct { // Required. This, SigningKey or KeySetUrl(deprecated) or KeySetUrls. SigningKeys map[string]interface{} - // URL where set of private keys could be downloaded. - // Required. This, SigningKey or SigningKeys or KeySetURLs - // Deprecated, use KeySetURLs - KeySetURL string - - // URLs where set of private keys could be downloaded. - // Required. This, SigningKey or SigningKeys or KeySetURL(deprecated) - // duplicate key entries are overwritten as encountered across urls - KeySetURLs []string - - // KeyRefreshSuccessHandler defines a function which is executed on successful refresh of key set. - // Optional. Default: nil - KeyRefreshSuccessHandler KeyRefreshSuccessHandler - - // KeyRefreshErrorHandler defines a function which is executed for refresh key set failure. - // Optional. Default: nil - KeyRefreshErrorHandler KeyRefreshErrorHandler - - // KeyRefreshInterval is the duration to refresh the JWKs in the background via a new HTTP request. If this is not nil, - // then a background refresh will be requested in a separate goroutine at this interval until the JWKs method - // EndBackground is called. - // Optional. If set, the value will be used only if `KeySetUrl`(deprecated) or `KeySetUrls` is also present - KeyRefreshInterval *time.Duration - - // KeyRefreshRateLimit limits the rate at which refresh requests are granted. Only one refresh request can be queued - // at a time any refresh requests received while there is already a queue are ignored. It does not make sense to - // have RefreshInterval's value shorter than this. - // Optional. If set, the value will be used only if `KeySetUrl`(deprecated) or `KeySetUrls` is also present - KeyRefreshRateLimit *time.Duration - - // KeyRefreshTimeout is the duration for the context used to create the HTTP request for a refresh of the JWKs. This - // defaults to one minute. This is only effectual if RefreshInterval is not nil. - // Optional. If set, the value will be used only if `KeySetUrl`(deprecated) or `KeySetUrls` is also present - KeyRefreshTimeout *time.Duration - - // KeyRefreshUnknownKID indicates that the JWKs refresh request will occur every time a kid that isn't cached is seen. - // 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. - // Optional. If set, the value will be used only if `KeySetUrl`(deprecated) or `KeySetUrls` is also present - KeyRefreshUnknownKID *bool - // Signing method, used to check token signing method. // Optional. Default: "HS256". // Possible values: "HS256", "HS384", "HS512", "ES256", "ES384", "ES512", "RS256", "RS384", "RS512" @@ -107,16 +60,26 @@ type Config struct { // Optional. Default: "Bearer". AuthScheme string - // KeyFunc defines a user-defined function that supplies the public key for a token validation. + // KeyFunc is a function that supplies the public key for JWT cryptographic verification. // The function shall take care of verifying the signing algorithm and selecting the proper key. - // A user-defined KeyFunc can be useful if tokens are issued by an external party. + // Internally, github.com/MicahParks/keyfunc/v2 package is used project defaults. If you need more customization, + // you can provide a jwt.Keyfunc using that package or make your own implementation. + // + // This option is mutually exclusive with and takes precedence over JWKSetURLs, SigningKeys, and SigningKey. + KeyFunc jwt.Keyfunc // TODO Could be renamed to Keyfunc + + // JWKSetURLs is a slice of HTTP URLs that contain the JSON Web Key Set (JWKS) used to verify the signatures of + // JWTs. Use of HTTPS is recommended. The presence of the "kid" field in the JWT header and JWKs is mandatory for + // this feature. + // + // By default, all JWK Sets in this slice will: + // * Refresh every hour. + // * Refresh automatically if a new "kid" is seen in a JWT being verified. + // * Rate limit refreshes to once every 5 minutes. + // * Timeout refreshes after 10 seconds. // - // When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored. - // This is one of the three options to provide a token validation key. - // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. - // Required if neither SigningKeys nor SigningKey is provided. - // Default to an internal implementation verifying the signing algorithm and selecting the proper key. - KeyFunc jwt.Keyfunc + // This field is compatible with the SigningKeys field. + JWKSetURLs []string } // makeCfg function will check correctness of supplied configuration @@ -138,13 +101,10 @@ func makeCfg(config []Config) (cfg Config) { return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired JWT") } } - if cfg.KeySetURL != "" { - cfg.KeySetURLs = append(cfg.KeySetURLs, cfg.KeySetURL) + if cfg.SigningKey == nil && len(cfg.SigningKeys) == 0 && len(cfg.JWKSetURLs) == 0 && cfg.KeyFunc == nil { + panic("Fiber: JWT middleware requires at least one signing key or JWK Set URL") } - if cfg.SigningKey == nil && len(cfg.SigningKeys) == 0 && len(cfg.KeySetURLs) == 0 && cfg.KeyFunc == nil { - panic("Fiber: JWT middleware requires signing key or url where to download one") - } - if cfg.SigningMethod == "" && len(cfg.KeySetURLs) == 0 { + if cfg.SigningMethod == "" && len(cfg.JWKSetURLs) == 0 { cfg.SigningMethod = "HS256" } if cfg.ContextKey == "" { @@ -160,23 +120,74 @@ func makeCfg(config []Config) (cfg Config) { cfg.AuthScheme = "Bearer" } } - if cfg.KeyRefreshTimeout == nil { - cfg.KeyRefreshTimeout = &defaultKeyRefreshTimeout - } if cfg.KeyFunc == nil { - if len(cfg.KeySetURLs) > 0 { - jwks := &KeySet{ - Config: &cfg, + if len(cfg.SigningKeys) > 0 || len(cfg.JWKSetURLs) > 0 { + var givenKeys map[string]keyfunc.GivenKey + if cfg.SigningKeys != nil { + givenKeys = make(map[string]keyfunc.GivenKey, len(cfg.SigningKeys)) + for kid, key := range cfg.SigningKeys { + givenKeys[kid] = keyfunc.NewGivenCustom(key, keyfunc.GivenKeyOptions{}) // TODO User supplied alg? + } + } + if len(cfg.JWKSetURLs) > 0 { + var err error + if len(cfg.JWKSetURLs) == 1 { + cfg.KeyFunc, err = oneKeyfunc(cfg, givenKeys) + } else { + cfg.KeyFunc, err = multiKeyfunc(givenKeys, cfg.JWKSetURLs) + } + if err != nil { + panic("Failed to create keyfunc from JWK Set URL: " + err.Error()) // TODO Don't panic? + } + } else { + cfg.KeyFunc = keyfunc.NewGiven(givenKeys).Keyfunc } - cfg.KeyFunc = jwks.keyFunc() } else { - cfg.KeyFunc = jwtKeyFunc(cfg) + cfg.KeyFunc = func(token *jwt.Token) (interface{}, error) { + return cfg.SigningKey, nil + } } } + return cfg } +func oneKeyfunc(cfg Config, givenKeys map[string]keyfunc.GivenKey) (jwt.Keyfunc, error) { + jwks, err := keyfunc.Get(cfg.JWKSetURLs[0], keyfuncOptions(givenKeys)) + if err != nil { + return nil, fmt.Errorf("failed to get JWK Set URL: %w", err) + } + return jwks.Keyfunc, nil +} + +func multiKeyfunc(givenKeys map[string]keyfunc.GivenKey, jwkSetURLs []string) (jwt.Keyfunc, error) { + opts := keyfuncOptions(givenKeys) + multiple := make(map[string]keyfunc.Options, len(jwkSetURLs)) + for _, url := range jwkSetURLs { + multiple[url] = opts + } + multiOpts := keyfunc.MultipleOptions{ + KeySelector: keyfunc.KeySelectorFirst, + } + multi, err := keyfunc.GetMultiple(multiple, multiOpts) + if err != nil { + return nil, fmt.Errorf("failed to get multiple JWK Set URLs: %w", err) + } + return multi.Keyfunc, nil +} + +func keyfuncOptions(givenKeys map[string]keyfunc.GivenKey) keyfunc.Options { + return keyfunc.Options{ + // TODO Add error logger? + GivenKeys: givenKeys, + RefreshInterval: time.Hour, + RefreshRateLimit: time.Minute * 5, + RefreshTimeout: time.Second * 10, + RefreshUnknownKID: true, + } +} + // getExtractors function will create a slice of functions which will be used // for token sarch and will perform extraction of the value func (cfg *Config) getExtractors() []jwtExtractor { diff --git a/crypto.go b/crypto.go index 0f58d0c..fa8106b 100644 --- a/crypto.go +++ b/crypto.go @@ -1,14 +1,5 @@ package jwtware -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rsa" - "encoding/base64" - "fmt" - "math/big" -) - const ( // HS256 represents a public cryptography key generated by a 256 bit HMAC algorithm. HS256 = "HS256" @@ -55,111 +46,3 @@ const ( // PS512 represents a public cryptography key generated by a 512 bit RSA algorithm. PS512 = "PS512" ) - -// getECDSA parses a JSONKey and turns it into an ECDSA public key. -func (j *rawJWK) getECDSA() (publicKey *ecdsa.PublicKey, err error) { - // Check if the key has already been computed. - if j.precomputed != nil { - var ok bool - if publicKey, ok = j.precomputed.(*ecdsa.PublicKey); ok { - return publicKey, nil - } - } - - // Confirm everything needed is present. - if j.X == "" || j.Y == "" || j.Curve == "" { - return nil, fmt.Errorf("%w: ecdsa", errMissingAssets) - } - - // Decode the X coordinate from Base64. - // - // According to RFC 7518, this is a Base64 URL unsigned integer. - // https://tools.ietf.org/html/rfc7518#section-6.3 - var xCoordinate []byte - if xCoordinate, err = base64.RawURLEncoding.DecodeString(j.X); err != nil { - return nil, err - } - - // Decode the Y coordinate from Base64. - var yCoordinate []byte - if yCoordinate, err = base64.RawURLEncoding.DecodeString(j.Y); err != nil { - return nil, err - } - - // Create the ECDSA public key. - publicKey = &ecdsa.PublicKey{} - - // Set the curve type. - var curve elliptic.Curve - switch j.Curve { - case P256: - curve = elliptic.P256() - case P384: - curve = elliptic.P384() - case P521: - curve = elliptic.P521() - } - publicKey.Curve = curve - - // Turn the X coordinate into *big.Int. - // - // According to RFC 7517, these numbers are in big-endian format. - // https://tools.ietf.org/html/rfc7517#appendix-A.1 - publicKey.X = big.NewInt(0).SetBytes(xCoordinate) - - // Turn the Y coordinate into a *big.Int. - publicKey.Y = big.NewInt(0).SetBytes(yCoordinate) - - // Keep the public key so it won't have to be computed every time. - j.precomputed = publicKey - - return publicKey, nil -} - -// getRSA parses a JSONKey and turns it into an RSA public key. -func (j *rawJWK) getRSA() (publicKey *rsa.PublicKey, err error) { - // Check if the key has already been computed. - if j.precomputed != nil { - var ok bool - if publicKey, ok = j.precomputed.(*rsa.PublicKey); ok { - return publicKey, nil - } - } - - // Confirm everything needed is present. - if j.Exponent == "" || j.Modulus == "" { - return nil, fmt.Errorf("%w: rsa", errMissingAssets) - } - - // Decode the exponent from Base64. - // - // According to RFC 7518, this is a Base64 URL unsigned integer. - // https://tools.ietf.org/html/rfc7518#section-6.3 - var exponent []byte - if exponent, err = base64.RawURLEncoding.DecodeString(j.Exponent); err != nil { - return nil, err - } - - // Decode the modulus from Base64. - var modulus []byte - if modulus, err = base64.RawURLEncoding.DecodeString(j.Modulus); err != nil { - return nil, err - } - - // Create the RSA public key. - publicKey = &rsa.PublicKey{} - - // Turn the exponent into an integer. - // - // According to RFC 7517, these numbers are in big-endian format. - // https://tools.ietf.org/html/rfc7517#appendix-A.1 - publicKey.E = int(big.NewInt(0).SetBytes(exponent).Uint64()) - - // Turn the modulus into a *big.Int. - publicKey.N = big.NewInt(0).SetBytes(modulus) - - // Keep the public key so it won't have to be computed every time. - j.precomputed = publicKey - - return publicKey, nil -} diff --git a/go.mod b/go.mod index 7e6c94a..b4f6e6d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/gofiber/jwt/v3 go 1.16 require ( + github.com/MicahParks/keyfunc/v2 v2.0.1 github.com/gofiber/fiber/v2 v2.44.0 - github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/golang-jwt/jwt/v5 v5.0.0 ) diff --git a/go.sum b/go.sum index bd90747..7301f20 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,11 @@ +github.com/MicahParks/keyfunc/v2 v2.0.1 h1:6FrNNvG/20gEKkjxV+5anrkq0VOF666G2zUn8lk8dgk= +github.com/MicahParks/keyfunc/v2 v2.0.1/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/gofiber/fiber/v2 v2.44.0 h1:Z90bEvPcJM5GFJnu1py0E1ojoerkyew3iiNJ78MQCM8= github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= diff --git a/jwks.go b/jwks.go deleted file mode 100644 index 62d2afc..0000000 --- a/jwks.go +++ /dev/null @@ -1,361 +0,0 @@ -package jwtware - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "net/http" - "sync" - "time" - - "github.com/golang-jwt/jwt/v4" -) - -var ( // ErrKID indicates that the JWT had an invalid kid. - errMissingKeySet = errors.New("not able to download JWKs") - - // errKID indicates that the JWT had an invalid kid. - errKID = errors.New("the JWT has an invalid kid") - - // errUnsupportedKeyType indicates the JWT key type is an unsupported type. - errUnsupportedKeyType = errors.New("the JWT key type is unsupported") - - // errKIDNotFound indicates that the given key ID was not found in the JWKs. - errKIDNotFound = errors.New("the given key ID was not found in the JWKs") - - // errMissingAssets indicates there are required assets missing to create a public key. - errMissingAssets = errors.New("required assets are missing to create a public key") -) - -// rawJWK represents a raw key inside a JWKs. -type rawJWK struct { - Curve string `json:"crv"` - Exponent string `json:"e"` - ID string `json:"kid"` - Modulus string `json:"n"` - X string `json:"x"` - Y string `json:"y"` - precomputed interface{} -} - -// rawJWKs represents a JWKs in JSON format. -type rawJWKs struct { - Keys []rawJWK `json:"keys"` -} - -// KeySet represents a JSON Web Key Set. -type KeySet struct { - Keys map[string]*rawJWK - Config *Config - cancel context.CancelFunc - client *http.Client - ctx context.Context - mux sync.RWMutex - refreshRequests chan context.CancelFunc -} - -// keyFunc is a compatibility function that matches the signature of github.com/dgrijalva/jwt-go's keyFunc function. -func (j *KeySet) keyFunc() jwt.Keyfunc { - return func(token *jwt.Token) (interface{}, error) { - if j.Keys == nil { - err := j.downloadKeySet() - if err != nil { - return nil, fmt.Errorf("%w: key set URL is not accessible", errMissingKeySet) - } - } - - // Get the kid from the token header. - kidInter, ok := token.Header["kid"] - if !ok { - return nil, fmt.Errorf("%w: could not find kid in JWT header", errKID) - } - kid, ok := kidInter.(string) - if !ok { - return nil, fmt.Errorf("%w: could not convert kid in JWT header to string", errKID) - } - - // Get the JSONKey. - jsonKey, err := j.getKey(kid) - if err != nil { - return nil, err - } - - // Determine the key's algorithm and return the appropriate public key. - switch keyAlg := token.Header["alg"]; keyAlg { - case ES256, ES384, ES512: - return jsonKey.getECDSA() - case PS256, PS384, PS512, RS256, RS384, RS512: - return jsonKey.getRSA() - default: - return nil, fmt.Errorf("%w: %s: feel free to add a feature request or contribute to https://github.com/MicahParks/keyfunc", errUnsupportedKeyType, keyAlg) - } - } -} - -// downloadKeySet loads the JWKs at the given URL. -func (j *KeySet) downloadKeySet() (err error) { - // Apply some defaults if options were not provided. - if j.client == nil { - j.client = http.DefaultClient - } - - // Get the keys for the JWKs. - if err = j.refresh(); err != nil { - return err - } - - // Check to see if a background refresh of the JWKs should happen. - if j.Config.KeyRefreshInterval != nil || j.Config.KeyRefreshRateLimit != nil { - // Attach a context used to end the background goroutine. - j.ctx, j.cancel = context.WithCancel(context.Background()) - - // Create a channel that will accept requests to refresh the JWKs. - j.refreshRequests = make(chan context.CancelFunc, 1) - - // Start the background goroutine for data refresh. - go j.startRefreshing() - } - - return nil -} - -// New creates a new JWKs from a raw JSON message. -func parseKeySet(jwksBytes json.RawMessage) (keys map[string]*rawJWK, err error) { - // Turn the raw JWKs into the correct Go type. - var rawKS rawJWKs - if err = json.Unmarshal(jwksBytes, &rawKS); err != nil { - return nil, err - } - - // Iterate through the keys in the raw JWKs. Add them to the JWKs. - keys = make(map[string]*rawJWK, len(rawKS.Keys)) - for _, key := range rawKS.Keys { - key := key - keys[key.ID] = &key - } - - return keys, nil -} - -// getKey gets the JSONKey from the given KID from the JWKs. It may refresh the JWKs if configured to. -func (j *KeySet) getKey(kid string) (jsonKey *rawJWK, err error) { - // Get the JSONKey from the JWKs. - var ok bool - j.mux.RLock() - jsonKey, ok = j.Keys[kid] - j.mux.RUnlock() - - // Check if the key was present. - if !ok { - // Check to see if configured to refresh on unknown kid. - if j.Config.KeyRefreshUnknownKID != nil && *j.Config.KeyRefreshUnknownKID { - // Create a context for refreshing the JWKs. - ctx, cancel := context.WithCancel(j.ctx) - - // Refresh the JWKs. - select { - case <-j.ctx.Done(): - return - case j.refreshRequests <- cancel: - default: - - // If the j.refreshRequests channel is full, return the error early. - return nil, errKIDNotFound - } - - // Wait for the JWKs refresh to done. - <-ctx.Done() - - // Lock the JWKs for async safe use. - j.mux.RLock() - defer j.mux.RUnlock() - - // Check if the JWKs refresh contained the requested key. - if jsonKey, ok = j.Keys[kid]; ok { - return jsonKey, nil - } - } - - return nil, errKIDNotFound - } - - return jsonKey, nil -} - -// startRefreshing is meant to be a separate goroutine that will update the keys in a JWKs over a given interval of -// time. -func (j *KeySet) startRefreshing() { - // Create some rate limiting assets. - var lastRefresh time.Time - var queueOnce sync.Once - var refreshMux sync.Mutex - if j.Config.KeyRefreshRateLimit != nil { - lastRefresh = time.Now().Add(-*j.Config.KeyRefreshRateLimit) - } - - // Create a channel that will never send anything unless there is a refresh interval. - refreshInterval := make(<-chan time.Time) - - // Enter an infinite loop that ends when the background ends. - for { - // If there is a refresh interval, create the channel for it. - if j.Config.KeyRefreshInterval != nil { - refreshInterval = time.After(*j.Config.KeyRefreshInterval) - } - - // Wait for a refresh to occur or the background to end. - select { - - // Send a refresh request the JWKs after the given interval. - case <-refreshInterval: - select { - case <-j.ctx.Done(): - return - case j.refreshRequests <- func() {}: - default: // If the j.refreshRequests channel is full, don't don't send another request. - } - - // Accept refresh requests. - case cancel := <-j.refreshRequests: - // Rate limit, if needed. - refreshMux.Lock() - if j.Config.KeyRefreshRateLimit != nil && lastRefresh.Add(*j.Config.KeyRefreshRateLimit).After(time.Now()) { - // Don't make the JWT parsing goroutine wait for the JWKs to refresh. - cancel() - - // Only queue a refresh once. - queueOnce.Do(func() { - - // Launch a goroutine that will get a reservation for a JWKs refresh or fail to and immediately return. - go func() { - // Wait for the next time to refresh. - refreshMux.Lock() - wait := time.Until(lastRefresh.Add(*j.Config.KeyRefreshRateLimit)) - refreshMux.Unlock() - select { - case <-j.ctx.Done(): - return - case <-time.After(wait): - } - - // Refresh the JWKs. - refreshMux.Lock() - defer refreshMux.Unlock() - if err := j.refresh(); err != nil && j.Config.KeyRefreshErrorHandler != nil { - j.Config.KeyRefreshErrorHandler(j, err) - } else if err == nil && j.Config.KeyRefreshSuccessHandler != nil { - j.Config.KeyRefreshSuccessHandler(j) - } - - // Reset the last time for the refresh to now. - lastRefresh = time.Now() - - // Allow another queue. - queueOnce = sync.Once{} - }() - }) - } else { - // Refresh the JWKs. - if err := j.refresh(); err != nil && j.Config.KeyRefreshErrorHandler != nil { - j.Config.KeyRefreshErrorHandler(j, err) - } else if err == nil && j.Config.KeyRefreshSuccessHandler != nil { - j.Config.KeyRefreshSuccessHandler(j) - } - - // Reset the last time for the refresh to now. - lastRefresh = time.Now() - - // Allow the JWT parsing goroutine to continue with the refreshed JWKs. - cancel() - } - refreshMux.Unlock() - - // Clean up this goroutine when its context expires. - case <-j.ctx.Done(): - return - } - } -} - -// refresh does an HTTP GET on the JWKs URLs in parallel to rebuild the JWKs. -func (j *KeySet) refresh() (err error) { - // Create a context for the request. - var ctx context.Context - var cancel context.CancelFunc - if j.ctx != nil { - ctx, cancel = context.WithTimeout(j.ctx, *j.Config.KeyRefreshTimeout) - } else { - ctx, cancel = context.WithTimeout(context.Background(), *j.Config.KeyRefreshTimeout) - } - defer cancel() - - // Create the HTTP request. - var keys map[string]*rawJWK - for _, url := range j.Config.KeySetURLs { - var req *http.Request - if req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, bytes.NewReader(nil)); err != nil { - return err - } - - // Get the JWKs as JSON from the given URL. - var resp *http.Response - if resp, err = j.client.Do(req); err != nil { - return err - } - - // Read the raw JWKs from the body of the response. - var jwksBytes []byte - if jwksBytes, err = io.ReadAll(resp.Body); err != nil { - if cErr := resp.Body.Close(); cErr != nil { - log.Printf("error closing response body: %s", cErr.Error()) - } - return err - } - if cErr := resp.Body.Close(); cErr != nil { - log.Printf("error closing response body: %s", cErr.Error()) - } - - // Create an updated JWKs. - if urlKeys, urlErr := parseKeySet(jwksBytes); urlErr != nil { - return urlErr - } else if urlKeys != nil { - keys = mergemap(keys, urlKeys) - } - } - - // Lock the JWKs for async safe usage. - j.mux.Lock() - defer j.mux.Unlock() - - // Update the keys. - j.Keys = keys - - return nil -} - -// StopRefreshing ends the background goroutine to update the JWKs. It can only happen once and is only effective if the -// JWKs has a background goroutine refreshing the JWKs keys. -func (j *KeySet) StopRefreshing() { - if j.cancel != nil { - j.cancel() - } -} - -// creates a new map with values of origMap overwritten by those in newMap -func mergemap(origMap, newMap map[string]*rawJWK) map[string]*rawJWK { - var mp map[string]*rawJWK - if len(origMap) > 0 || len(newMap) > 0 { - mp = make(map[string]*rawJWK) - } - for k, v := range origMap { - mp[k] = v - } - for k, v := range newMap { - mp[k] = v - } - return mp -} diff --git a/jwt.go b/jwt.go index 5f014c7..5b3b4e3 100644 --- a/jwt.go +++ b/jwt.go @@ -2,34 +2,13 @@ package jwtware import ( "errors" - "fmt" "strings" "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v4" ) type jwtExtractor func(c *fiber.Ctx) (string, error) -// jwtKeyFunc returns a function that returns signing key for given token. -func jwtKeyFunc(config Config) jwt.Keyfunc { - return func(t *jwt.Token) (interface{}, error) { - // Check the signing method - if t.Method.Alg() != config.SigningMethod { - return nil, fmt.Errorf("Unexpected jwt signing method=%v", t.Header["alg"]) - } - if len(config.SigningKeys) > 0 { - if kid, ok := t.Header["kid"].(string); ok { - if key, ok := config.SigningKeys[kid]; ok { - return key, nil - } - } - return nil, fmt.Errorf("Unexpected jwt key id=%v", t.Header["kid"]) - } - return config.SigningKey, nil - } -} - // jwtFromHeader returns a function that extracts token from the request header. func jwtFromHeader(header string, authScheme string) func(c *fiber.Ctx) (string, error) { return func(c *fiber.Ctx) (string, error) { diff --git a/main.go b/main.go index 9c7c0fc..8a2af78 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,7 @@ import ( "time" "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" ) var ( diff --git a/main_test.go b/main_test.go index a012659..21e1aaa 100644 --- a/main_test.go +++ b/main_test.go @@ -8,10 +8,9 @@ import ( "path/filepath" "testing" - "github.com/golang-jwt/jwt/v4" - "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/utils" + "github.com/golang-jwt/jwt/v5" jwtware "github.com/gofiber/jwt/v3" ) @@ -216,7 +215,7 @@ func TestJwkFromServer(t *testing.T) { app := fiber.New() app.Use(jwtware.New(jwtware.Config{ - KeySetURL: server.URL + "/jwks.json", + JWKSetURLs: []string{server.URL + "/jwks.json"}, })) app.Get("/ok", func(c *fiber.Ctx) error { @@ -277,7 +276,7 @@ func TestJwkFromServers(t *testing.T) { app := fiber.New() app.Use(jwtware.New(jwtware.Config{ - KeySetURLs: []string{server.URL + "/jwks.json", server.URL + "/jwks2.json"}, + JWKSetURLs: []string{server.URL + "/jwks.json", server.URL + "/jwks2.json"}, })) app.Get("/ok", func(c *fiber.Ctx) error { @@ -296,7 +295,7 @@ func TestJwkFromServers(t *testing.T) { } } -func TestCustomKeyFunc(t *testing.T) { +func TestCustomKeyfunc(t *testing.T) { t.Parallel() defer func() { @@ -311,7 +310,7 @@ func TestCustomKeyFunc(t *testing.T) { app := fiber.New() app.Use(jwtware.New(jwtware.Config{ - KeyFunc: customKeyFunc(), + KeyFunc: customKeyfunc(), })) app.Get("/ok", func(c *fiber.Ctx) error { @@ -329,7 +328,7 @@ func TestCustomKeyFunc(t *testing.T) { utils.AssertEqual(t, 200, resp.StatusCode) } -func customKeyFunc() jwt.Keyfunc { +func customKeyfunc() jwt.Keyfunc { return func(t *jwt.Token) (interface{}, error) { // Always check the signing method if t.Method.Alg() != jwtware.HS256 { From 991a3f91c8925ca457314ad54c4418e9260c8ce8 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sun, 23 Apr 2023 21:51:54 -0400 Subject: [PATCH 02/18] Update go.mod --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index b4f6e6d..e8f2ad3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/gofiber/jwt/v3 +module github.com/gofiber/jwt/v4 -go 1.16 +go 1.18 require ( github.com/MicahParks/keyfunc/v2 v2.0.1 From b24bc363dc0c0c0950a62ea250df8f7a3b75cac9 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sun, 7 May 2023 15:38:16 -0400 Subject: [PATCH 03/18] Log when keyfunc background goroutine fails to refresh JWK Set --- config.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/config.go b/config.go index f397533..3ae1b62 100644 --- a/config.go +++ b/config.go @@ -2,6 +2,7 @@ package jwtware import ( "fmt" + "log" "strings" "time" @@ -179,8 +180,10 @@ func multiKeyfunc(givenKeys map[string]keyfunc.GivenKey, jwkSetURLs []string) (j func keyfuncOptions(givenKeys map[string]keyfunc.GivenKey) keyfunc.Options { return keyfunc.Options{ - // TODO Add error logger? - GivenKeys: givenKeys, + GivenKeys: givenKeys, + RefreshErrorHandler: func(err error) { + log.Printf("Failed to perform background refresh of JWK Set: %s.", err) + }, RefreshInterval: time.Hour, RefreshRateLimit: time.Minute * 5, RefreshTimeout: time.Second * 10, From 8b3a6c0ac64b0177a5d2aadf5e65f318ae9d4913 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Mon, 8 May 2023 09:05:52 -0400 Subject: [PATCH 04/18] Correct /v3 import to /v4 and run go mod tidy --- go.mod | 18 ++++++++++++++++++ go.sum | 10 ---------- main_test.go | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index e8f2ad3..8b925aa 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,21 @@ require ( github.com/gofiber/fiber/v2 v2.44.0 github.com/golang-jwt/jwt/v5 v5.0.0 ) + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/klauspost/compress v1.16.3 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect + github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect + github.com/tinylib/msgp v1.1.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.45.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/sys v0.7.0 // indirect +) diff --git a/go.sum b/go.sum index 7301f20..d9e058b 100644 --- a/go.sum +++ b/go.sum @@ -42,19 +42,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -68,27 +64,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main_test.go b/main_test.go index 21e1aaa..19154c1 100644 --- a/main_test.go +++ b/main_test.go @@ -12,7 +12,7 @@ import ( "github.com/gofiber/fiber/v2/utils" "github.com/golang-jwt/jwt/v5" - jwtware "github.com/gofiber/jwt/v3" + jwtware "github.com/gofiber/jwt/v4" ) type TestToken struct { From db9061871212132292289fc1d459eb9fc4fe62a5 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sat, 13 May 2023 19:37:26 -0400 Subject: [PATCH 05/18] Update the keyfunc patch version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8b925aa..7e35f7f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/gofiber/jwt/v4 go 1.18 require ( - github.com/MicahParks/keyfunc/v2 v2.0.1 + github.com/MicahParks/keyfunc/v2 v2.0.2 github.com/gofiber/fiber/v2 v2.44.0 github.com/golang-jwt/jwt/v5 v5.0.0 ) diff --git a/go.sum b/go.sum index d9e058b..16cbb1c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/MicahParks/keyfunc/v2 v2.0.1 h1:6FrNNvG/20gEKkjxV+5anrkq0VOF666G2zUn8lk8dgk= -github.com/MicahParks/keyfunc/v2 v2.0.1/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= +github.com/MicahParks/keyfunc/v2 v2.0.2 h1:3gpuVccOhHnJmqbz9VvKpDSYsnUHZdGJsXhmT9HGNgI= +github.com/MicahParks/keyfunc/v2 v2.0.2/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/gofiber/fiber/v2 v2.44.0 h1:Z90bEvPcJM5GFJnu1py0E1ojoerkyew3iiNJ78MQCM8= From f023b1cc12f19c0dd75fe0889db9a78bb48d66da Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sat, 13 May 2023 19:39:03 -0400 Subject: [PATCH 06/18] Change keyfunc creation to be less complex --- config.go | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/config.go b/config.go index 3ae1b62..2becc75 100644 --- a/config.go +++ b/config.go @@ -133,11 +133,7 @@ func makeCfg(config []Config) (cfg Config) { } if len(cfg.JWKSetURLs) > 0 { var err error - if len(cfg.JWKSetURLs) == 1 { - cfg.KeyFunc, err = oneKeyfunc(cfg, givenKeys) - } else { - cfg.KeyFunc, err = multiKeyfunc(givenKeys, cfg.JWKSetURLs) - } + cfg.KeyFunc, err = multiKeyfunc(givenKeys, cfg.JWKSetURLs) if err != nil { panic("Failed to create keyfunc from JWK Set URL: " + err.Error()) // TODO Don't panic? } @@ -154,14 +150,6 @@ func makeCfg(config []Config) (cfg Config) { return cfg } -func oneKeyfunc(cfg Config, givenKeys map[string]keyfunc.GivenKey) (jwt.Keyfunc, error) { - jwks, err := keyfunc.Get(cfg.JWKSetURLs[0], keyfuncOptions(givenKeys)) - if err != nil { - return nil, fmt.Errorf("failed to get JWK Set URL: %w", err) - } - return jwks.Keyfunc, nil -} - func multiKeyfunc(givenKeys map[string]keyfunc.GivenKey, jwkSetURLs []string) (jwt.Keyfunc, error) { opts := keyfuncOptions(givenKeys) multiple := make(map[string]keyfunc.Options, len(jwkSetURLs)) From 127cfddf251666db38526bf98fe34b5712ef72b8 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sat, 13 May 2023 19:39:20 -0400 Subject: [PATCH 07/18] Auto-format Markdown table --- README.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b6b7987..161ca3d 100644 --- a/README.md +++ b/README.md @@ -29,27 +29,27 @@ jwtware.New(config ...jwtware.Config) func(*fiber.Ctx) error ``` ### Config -| Property | Type | Description | Default | -|:-------------------------| :--- |:-----------------------------------------------------------------------------------------------------------------------------------------------------| :--- | -| Filter | `func(*fiber.Ctx) bool` | Defines a function to skip middleware | `nil` | -| SuccessHandler | `func(*fiber.Ctx) error` | SuccessHandler defines a function which is executed for a valid token. | `nil` | -| ErrorHandler | `func(*fiber.Ctx, error) error` | ErrorHandler defines a function which is executed for an invalid token. | `401 Invalid or expired JWT` | -| SigningKey | `interface{}` | Signing key to validate token. Used as fallback if SigningKeys has length 0. | `nil` | -| SigningKeys | `map[string]interface{}` | Map of signing keys to validate token with kid field usage. | `nil` | -| SigningMethod | `string` | Signing method, used to check token signing method. Possible values: `HS256`, `HS384`, `HS512`, `ES256`, `ES384`, `ES512`, `RS256`, `RS384`, `RS512` | `"HS256"` | -| ContextKey | `string` | Context key to store user information from the token into context. | `"user"` | -| Claims | `jwt.Claim` | Claims are extendable claims data defining token content. | `jwt.MapClaims{}` | -| TokenLookup | `string` | TokenLookup is a string in the form of `:` that is used | `"header:Authorization"` | -| AuthScheme | `string` | AuthScheme to be used in the Authorization header. The default value (`"Bearer"`) will only be used in conjuction with the default `TokenLookup` value. | `"Bearer"` | -| KeySetURL(deprecated) | `string` | KeySetURL location of JSON file with signing keys. | `""` | -| KeySetURLs | `string` | KeySetURL locations of JSON file with signing keys. | `""` | -| KeyRefreshSuccessHandler | `func(j *KeySet)` | KeyRefreshSuccessHandler defines a function which is executed for a valid refresh of signing keys. | `nil` | -| KeyRefreshErrorHandler | `func(j *KeySet, err error)` | KeyRefreshErrorHandler defines a function which is executed for an invalid refresh of signing keys. | `nil` | -| KeyRefreshInterval | `*time.Duration` | KeyRefreshInterval is the duration to refresh the JWKs in the background via a new HTTP request. | `nil` | -| KeyRefreshRateLimit | `*time.Duration` | KeyRefreshRateLimit limits the rate at which refresh requests are granted. | `nil` | -| KeyRefreshTimeout | `*time.Duration` | KeyRefreshTimeout is the duration for the context used to create the HTTP request for a refresh of the JWKs. | `1min` | -| KeyRefreshUnknownKID | `bool` | KeyRefreshUnknownKID indicates that the JWKs refresh request will occur every time a kid that isn't cached is seen. | `false` | -| KeyFunc | `func() jwt.Keyfunc` | KeyFunc defines a user-defined function that supplies the public key for a token validation. | `jwtKeyFunc` | +| Property | Type | Description | Default | +|:-------------------------|:--------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------| +| Filter | `func(*fiber.Ctx) bool` | Defines a function to skip middleware | `nil` | +| SuccessHandler | `func(*fiber.Ctx) error` | SuccessHandler defines a function which is executed for a valid token. | `nil` | +| ErrorHandler | `func(*fiber.Ctx, error) error` | ErrorHandler defines a function which is executed for an invalid token. | `401 Invalid or expired JWT` | +| SigningKey | `interface{}` | Signing key to validate token. Used as fallback if SigningKeys has length 0. | `nil` | +| SigningKeys | `map[string]interface{}` | Map of signing keys to validate token with kid field usage. | `nil` | +| SigningMethod | `string` | Signing method, used to check token signing method. Possible values: `HS256`, `HS384`, `HS512`, `ES256`, `ES384`, `ES512`, `RS256`, `RS384`, `RS512` | `"HS256"` | +| ContextKey | `string` | Context key to store user information from the token into context. | `"user"` | +| Claims | `jwt.Claim` | Claims are extendable claims data defining token content. | `jwt.MapClaims{}` | +| TokenLookup | `string` | TokenLookup is a string in the form of `:` that is used | `"header:Authorization"` | +| AuthScheme | `string` | AuthScheme to be used in the Authorization header. The default value (`"Bearer"`) will only be used in conjuction with the default `TokenLookup` value. | `"Bearer"` | +| KeySetURL(deprecated) | `string` | KeySetURL location of JSON file with signing keys. | `""` | +| KeySetURLs | `string` | KeySetURL locations of JSON file with signing keys. | `""` | +| KeyRefreshSuccessHandler | `func(j *KeySet)` | KeyRefreshSuccessHandler defines a function which is executed for a valid refresh of signing keys. | `nil` | +| KeyRefreshErrorHandler | `func(j *KeySet, err error)` | KeyRefreshErrorHandler defines a function which is executed for an invalid refresh of signing keys. | `nil` | +| KeyRefreshInterval | `*time.Duration` | KeyRefreshInterval is the duration to refresh the JWKs in the background via a new HTTP request. | `nil` | +| KeyRefreshRateLimit | `*time.Duration` | KeyRefreshRateLimit limits the rate at which refresh requests are granted. | `nil` | +| KeyRefreshTimeout | `*time.Duration` | KeyRefreshTimeout is the duration for the context used to create the HTTP request for a refresh of the JWKs. | `1min` | +| KeyRefreshUnknownKID | `bool` | KeyRefreshUnknownKID indicates that the JWKs refresh request will occur every time a kid that isn't cached is seen. | `false` | +| KeyFunc | `func() jwt.Keyfunc` | KeyFunc defines a user-defined function that supplies the public key for a token validation. | `jwtKeyFunc` | ### HS256 Example From 00ea1fd9ebd6c36a2e7ea6af1bc76a4a8f14446a Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sat, 13 May 2023 19:45:50 -0400 Subject: [PATCH 08/18] Update import path and config table in README.md --- README.md | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 161ca3d..04b91ab 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This middleware supports Fiber v1 & v2, install accordingly. ``` go get -u github.com/gofiber/fiber/v2 -go get -u github.com/gofiber/jwt/v3 +go get -u github.com/gofiber/jwt/v4 go get -u github.com/golang-jwt/jwt/v4 ``` @@ -29,27 +29,20 @@ jwtware.New(config ...jwtware.Config) func(*fiber.Ctx) error ``` ### Config -| Property | Type | Description | Default | -|:-------------------------|:--------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------| -| Filter | `func(*fiber.Ctx) bool` | Defines a function to skip middleware | `nil` | -| SuccessHandler | `func(*fiber.Ctx) error` | SuccessHandler defines a function which is executed for a valid token. | `nil` | -| ErrorHandler | `func(*fiber.Ctx, error) error` | ErrorHandler defines a function which is executed for an invalid token. | `401 Invalid or expired JWT` | -| SigningKey | `interface{}` | Signing key to validate token. Used as fallback if SigningKeys has length 0. | `nil` | -| SigningKeys | `map[string]interface{}` | Map of signing keys to validate token with kid field usage. | `nil` | -| SigningMethod | `string` | Signing method, used to check token signing method. Possible values: `HS256`, `HS384`, `HS512`, `ES256`, `ES384`, `ES512`, `RS256`, `RS384`, `RS512` | `"HS256"` | -| ContextKey | `string` | Context key to store user information from the token into context. | `"user"` | -| Claims | `jwt.Claim` | Claims are extendable claims data defining token content. | `jwt.MapClaims{}` | -| TokenLookup | `string` | TokenLookup is a string in the form of `:` that is used | `"header:Authorization"` | -| AuthScheme | `string` | AuthScheme to be used in the Authorization header. The default value (`"Bearer"`) will only be used in conjuction with the default `TokenLookup` value. | `"Bearer"` | -| KeySetURL(deprecated) | `string` | KeySetURL location of JSON file with signing keys. | `""` | -| KeySetURLs | `string` | KeySetURL locations of JSON file with signing keys. | `""` | -| KeyRefreshSuccessHandler | `func(j *KeySet)` | KeyRefreshSuccessHandler defines a function which is executed for a valid refresh of signing keys. | `nil` | -| KeyRefreshErrorHandler | `func(j *KeySet, err error)` | KeyRefreshErrorHandler defines a function which is executed for an invalid refresh of signing keys. | `nil` | -| KeyRefreshInterval | `*time.Duration` | KeyRefreshInterval is the duration to refresh the JWKs in the background via a new HTTP request. | `nil` | -| KeyRefreshRateLimit | `*time.Duration` | KeyRefreshRateLimit limits the rate at which refresh requests are granted. | `nil` | -| KeyRefreshTimeout | `*time.Duration` | KeyRefreshTimeout is the duration for the context used to create the HTTP request for a refresh of the JWKs. | `1min` | -| KeyRefreshUnknownKID | `bool` | KeyRefreshUnknownKID indicates that the JWKs refresh request will occur every time a kid that isn't cached is seen. | `false` | -| KeyFunc | `func() jwt.Keyfunc` | KeyFunc defines a user-defined function that supplies the public key for a token validation. | `jwtKeyFunc` | +| Property | Type | Description | Default | +|:---------------|:--------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------| +| Filter | `func(*fiber.Ctx) bool` | Defines a function to skip middleware | `nil` | +| SuccessHandler | `func(*fiber.Ctx) error` | SuccessHandler defines a function which is executed for a valid token. | `nil` | +| ErrorHandler | `func(*fiber.Ctx, error) error` | ErrorHandler defines a function which is executed for an invalid token. | `401 Invalid or expired JWT` | +| SigningKey | `interface{}` | Signing key to validate token. Used as fallback if SigningKeys has length 0. | `nil` | +| SigningKeys | `map[string]interface{}` | Map of signing keys to validate token with kid field usage. | `nil` | +| SigningMethod | `string` | Signing method, used to check token signing method. Possible values: `HS256`, `HS384`, `HS512`, `ES256`, `ES384`, `ES512`, `RS256`, `RS384`, `RS512` | `"HS256"` | +| ContextKey | `string` | Context key to store user information from the token into context. | `"user"` | +| Claims | `jwt.Claim` | Claims are extendable claims data defining token content. | `jwt.MapClaims{}` | +| TokenLookup | `string` | TokenLookup is a string in the form of `:` that is used | `"header:Authorization"` | +| AuthScheme | `string` | AuthScheme to be used in the Authorization header. The default value (`"Bearer"`) will only be used in conjuction with the default `TokenLookup` value. | `"Bearer"` | +| KeyFunc | `func() jwt.Keyfunc` | KeyFunc defines a user-defined function that supplies the public key for a token validation. | `jwtKeyFunc` | +| JWKSetURLs | `[]string` | A slice of unique JSON Web Key (JWK) Set URLs to used to parse JWTs. | `nil` | ### HS256 Example @@ -61,7 +54,7 @@ import ( "github.com/gofiber/fiber/v2" - jwtware "github.com/gofiber/jwt/v3" + jwtware "github.com/gofiber/jwt/v4" "github.com/golang-jwt/jwt/v4" ) @@ -160,7 +153,7 @@ import ( "github.com/gofiber/fiber/v2" - jwtware "github.com/gofiber/jwt/v3" + jwtware "github.com/gofiber/jwt/v4" "github.com/golang-jwt/jwt/v4" ) @@ -267,7 +260,7 @@ import ( "fmt" "github.com/gofiber/fiber/v2" - jwtware "github.com/gofiber/jwt/v3" + jwtware "github.com/gofiber/jwt/v4" "github.com/golang-jwt/jwt/v4" ) From 5115c4e8694c71128dcb41dc2f2a7f75c4711c57 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sat, 13 May 2023 19:49:21 -0400 Subject: [PATCH 09/18] Update testing note in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 04b91ab..bd75e75 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ func restricted(c *fiber.Ctx) error { The RS256 is actually identical to the HS256 test above. ### JWKs Test -The tests are identical to basic `JWT` tests above, with exception that `KeySetURL`(deprecated) or `KeySetUrls` to valid public keys collection in JSON format should be supplied. +The tests are identical to basic `JWT` tests above, with exception that `JWKSetURLs` to valid public keys collection in JSON Web Key format should be supplied. See [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517). ### Custom KeyFunc example From 9b226ca61193e0c75d679e6a0d0fa3df67cb1691 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sat, 13 May 2023 19:50:38 -0400 Subject: [PATCH 10/18] Update title of JWK Set test in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd75e75..5d3f36d 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ func restricted(c *fiber.Ctx) error { ### RS256 Test The RS256 is actually identical to the HS256 test above. -### JWKs Test +### JWK Set Test The tests are identical to basic `JWT` tests above, with exception that `JWKSetURLs` to valid public keys collection in JSON Web Key format should be supplied. See [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517). ### Custom KeyFunc example From 17f68b13fdeef9e70605a8aee30d45c3e765f2a0 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sat, 13 May 2023 19:51:17 -0400 Subject: [PATCH 11/18] Specify JWK acronym in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d3f36d..1e6a253 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ func restricted(c *fiber.Ctx) error { The RS256 is actually identical to the HS256 test above. ### JWK Set Test -The tests are identical to basic `JWT` tests above, with exception that `JWKSetURLs` to valid public keys collection in JSON Web Key format should be supplied. See [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517). +The tests are identical to basic `JWT` tests above, with exception that `JWKSetURLs` to valid public keys collection in JSON Web Key (JWK) Set format should be supplied. See [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517). ### Custom KeyFunc example From 4ae73953931886c8dc0e556cea53409b1d0ef8f6 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sat, 13 May 2023 19:53:48 -0400 Subject: [PATCH 12/18] Update import paths for golang-jwt in README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1e6a253..0c6e83a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This middleware supports Fiber v1 & v2, install accordingly. ``` go get -u github.com/gofiber/fiber/v2 go get -u github.com/gofiber/jwt/v4 -go get -u github.com/golang-jwt/jwt/v4 +go get -u github.com/golang-jwt/jwt/v5 ``` ### Signature @@ -55,7 +55,7 @@ import ( "github.com/gofiber/fiber/v2" jwtware "github.com/gofiber/jwt/v4" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" ) func main() { @@ -154,7 +154,7 @@ import ( "github.com/gofiber/fiber/v2" jwtware "github.com/gofiber/jwt/v4" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" ) var ( @@ -261,7 +261,7 @@ import ( "github.com/gofiber/fiber/v2" jwtware "github.com/gofiber/jwt/v4" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" ) func main() { From 3cbd1ea8e9ef4fbf599a62e7c5720a908ada751c Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sat, 13 May 2023 20:00:18 -0400 Subject: [PATCH 13/18] Redo version bump from merge conflict --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 7e35f7f..17086f9 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/MicahParks/keyfunc/v2 v2.0.2 - github.com/gofiber/fiber/v2 v2.44.0 + github.com/gofiber/fiber/v2 v2.45.0 github.com/golang-jwt/jwt/v5 v5.0.0 ) @@ -21,7 +21,7 @@ require ( github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.45.0 // indirect + github.com/valyala/fasthttp v1.47.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/sys v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index 16cbb1c..a739adb 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/MicahParks/keyfunc/v2 v2.0.2 h1:3gpuVccOhHnJmqbz9VvKpDSYsnUHZdGJsXhmT github.com/MicahParks/keyfunc/v2 v2.0.2/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/gofiber/fiber/v2 v2.44.0 h1:Z90bEvPcJM5GFJnu1py0E1ojoerkyew3iiNJ78MQCM8= -github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w= +github.com/gofiber/fiber/v2 v2.45.0 h1:p4RpkJT9GAW6parBSbcNFH2ApnAuW3OzaQzbOCoDu+s= +github.com/gofiber/fiber/v2 v2.45.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -32,8 +32,8 @@ github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.45.0 h1:zPkkzpIn8tdHZUrVa6PzYd0i5verqiPSkgTd3bSUcpA= -github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c= +github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -65,8 +65,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= From c1909431b78db3d95f4c4ed18e778d78c74dbd8f Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sat, 13 May 2023 20:05:00 -0400 Subject: [PATCH 14/18] Update Go version in GitHub Actions --- .github/workflows/gotidy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gotidy.yml b/.github/workflows/gotidy.yml index e5e7ae5..b4c9e99 100644 --- a/.github/workflows/gotidy.yml +++ b/.github/workflows/gotidy.yml @@ -20,7 +20,7 @@ jobs: name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.17 + go-version: 1.18 - name: Tidy run: | From 363e7ee632d29f2116a78ced78c5707f977cd181 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Sat, 13 May 2023 20:06:19 -0400 Subject: [PATCH 15/18] Remove 1.17 Go version from GitHub Actions --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca10d5b..0cd78c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,6 @@ jobs: strategy: matrix: go-version: - - 1.17.x - 1.18.x - 1.20.x platform: From 16b38114a434e3e2651690216e5c22a2d9f26eb3 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Wed, 17 May 2023 18:50:16 -0400 Subject: [PATCH 16/18] Remove review TODOs --- config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.go b/config.go index 2becc75..929abcd 100644 --- a/config.go +++ b/config.go @@ -67,7 +67,7 @@ type Config struct { // you can provide a jwt.Keyfunc using that package or make your own implementation. // // This option is mutually exclusive with and takes precedence over JWKSetURLs, SigningKeys, and SigningKey. - KeyFunc jwt.Keyfunc // TODO Could be renamed to Keyfunc + KeyFunc jwt.Keyfunc // JWKSetURLs is a slice of HTTP URLs that contain the JSON Web Key Set (JWKS) used to verify the signatures of // JWTs. Use of HTTPS is recommended. The presence of the "kid" field in the JWT header and JWKs is mandatory for @@ -135,7 +135,7 @@ func makeCfg(config []Config) (cfg Config) { var err error cfg.KeyFunc, err = multiKeyfunc(givenKeys, cfg.JWKSetURLs) if err != nil { - panic("Failed to create keyfunc from JWK Set URL: " + err.Error()) // TODO Don't panic? + panic("Failed to create keyfunc from JWK Set URL: " + err.Error()) } } else { cfg.KeyFunc = keyfunc.NewGiven(givenKeys).Keyfunc From fbe2883960301bca99cb6d1d16c717163edcb756 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Wed, 17 May 2023 19:29:51 -0400 Subject: [PATCH 17/18] Change how users can specify signing algorithms --- README.md | 13 +++++----- config.go | 69 +++++++++++++++++++++++++++++++++++--------------- config_test.go | 9 +++---- main_test.go | 14 ++++++---- 4 files changed, 68 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 0c6e83a..48f4675 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ jwtware.New(config ...jwtware.Config) func(*fiber.Ctx) error | ErrorHandler | `func(*fiber.Ctx, error) error` | ErrorHandler defines a function which is executed for an invalid token. | `401 Invalid or expired JWT` | | SigningKey | `interface{}` | Signing key to validate token. Used as fallback if SigningKeys has length 0. | `nil` | | SigningKeys | `map[string]interface{}` | Map of signing keys to validate token with kid field usage. | `nil` | -| SigningMethod | `string` | Signing method, used to check token signing method. Possible values: `HS256`, `HS384`, `HS512`, `ES256`, `ES384`, `ES512`, `RS256`, `RS384`, `RS512` | `"HS256"` | | ContextKey | `string` | Context key to store user information from the token into context. | `"user"` | | Claims | `jwt.Claim` | Claims are extendable claims data defining token content. | `jwt.MapClaims{}` | | TokenLookup | `string` | TokenLookup is a string in the form of `:` that is used | `"header:Authorization"` | @@ -69,7 +68,7 @@ func main() { // JWT Middleware app.Use(jwtware.New(jwtware.Config{ - SigningKey: []byte("secret"), + SigningKey: SigningKey{Key: []byte("secret")}, })) // Restricted Routes @@ -153,8 +152,9 @@ import ( "github.com/gofiber/fiber/v2" - jwtware "github.com/gofiber/jwt/v4" "github.com/golang-jwt/jwt/v5" + + jwtware "github.com/gofiber/jwt/v4" ) var ( @@ -183,8 +183,10 @@ func main() { // JWT Middleware app.Use(jwtware.New(jwtware.Config{ - SigningMethod: "RS256", - SigningKey: privateKey.Public(), + SigningKey: jwtware.SigningKey{ + JWTAlg: jwtware.RS256, + Key: privateKey.Public(), + }, })) // Restricted Routes @@ -232,7 +234,6 @@ func restricted(c *fiber.Ctx) error { name := claims["name"].(string) return c.SendString("Welcome " + name) } - ``` ### RS256 Test diff --git a/config.go b/config.go index 929abcd..20238f6 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,7 @@ package jwtware import ( + "errors" "fmt" "log" "strings" @@ -11,6 +12,11 @@ import ( "github.com/golang-jwt/jwt/v5" ) +var ( + // ErrJWTAlg is returned when the JWT header did not contain the expected algorithm. + ErrJWTAlg = errors.New("the JWT header did not contain the expected algorithm") +) + // Config defines the config for JWT middleware type Config struct { // Filter defines a function to skip middleware. @@ -27,17 +33,14 @@ type Config struct { ErrorHandler fiber.ErrorHandler // Signing key to validate token. Used as fallback if SigningKeys has length 0. - // Required. This, SigningKeys or KeySetUrl. - SigningKey interface{} + // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. + // The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey. + SigningKey SigningKey // Map of signing keys to validate token with kid field usage. - // Required. This, SigningKey or KeySetUrl(deprecated) or KeySetUrls. - SigningKeys map[string]interface{} - - // Signing method, used to check token signing method. - // Optional. Default: "HS256". - // Possible values: "HS256", "HS384", "HS512", "ES256", "ES384", "ES512", "RS256", "RS384", "RS512" - SigningMethod string + // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. + // The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey. + SigningKeys map[string]SigningKey // Context key to store user information from the token into context. // Optional. Default: "user". @@ -66,7 +69,8 @@ type Config struct { // Internally, github.com/MicahParks/keyfunc/v2 package is used project defaults. If you need more customization, // you can provide a jwt.Keyfunc using that package or make your own implementation. // - // This option is mutually exclusive with and takes precedence over JWKSetURLs, SigningKeys, and SigningKey. + // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. + // The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey. KeyFunc jwt.Keyfunc // JWKSetURLs is a slice of HTTP URLs that contain the JSON Web Key Set (JWKS) used to verify the signatures of @@ -79,10 +83,23 @@ type Config struct { // * Rate limit refreshes to once every 5 minutes. // * Timeout refreshes after 10 seconds. // - // This field is compatible with the SigningKeys field. + // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. + // The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey. JWKSetURLs []string } +// SigningKey holds information about the recognized cryptographic keys used to sign JWTs by this program. +type SigningKey struct { + // JWTAlg is the algorithm used to sign JWTs. If this value is a non-empty string, this will be checked against the + // "alg" value in the JWT header. + // + // https://www.rfc-editor.org/rfc/rfc7518#section-3.1 + JWTAlg string + // Key is the cryptographic key used to sign JWTs. For supported types, please see + // https://github.com/golang-jwt/jwt. + Key interface{} +} + // makeCfg function will check correctness of supplied configuration // and will complement it with default values instead of missing ones func makeCfg(config []Config) (cfg Config) { @@ -102,11 +119,8 @@ func makeCfg(config []Config) (cfg Config) { return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired JWT") } } - if cfg.SigningKey == nil && len(cfg.SigningKeys) == 0 && len(cfg.JWKSetURLs) == 0 && cfg.KeyFunc == nil { - panic("Fiber: JWT middleware requires at least one signing key or JWK Set URL") - } - if cfg.SigningMethod == "" && len(cfg.JWKSetURLs) == 0 { - cfg.SigningMethod = "HS256" + if cfg.SigningKey.Key == nil && len(cfg.SigningKeys) == 0 && len(cfg.JWKSetURLs) == 0 && cfg.KeyFunc == nil { + panic("Fiber: JWT middleware configuration: At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.") } if cfg.ContextKey == "" { cfg.ContextKey = "user" @@ -128,7 +142,9 @@ func makeCfg(config []Config) (cfg Config) { if cfg.SigningKeys != nil { givenKeys = make(map[string]keyfunc.GivenKey, len(cfg.SigningKeys)) for kid, key := range cfg.SigningKeys { - givenKeys[kid] = keyfunc.NewGivenCustom(key, keyfunc.GivenKeyOptions{}) // TODO User supplied alg? + givenKeys[kid] = keyfunc.NewGivenCustom(key, keyfunc.GivenKeyOptions{ + Algorithm: key.JWTAlg, + }) } } if len(cfg.JWKSetURLs) > 0 { @@ -141,9 +157,7 @@ func makeCfg(config []Config) (cfg Config) { cfg.KeyFunc = keyfunc.NewGiven(givenKeys).Keyfunc } } else { - cfg.KeyFunc = func(token *jwt.Token) (interface{}, error) { - return cfg.SigningKey, nil - } + cfg.KeyFunc = signingKeyFunc(cfg.SigningKey) } } @@ -201,3 +215,18 @@ func (cfg *Config) getExtractors() []jwtExtractor { } return extractors } + +func signingKeyFunc(key SigningKey) jwt.Keyfunc { + return func(token *jwt.Token) (interface{}, error) { + if key.JWTAlg != "" { + alg, ok := token.Header["alg"].(string) + if !ok { + return nil, fmt.Errorf("unexpected jwt signing method: expected: %q: got: missing or unexpected JSON type", key.JWTAlg) + } + if alg != key.JWTAlg { + return nil, fmt.Errorf("unexpected jwt signing method: expected: %q: got: %q", key.JWTAlg, alg) + } + } + return key.Key, nil + } +} diff --git a/config_test.go b/config_test.go index 08681a7..78cb829 100644 --- a/config_test.go +++ b/config_test.go @@ -33,16 +33,13 @@ func TestDefaultConfiguration(t *testing.T) { // Arrange config := append(make([]Config, 0), Config{ - SigningKey: "", + SigningKey: SigningKey{Key: []byte("")}, }) // Act cfg := makeCfg(config) // Assert - if cfg.SigningMethod != HS256 { - t.Fatalf("Default signing method should be 'HS256'") - } if cfg.ContextKey != "user" { t.Fatalf("Default context key should be 'user'") } @@ -70,7 +67,7 @@ func TestExtractorsInitialization(t *testing.T) { // Arrange cfg := Config{ - SigningKey: "", + SigningKey: SigningKey{Key: []byte("")}, TokenLookup: defaultTokenLookup + ",query:token,param:token,cookie:token,something:something", } @@ -100,7 +97,7 @@ func TestCustomTokenLookup(t *testing.T) { lookup := `header:X-Auth` scheme := "Token" cfg := Config{ - SigningKey: "", + SigningKey: SigningKey{Key: []byte("")}, TokenLookup: lookup, AuthScheme: scheme, } diff --git a/main_test.go b/main_test.go index 19154c1..0da5d9e 100644 --- a/main_test.go +++ b/main_test.go @@ -119,8 +119,10 @@ func TestJwtFromHeader(t *testing.T) { app := fiber.New() app.Use(jwtware.New(jwtware.Config{ - SigningKey: []byte(defaultSigningKey), - SigningMethod: test.SigningMethod, + SigningKey: jwtware.SigningKey{ + JWTAlg: test.SigningMethod, + Key: []byte(defaultSigningKey), + }, })) app.Get("/ok", func(c *fiber.Ctx) error { @@ -154,9 +156,11 @@ func TestJwtFromCookie(t *testing.T) { app := fiber.New() app.Use(jwtware.New(jwtware.Config{ - SigningKey: []byte(defaultSigningKey), - SigningMethod: test.SigningMethod, - TokenLookup: "cookie:Token", + SigningKey: jwtware.SigningKey{ + JWTAlg: test.SigningMethod, + Key: []byte(defaultSigningKey), + }, + TokenLookup: "cookie:Token", })) app.Get("/ok", func(c *fiber.Ctx) error { From d355447060367c2777296c0f208ac2392cc6a6a7 Mon Sep 17 00:00:00 2001 From: Micah Parks Date: Wed, 17 May 2023 19:34:45 -0400 Subject: [PATCH 18/18] Remove unused global and export reused error --- jwt.go | 13 +++++++++---- main.go | 5 ----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/jwt.go b/jwt.go index 5b3b4e3..c44213a 100644 --- a/jwt.go +++ b/jwt.go @@ -7,6 +7,11 @@ import ( "github.com/gofiber/fiber/v2" ) +var ( + // ErrJWTMissingOrMalformed is returned when the JWT is missing or malformed. + ErrJWTMissingOrMalformed = errors.New("missing or malformed JWT") +) + type jwtExtractor func(c *fiber.Ctx) (string, error) // jwtFromHeader returns a function that extracts token from the request header. @@ -17,7 +22,7 @@ func jwtFromHeader(header string, authScheme string) func(c *fiber.Ctx) (string, if len(auth) > l+1 && strings.EqualFold(auth[:l], authScheme) { return strings.TrimSpace(auth[l:]), nil } - return "", errors.New("Missing or malformed JWT") + return "", ErrJWTMissingOrMalformed } } @@ -26,7 +31,7 @@ func jwtFromQuery(param string) func(c *fiber.Ctx) (string, error) { return func(c *fiber.Ctx) (string, error) { token := c.Query(param) if token == "" { - return "", errors.New("Missing or malformed JWT") + return "", ErrJWTMissingOrMalformed } return token, nil } @@ -37,7 +42,7 @@ func jwtFromParam(param string) func(c *fiber.Ctx) (string, error) { return func(c *fiber.Ctx) (string, error) { token := c.Params(param) if token == "" { - return "", errors.New("Missing or malformed JWT") + return "", ErrJWTMissingOrMalformed } return token, nil } @@ -48,7 +53,7 @@ func jwtFromCookie(name string) func(c *fiber.Ctx) (string, error) { return func(c *fiber.Ctx) (string, error) { token := c.Cookies(name) if token == "" { - return "", errors.New("Missing or malformed JWT") + return "", ErrJWTMissingOrMalformed } return token, nil } diff --git a/main.go b/main.go index 8a2af78..9413970 100644 --- a/main.go +++ b/main.go @@ -7,17 +7,12 @@ package jwtware import ( "reflect" - "time" "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v5" ) var ( - // defaultRefreshTimeout is the default duration for the context used to create the HTTP request for a refresh of - // the JWKs. - defaultKeyRefreshTimeout = time.Minute - defaultTokenLookup = "header:" + fiber.HeaderAuthorization )