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: | 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: diff --git a/README.md b/README.md index b6b7987..48f4675 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ 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/golang-jwt/jwt/v4 +go get -u github.com/gofiber/jwt/v4 +go get -u github.com/golang-jwt/jwt/v5 ``` ### Signature @@ -29,27 +29,19 @@ 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` | +| 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,8 +53,8 @@ import ( "github.com/gofiber/fiber/v2" - jwtware "github.com/gofiber/jwt/v3" - "github.com/golang-jwt/jwt/v4" + jwtware "github.com/gofiber/jwt/v4" + "github.com/golang-jwt/jwt/v5" ) func main() { @@ -76,7 +68,7 @@ func main() { // JWT Middleware app.Use(jwtware.New(jwtware.Config{ - SigningKey: []byte("secret"), + SigningKey: SigningKey{Key: []byte("secret")}, })) // Restricted Routes @@ -160,8 +152,9 @@ import ( "github.com/gofiber/fiber/v2" - jwtware "github.com/gofiber/jwt/v3" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" + + jwtware "github.com/gofiber/jwt/v4" ) var ( @@ -190,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 @@ -239,14 +234,13 @@ func restricted(c *fiber.Ctx) error { name := claims["name"].(string) return c.SendString("Welcome " + name) } - ``` ### RS256 Test 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. +### 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 (JWK) Set format should be supplied. See [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517). ### Custom KeyFunc example @@ -267,8 +261,8 @@ import ( "fmt" "github.com/gofiber/fiber/v2" - jwtware "github.com/gofiber/jwt/v3" - "github.com/golang-jwt/jwt/v4" + jwtware "github.com/gofiber/jwt/v4" + "github.com/golang-jwt/jwt/v5" ) func main() { diff --git a/config.go b/config.go index d4156fa..20238f6 100644 --- a/config.go +++ b/config.go @@ -1,20 +1,21 @@ package jwtware import ( + "errors" + "fmt" + "log" "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) +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 { @@ -32,58 +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{} - - // 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" - 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". @@ -107,16 +64,40 @@ 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. // - // 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. + // 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 + // 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. + // + // 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 @@ -138,14 +119,8 @@ 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.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 { - 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" @@ -160,23 +135,64 @@ 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{ + Algorithm: key.JWTAlg, + }) + } + } + if len(cfg.JWKSetURLs) > 0 { + var err error + cfg.KeyFunc, err = multiKeyfunc(givenKeys, cfg.JWKSetURLs) + if err != nil { + panic("Failed to create keyfunc from JWK Set URL: " + err.Error()) + } + } else { + cfg.KeyFunc = keyfunc.NewGiven(givenKeys).Keyfunc } - cfg.KeyFunc = jwks.keyFunc() } else { - cfg.KeyFunc = jwtKeyFunc(cfg) + cfg.KeyFunc = signingKeyFunc(cfg.SigningKey) } } + return cfg } +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{ + 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, + 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 { @@ -199,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/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 895316c..17086f9 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,27 @@ -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.2 github.com/gofiber/fiber/v2 v2.45.0 - github.com/golang-jwt/jwt/v4 v4.5.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.47.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/sys v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index 6d00f9c..a739adb 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,11 @@ +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.45.0 h1:p4RpkJT9GAW6parBSbcNFH2ApnAuW3OzaQzbOCoDu+s= github.com/gofiber/fiber/v2 v2.45.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc= -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= @@ -40,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= @@ -66,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.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= -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/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..c44213a 100644 --- a/jwt.go +++ b/jwt.go @@ -2,33 +2,17 @@ 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) +var ( + // ErrJWTMissingOrMalformed is returned when the JWT is missing or malformed. + ErrJWTMissingOrMalformed = errors.New("missing or malformed JWT") +) -// 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 - } -} +type jwtExtractor func(c *fiber.Ctx) (string, error) // jwtFromHeader returns a function that extracts token from the request header. func jwtFromHeader(header string, authScheme string) func(c *fiber.Ctx) (string, error) { @@ -38,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 } } @@ -47,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 } @@ -58,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 } @@ -69,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 9c7c0fc..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/v4" + "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 ) diff --git a/main_test.go b/main_test.go index a012659..0da5d9e 100644 --- a/main_test.go +++ b/main_test.go @@ -8,12 +8,11 @@ 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" + jwtware "github.com/gofiber/jwt/v4" ) type TestToken struct { @@ -120,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 { @@ -155,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 { @@ -216,7 +219,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 +280,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 +299,7 @@ func TestJwkFromServers(t *testing.T) { } } -func TestCustomKeyFunc(t *testing.T) { +func TestCustomKeyfunc(t *testing.T) { t.Parallel() defer func() { @@ -311,7 +314,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 +332,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 {