diff --git a/README.md b/README.md index db16671..d84a9c5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A [go](http://www.golang.org) (or 'golang' for search engine friendliness) imple [![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go) +**BREAKING CHANGES COMING:*** Version 3.0.0 is almost complete. It will include _a lot_ of changes including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes will be available before 3.0.0 lands. If you would like to have any input befor 3.0.0 is locked, now's the time to review and provide feedback. + **NOTICE:** A vulnerability in JWT was [recently published](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). As this library doesn't force users to validate the `alg` is what they expected, it's possible your usage is effected. There will be an update soon to remedy this, and it will likey require backwards-incompatible changes to the API. In the short term, please make sure your implementation verifies the `alg` is what you expect. diff --git a/VERSION_HISTORY.md b/VERSION_HISTORY.md index 174a641..2a39b24 100644 --- a/VERSION_HISTORY.md +++ b/VERSION_HISTORY.md @@ -17,10 +17,16 @@ * Moved examples from README to executable example files * Signing method registry is now thread safe -#### 2.5.0 +#### 2.6.0 This will likely be the last backwards compatible release before 3.0.0. +* Exposed inner error within ValidationError +* Fixed validation errors when using UseJSONNumber flag +* Added several unit tests + +#### 2.5.0 + * Added support for signing method none. You shouldn't use this. The API tries to make this clear. * Updated/fixed some documentation * Added more helpful error message when trying to parse tokens that begin with `BEARER ` diff --git a/claims.go b/claims.go index 46b1a42..f0228f0 100644 --- a/claims.go +++ b/claims.go @@ -2,6 +2,8 @@ package jwt import ( "crypto/subtle" + "fmt" + "time" ) // For a type to be a Claims object, it must just have a Valid method that determines @@ -34,17 +36,18 @@ func (c StandardClaims) Valid() error { // The claims below are optional, by default, so if they are set to the // default value in Go, let's not fail the verification for them. if c.VerifyExpiresAt(now, false) == false { - vErr.err = "Token is expired" + delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0)) + vErr.Inner = fmt.Errorf("token is expired by %v", delta) vErr.Errors |= ValidationErrorExpired } if c.VerifyIssuedAt(now, false) == false { - vErr.err = "Token used before issued, clock skew issue?" + vErr.Inner = fmt.Errorf("Token used before issued") vErr.Errors |= ValidationErrorIssuedAt } if c.VerifyNotBefore(now, false) == false { - vErr.err = "Token is not valid yet" + vErr.Inner = fmt.Errorf("token is not valid yet") vErr.Errors |= ValidationErrorNotValidYet } diff --git a/cmd/jwt/README.md b/cmd/jwt/README.md new file mode 100644 index 0000000..4a68ba4 --- /dev/null +++ b/cmd/jwt/README.md @@ -0,0 +1,13 @@ +`jwt` command-line tool +======================= + +This is a simple tool to sign, verify and show JSON Web Tokens from +the command line. + +The following will create and sign a token, then verify it and output the original claims: + + echo {\"foo\":\"bar\"} | bin/jwt -key test/sample_key -alg RS256 -sign - | bin/jwt -key test/sample_key.pub -verify - + +To simply display a token, use: + + echo $JWT | jwt -show - diff --git a/cmd/jwt/app.go b/cmd/jwt/app.go index 4b74a46..c037114 100644 --- a/cmd/jwt/app.go +++ b/cmd/jwt/app.go @@ -16,7 +16,7 @@ import ( "regexp" "strings" - "github.com/dgrijalva/jwt-go" + jwt "github.com/dgrijalva/jwt-go" ) var ( @@ -29,6 +29,7 @@ var ( // Modes - exactly one of these is required flagSign = flag.String("sign", "", "path to claims object to sign or '-' to read from stdin") 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") ) func main() { @@ -56,6 +57,8 @@ func start() error { return signToken() } else if *flagVerify != "" { return verifyToken() + } else if *flagShow != "" { + return showToken() } else { flag.Usage() return fmt.Errorf("None of the required flags are present. What do you want me to do?") @@ -204,6 +207,39 @@ func signToken() error { return nil } +// showToken pretty-prints the token on the command line. +func showToken() error { + // get the token + tokData, err := loadData(*flagShow) + if err != nil { + return fmt.Errorf("Couldn't read token: %v", err) + } + + // trim possible whitespace from token + tokData = regexp.MustCompile(`\s*$`).ReplaceAll(tokData, []byte{}) + if *flagDebug { + fmt.Fprintf(os.Stderr, "Token len: %v bytes\n", len(tokData)) + } + + token, err := jwt.Parse(string(tokData), nil) + if token == nil { + return fmt.Errorf("malformed token: %v", err) + } + + // Print the token details + fmt.Println("Header:") + if err := printJSON(token.Header); err != nil { + return fmt.Errorf("Failed to output header: %v", err) + } + + fmt.Println("Claims:") + if err := printJSON(token.Claims); err != nil { + return fmt.Errorf("Failed to output claims: %v", err) + } + + return nil +} + func isEs() bool { return strings.HasPrefix(*flagAlg, "ES") } diff --git a/errors.go b/errors.go index 6a60e91..2e53f5d 100644 --- a/errors.go +++ b/errors.go @@ -26,18 +26,26 @@ const ( ValidationErrorClaimsInvalid // Generic claims validation error ) +// Helper for constructing a ValidationError with a string error message +func NewValidationError(errorText string, errorFlags uint32) *ValidationError { + return &ValidationError{ + Inner: errors.New(errorText), + Errors: errorFlags, + } +} + // The error from Parse if token is not valid type ValidationError struct { - err string + Inner error // stores the error returned by external dependencies, i.e.: KeyFunc Errors uint32 // bitfield. see ValidationError... constants } // Validation error is an error type func (e ValidationError) Error() string { - if e.err == "" { + if e.Inner == nil { return "token is invalid" } - return e.err + return e.Inner.Error() } // No errors diff --git a/map_claims.go b/map_claims.go index 1e9d59f..291213c 100644 --- a/map_claims.go +++ b/map_claims.go @@ -2,6 +2,8 @@ package jwt import ( "encoding/json" + "errors" + // "fmt" ) // Claims type that uses the map[string]interface{} for JSON decoding @@ -70,17 +72,17 @@ func (m MapClaims) Valid() error { now := TimeFunc().Unix() if m.VerifyExpiresAt(now, false) == false { - vErr.err = "Token is expired" + vErr.Inner = errors.New("Token is expired") vErr.Errors |= ValidationErrorExpired } if m.VerifyIssuedAt(now, false) == false { - vErr.err = "Token used before issued, clock skew issue?" + vErr.Inner = errors.New("Token used before issued") vErr.Errors |= ValidationErrorIssuedAt } if m.VerifyNotBefore(now, false) == false { - vErr.err = "Token is not valid yet" + vErr.Inner = errors.New("Token is not valid yet") vErr.Errors |= ValidationErrorNotValidYet } diff --git a/none.go b/none.go index d697578..f04d189 100644 --- a/none.go +++ b/none.go @@ -13,10 +13,8 @@ type unsafeNoneMagicConstant string func init() { SigningMethodNone = &signingMethodNone{} - NoneSignatureTypeDisallowedError = &ValidationError{ - "'none' signature type is not allowed", - ValidationErrorSignatureInvalid, - } + NoneSignatureTypeDisallowedError = NewValidationError("'none' signature type is not allowed", ValidationErrorSignatureInvalid) + RegisterSigningMethod(SigningMethodNone.Alg(), func() SigningMethod { return SigningMethodNone }) @@ -35,10 +33,10 @@ func (m *signingMethodNone) Verify(signingString, signature string, key interfac } // If signing method is none, signature must be an empty string if signature != "" { - return &ValidationError{ + return NewValidationError( "'none' signing method with non-empty signature", ValidationErrorSignatureInvalid, - } + ) } // Accept 'none' signing method. diff --git a/parser.go b/parser.go index ab3b234..7020c52 100644 --- a/parser.go +++ b/parser.go @@ -22,7 +22,7 @@ func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { parts := strings.Split(tokenString, ".") if len(parts) != 3 { - return nil, &ValidationError{err: "token contains an invalid number of segments", Errors: ValidationErrorMalformed} + return nil, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed) } var err error @@ -32,12 +32,12 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf var headerBytes []byte if headerBytes, err = DecodeSegment(parts[0]); err != nil { if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") { - return token, &ValidationError{err: "tokenstring should not contain 'bearer '", Errors: ValidationErrorMalformed} + return token, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed) } - return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed} + return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} } if err = json.Unmarshal(headerBytes, &token.Header); err != nil { - return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed} + return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} } // parse Claims @@ -45,7 +45,7 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf token.Claims = claims if claimBytes, err = DecodeSegment(parts[1]); err != nil { - return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed} + return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} } dec := json.NewDecoder(bytes.NewBuffer(claimBytes)) if p.UseJSONNumber { @@ -59,16 +59,16 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf } // Handle decode error if err != nil { - return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed} + return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} } // Lookup signature method if method, ok := token.Header["alg"].(string); ok { if token.Method = GetSigningMethod(method); token.Method == nil { - return token, &ValidationError{err: "signing method (alg) is unavailable.", Errors: ValidationErrorUnverifiable} + return token, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable) } } else { - return token, &ValidationError{err: "signing method (alg) is unspecified.", Errors: ValidationErrorUnverifiable} + return token, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable) } // Verify signing method is in the required set @@ -83,7 +83,7 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf } if !signingMethodValid { // signing method is not in the listed set - return token, &ValidationError{err: fmt.Sprintf("signing method %v is invalid", alg), Errors: ValidationErrorSignatureInvalid} + return token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid) } } @@ -91,11 +91,11 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf var key interface{} if keyFunc == nil { // keyFunc was not provided. short circuiting validation - return token, &ValidationError{err: "no Keyfunc was provided.", Errors: ValidationErrorUnverifiable} + return token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable) } if key, err = keyFunc(token); err != nil { // keyFunc returned an error - return token, &ValidationError{err: err.Error(), Errors: ValidationErrorUnverifiable} + return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable} } vErr := &ValidationError{} @@ -106,7 +106,7 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf // If the Claims Valid returned an error, check if it is a validation error, // If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set if e, ok := err.(*ValidationError); !ok { - vErr = &ValidationError{err: err.Error(), Errors: ValidationErrorClaimsInvalid} + vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid} } else { vErr = e } @@ -115,7 +115,7 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf // Perform validation token.Signature = parts[2] if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil { - vErr.err = err.Error() + vErr.Inner = err vErr.Errors |= ValidationErrorSignatureInvalid } diff --git a/parser_test.go b/parser_test.go index 03e3b2c..0c86801 100644 --- a/parser_test.go +++ b/parser_test.go @@ -12,11 +12,13 @@ import ( "github.com/dgrijalva/jwt-go/test" ) +var keyFuncError error = fmt.Errorf("error loading key") + var ( jwtTestDefaultKey *rsa.PublicKey defaultKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return jwtTestDefaultKey, nil } emptyKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, nil } - errorKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, fmt.Errorf("error loading key") } + errorKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, keyFuncError } nilKeyFunc jwt.Keyfunc = nil ) @@ -218,10 +220,16 @@ func TestParser_Parse(t *testing.T) { if err == nil { t.Errorf("[%v] Expecting error. Didn't get one.", data.name) } else { + + ve := err.(*jwt.ValidationError) // compare the bitfield part of the error - if e := err.(*jwt.ValidationError).Errors; e != data.errors { + if e := ve.Errors; e != data.errors { t.Errorf("[%v] Errors don't match expectation. %v != %v", data.name, e, data.errors) } + + if err.Error() == keyFuncError.Error() && ve.Inner != keyFuncError { + t.Errorf("[%v] Inner error does not match expectation. %v != %v", data.name, ve.Inner, keyFuncError) + } } } if data.valid && token.Signature == "" { diff --git a/rsa_utils.go b/rsa_utils.go index 6f3b6ff..213a90d 100644 --- a/rsa_utils.go +++ b/rsa_utils.go @@ -10,6 +10,7 @@ import ( var ( ErrKeyMustBePEMEncoded = errors.New("Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key") ErrNotRSAPrivateKey = errors.New("Key is not a valid RSA private key") + ErrNotRSAPublicKey = errors.New("Key is not a valid RSA public key") ) // Parse PEM encoded PKCS1 or PKCS8 private key @@ -61,7 +62,7 @@ func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) { var pkey *rsa.PublicKey var ok bool if pkey, ok = parsedKey.(*rsa.PublicKey); !ok { - return nil, ErrNotRSAPrivateKey + return nil, ErrNotRSAPublicKey } return pkey, nil diff --git a/token.go b/token.go index 5eb365c..d637e08 100644 --- a/token.go +++ b/token.go @@ -14,7 +14,7 @@ var TimeFunc = time.Now // Parse methods use this callback function to supply // the key for verification. The function receives the parsed, -// but unverified Token. This allows you to use propries in the +// but unverified Token. This allows you to use properties in the // Header of the token (such as `kid`) to identify which key to use. type Keyfunc func(*Token) (interface{}, error)