diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27974bbb..032eb767 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - go: ["1.19", "1.20", "1.21"] + go: ["1.21", "1.22", "1.23"] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f8bf5428..cec3b92b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,10 +14,10 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: "1.21" + go-version: "1.23" check-latest: true - name: golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v6 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version version: latest diff --git a/README.md b/README.md index 964598a3..0bb636f2 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ implementation of [JSON Web Tokens](https://datatracker.ietf.org/doc/html/rfc7519). Starting with [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0) -this project adds Go module support, but maintains backwards compatibility with +this project adds Go module support, but maintains backward compatibility with older `v3.x.y` tags and upstream `github.com/dgrijalva/jwt-go`. See the [`MIGRATION_GUIDE.md`](./MIGRATION_GUIDE.md) for more information. Version v5.0.0 introduces major improvements to the validation of tokens, but is not -entirely backwards compatible. +entirely backward compatible. > After the original author of the library suggested migrating the maintenance > of `jwt-go`, a dedicated team of open source maintainers decided to clone the @@ -24,7 +24,7 @@ entirely backwards compatible. **SECURITY NOTICE:** Some older versions of Go have a security issue in the -crypto/elliptic. Recommendation is to upgrade to at least 1.15 See issue +crypto/elliptic. The recommendation is to upgrade to at least 1.15 See issue [dgrijalva/jwt-go#216](https://github.com/dgrijalva/jwt-go/issues/216) for more detail. @@ -32,7 +32,7 @@ detail. what you expect](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). This library attempts to make it easy to do the right thing by requiring key -types match the expected alg, but you should take the extra step to verify it in +types to match the expected alg, but you should take the extra step to verify it in your usage. See the examples provided. ### Supported Go versions @@ -41,7 +41,7 @@ Our support of Go versions is aligned with Go's [version release policy](https://golang.org/doc/devel/release#policy). So we will support a major version of Go until there are two newer major releases. We no longer support building jwt-go with unsupported Go versions, as these contain security -vulnerabilities which will not be fixed. +vulnerabilities that will not be fixed. ## What the heck is a JWT? @@ -117,7 +117,7 @@ notable differences: This library is considered production ready. Feedback and feature requests are appreciated. The API should be considered stable. There should be very few -backwards-incompatible changes outside of major version updates (and only with +backward-incompatible changes outside of major version updates (and only with good reason). This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull @@ -125,8 +125,8 @@ requests will land on `main`. Periodically, versions will be tagged from `main`. You can find all the releases on [the project releases page](https://github.com/golang-jwt/jwt/releases). -**BREAKING CHANGES:*** A full list of breaking changes is available in -`VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating +**BREAKING CHANGES:** A full list of breaking changes is available in +`VERSION_HISTORY.md`. See [`MIGRATION_GUIDE.md`](./MIGRATION_GUIDE.md) for more information on updating your code. ## Extensions diff --git a/SECURITY.md b/SECURITY.md index b08402c3..2740597f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,11 +2,11 @@ ## Supported Versions -As of February 2022 (and until this document is updated), the latest version `v4` is supported. +As of November 2024 (and until this document is updated), the latest version `v5` is supported. In critical cases, we might supply back-ported patches for `v4`. ## Reporting a Vulnerability -If you think you found a vulnerability, and even if you are not sure, please report it to jwt-go-security@googlegroups.com or one of the other [golang-jwt maintainers](https://github.com/orgs/golang-jwt/people). Please try be explicit, describe steps to reproduce the security issue with code example(s). +If you think you found a vulnerability, and even if you are not sure, please report it a [GitHub Security Advisory](https://github.com/golang-jwt/jwt/security/advisories/new). Please try be explicit, describe steps to reproduce the security issue with code example(s). You will receive a response within a timely manner. If the issue is confirmed, we will do our best to release a patch as soon as possible given the complexity of the problem. diff --git a/cmd/jwt/main.go b/cmd/jwt/main.go index 37b4fccf..22031ca2 100644 --- a/cmd/jwt/main.go +++ b/cmd/jwt/main.go @@ -30,9 +30,9 @@ var ( flagHead = make(ArgList) // Modes - exactly one of these is required - flagSign = flag.String("sign", "", "path to claims object to sign, '-' to read from stdin, or '+' to use only -claim args") - flagVerify = flag.String("verify", "", "path to JWT token to verify or '-' to read from stdin") - flagShow = flag.String("show", "", "path to JWT file or '-' to read from stdin") + flagSign = flag.String("sign", "", "path to claims file to sign, '-' to read from stdin, or '+' to use only -claim args") + flagVerify = flag.String("verify", "", "path to JWT token file to verify or '-' to read from stdin") + flagShow = flag.String("show", "", "path to JWT token file to show without verification or '-' to read from stdin") ) func main() { @@ -43,7 +43,7 @@ func main() { // Usage message if you ask for -help or if you mess up inputs. flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " One of the following flags is required: sign, verify\n") + fmt.Fprintf(os.Stderr, " One of the following flags is required: sign, verify or show\n") flag.PrintDefaults() } @@ -69,7 +69,7 @@ func start() error { return showToken() default: flag.Usage() - return fmt.Errorf("none of the required flags are present. What do you want me to do?") + return fmt.Errorf("none of the required flags are present. What do you want me to do?") } } @@ -273,7 +273,7 @@ func showToken() error { fmt.Fprintf(os.Stderr, "Token len: %v bytes\n", len(tokData)) } - token, err := jwt.Parse(string(tokData), nil) + token, _, err := jwt.NewParser().ParseUnverified(string(tokData), make(jwt.MapClaims)) if err != nil { return fmt.Errorf("malformed token: %w", err) } diff --git a/hmac_example_test.go b/hmac_example_test.go index 1b1edf46..f8f8c26b 100644 --- a/hmac_example_test.go +++ b/hmac_example_test.go @@ -49,14 +49,9 @@ func ExampleParse_hmac() { // head of the token to identify which key to use, but the parsed token (head and claims) is provided // to the callback, providing flexibility. token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - // Don't forget to validate the alg is what you expect: - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) - } - // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") return hmacSampleSecret, nil - }) + }, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})) if err != nil { log.Fatal(err) } diff --git a/jwt_test.go b/jwt_test.go new file mode 100644 index 00000000..b01e899d --- /dev/null +++ b/jwt_test.go @@ -0,0 +1,89 @@ +package jwt + +import ( + "testing" +) + +func TestSplitToken(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expected []string + isValid bool + }{ + { + name: "valid token with three parts", + input: "header.claims.signature", + expected: []string{"header", "claims", "signature"}, + isValid: true, + }, + { + name: "invalid token with two parts only", + input: "header.claims", + expected: nil, + isValid: false, + }, + { + name: "invalid token with one part only", + input: "header", + expected: nil, + isValid: false, + }, + { + name: "invalid token with extra delimiter", + input: "header.claims.signature.extra", + expected: nil, + isValid: false, + }, + { + name: "invalid empty token", + input: "", + expected: nil, + isValid: false, + }, + { + name: "valid token with empty parts", + input: "..signature", + expected: []string{"", "", "signature"}, + isValid: true, + }, + { + // We are just splitting the token into parts, so we don't care about the actual values. + // It is up to the caller to validate the parts. + name: "valid token with all parts empty", + input: "..", + expected: []string{"", "", ""}, + isValid: true, + }, + { + name: "invalid token with just delimiters and extra part", + input: "...", + expected: nil, + isValid: false, + }, + { + name: "invalid token with many delimiters", + input: "header.claims.signature..................", + expected: nil, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parts, ok := splitToken(tt.input) + if ok != tt.isValid { + t.Errorf("expected %t, got %t", tt.isValid, ok) + } + if ok { + for i, part := range tt.expected { + if parts[i] != part { + t.Errorf("expected %s, got %s", part, parts[i]) + } + } + } + }) + } +} diff --git a/parser.go b/parser.go index ecf99af7..054c7eb6 100644 --- a/parser.go +++ b/parser.go @@ -8,6 +8,8 @@ import ( "strings" ) +const tokenDelimiter = "." + type Parser struct { // If populated, only these methods will be considered valid. validMethods []string @@ -136,9 +138,10 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf // It's only ever useful in cases where you know the signature is valid (since it has already // been or will be checked elsewhere in the stack) and you want to extract values from it. func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) { - parts = strings.Split(tokenString, ".") - if len(parts) != 3 { - return nil, parts, newError("token contains an invalid number of segments", ErrTokenMalformed) + var ok bool + parts, ok = splitToken(tokenString) + if !ok { + return nil, nil, newError("token contains an invalid number of segments", ErrTokenMalformed) } token = &Token{Raw: tokenString} @@ -196,6 +199,33 @@ func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Toke return token, parts, nil } +// splitToken splits a token string into three parts: header, claims, and signature. It will only +// return true if the token contains exactly two delimiters and three parts. In all other cases, it +// will return nil parts and false. +func splitToken(token string) ([]string, bool) { + parts := make([]string, 3) + header, remain, ok := strings.Cut(token, tokenDelimiter) + if !ok { + return nil, false + } + parts[0] = header + claims, remain, ok := strings.Cut(remain, tokenDelimiter) + if !ok { + return nil, false + } + parts[1] = claims + // One more cut to ensure the signature is the last part of the token and there are no more + // delimiters. This avoids an issue where malicious input could contain additional delimiters + // causing unecessary overhead parsing tokens. + signature, _, unexpected := strings.Cut(remain, tokenDelimiter) + if unexpected { + return nil, false + } + parts[2] = signature + + return parts, true +} + // DecodeSegment decodes a JWT specific base64url encoding. This function will // take into account whether the [Parser] is configured with additional options, // such as [WithStrictDecoding] or [WithPaddingAllowed]. diff --git a/token.go b/token.go index 352873a2..9c7f4ab0 100644 --- a/token.go +++ b/token.go @@ -75,7 +75,7 @@ func (t *Token) SignedString(key interface{}) (string, error) { } // SigningString generates the signing string. This is the most expensive part -// of the whole deal. Unless you need this for something special, just go +// of the whole deal. Unless you need this for something special, just go // straight for the SignedString. func (t *Token) SigningString() (string, error) { h, err := json.Marshal(t.Header)