From 94cd619027da432a792f92887ea66261e1f9e41d Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Mon, 13 Jul 2015 16:14:12 -0300 Subject: [PATCH 01/37] Switch to Claims interface, tests pass. --- jwt.go | 70 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/jwt.go b/jwt.go index 06995aa..36e58f4 100644 --- a/jwt.go +++ b/jwt.go @@ -19,13 +19,46 @@ var TimeFunc = time.Now // Header of the token (such as `kid`) to identify which key to use. type Keyfunc func(*Token) (interface{}, error) +// For a type to be a Claims object, it must just have a Valid method that determines +// if the token is invalid for any supported reason +type Claims interface { + Valid() error +} + +type MapClaim map[string]interface{} + +func (m MapClaim) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + if exp, ok := m["exp"].(float64); ok { + if now > int64(exp) { + vErr.err = "token is expired" + vErr.Errors |= ValidationErrorExpired + } + } + + if nbf, ok := m["nbf"].(float64); ok { + if now < int64(nbf) { + vErr.err = "token is not valid yet" + vErr.Errors |= ValidationErrorNotValidYet + } + } + + if vErr.valid() { + return nil + } + + return vErr +} + // A JWT Token. Different fields will be used depending on whether you're // creating or parsing/verifying a token. type Token struct { Raw string // The raw token. Populated when you Parse a token Method SigningMethod // The signing method used or to be used Header map[string]interface{} // The first segment of the token - Claims map[string]interface{} // The second segment of the token + Claims Claims // The second segment of the token Signature string // The third segment of the token. Populated when you Parse a token Valid bool // Is the token valid? Populated when you Parse/Verify a token } @@ -37,7 +70,7 @@ func New(method SigningMethod) *Token { "typ": "JWT", "alg": method.Alg(), }, - Claims: make(map[string]interface{}), + Claims: make(MapClaim), Method: method, } } @@ -63,16 +96,15 @@ func (t *Token) SigningString() (string, error) { var err error parts := make([]string, 2) for i, _ := range parts { - var source map[string]interface{} - if i == 0 { - source = t.Header - } else { - source = t.Claims - } - var jsonValue []byte - if jsonValue, err = json.Marshal(source); err != nil { - return "", err + if i == 0 { + if jsonValue, err = json.Marshal(t.Header); err != nil { + return "", err + } + } else { + if jsonValue, err = json.Marshal(t.Claims); err != nil { + return "", err + } } parts[i] = EncodeSegment(jsonValue) @@ -130,20 +162,8 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { } // Check expiration times - vErr := &ValidationError{} - now := TimeFunc().Unix() - if exp, ok := token.Claims["exp"].(float64); ok { - if now > int64(exp) { - vErr.err = "token is expired" - vErr.Errors |= ValidationErrorExpired - } - } - if nbf, ok := token.Claims["nbf"].(float64); ok { - if now < int64(nbf) { - vErr.err = "token is not valid yet" - vErr.Errors |= ValidationErrorNotValidYet - } - } + err = token.Claims.Valid() + vErr := err.(ValidationError) // Perform validation if err = token.Method.Verify(strings.Join(parts[0:2], "."), parts[2], key); err != nil { From fa9a0b8c450cbee2c0a2f94768c63da3913024e8 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Tue, 14 Jul 2015 14:31:32 -0300 Subject: [PATCH 02/37] Add validation error bit for generic validation error. --- errors.go | 1 + 1 file changed, 1 insertion(+) diff --git a/errors.go b/errors.go index e9e788f..6acea3f 100644 --- a/errors.go +++ b/errors.go @@ -18,6 +18,7 @@ const ( ValidationErrorSignatureInvalid // Signature validation failed ValidationErrorExpired // Exp validation failed ValidationErrorNotValidYet // NBF validation failed + ValidationErrorClaimsInvalid // Generic claims validation error ) // The error from Parse if token is not valid From febded4195ff4dc721727542f5d51da195e40896 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Tue, 14 Jul 2015 14:34:09 -0300 Subject: [PATCH 03/37] Added a few new methods to use custom Claims structs. Must implement interface Claims, which means there is a Valid method. --- jwt.go | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/jwt.go b/jwt.go index 36e58f4..19ef53b 100644 --- a/jwt.go +++ b/jwt.go @@ -75,6 +75,17 @@ func New(method SigningMethod) *Token { } } +func NewWithClaims(method SigningMethod, claims Claims) *Token { + return &Token{ + Header: map[string]interface{}{ + "typ": "JWT", + "alg": method.Alg(), + }, + Claims: claims, + Method: method, + } +} + // Get the complete, signed token func (t *Token) SignedString(key interface{}) (string, error) { var sig, sstr string @@ -116,13 +127,20 @@ func (t *Token) SigningString() (string, error) { // keyFunc will receive the parsed token and should return the key for validating. // If everything is kosher, err will be nil func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { + return ParseWithClaims(tokenString, keyFunc, make(MapClaim)) +} + +func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token, error) { parts := strings.Split(tokenString, ".") if len(parts) != 3 { return nil, &ValidationError{err: "token contains an invalid number of segments", Errors: ValidationErrorMalformed} } var err error - token := &Token{Raw: tokenString} + token := &Token{ + Raw: tokenString, + } + // parse Header var headerBytes []byte if headerBytes, err = DecodeSegment(parts[0]); err != nil { @@ -134,12 +152,15 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { // parse Claims var claimBytes []byte + if claimBytes, err = DecodeSegment(parts[1]); err != nil { return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed} } - if err = json.Unmarshal(claimBytes, &token.Claims); err != nil { + + if err = json.Unmarshal(claimBytes, &claims); err != nil { return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed} } + token.Claims = claims // Lookup signature method if method, ok := token.Header["alg"].(string); ok { @@ -163,7 +184,17 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { // Check expiration times err = token.Claims.Valid() - vErr := err.(ValidationError) + var vErr *ValidationError + + // If the Claims Valid returned an error, check if it is a validation error, + // if not, convert it into one with a generic ClaimsInvalid flag set + if err != nil { + if e, ok := err.(*ValidationError); !ok { + vErr = &ValidationError{err: err.Error(), Errors: ValidationErrorClaimsInvalid} + } else { + vErr = e + } + } // Perform validation if err = token.Method.Verify(strings.Join(parts[0:2], "."), parts[2], key); err != nil { @@ -171,7 +202,7 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { vErr.Errors |= ValidationErrorSignatureInvalid } - if vErr.valid() { + if vErr == nil || vErr.valid() { token.Valid = true return token, nil } From 44718f8a89b030a85860eef946090aac75faffac Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Fri, 17 Jul 2015 11:35:56 -0300 Subject: [PATCH 04/37] Structured Claims object! Only verify claim if it isn't a default value. The alternative here would be to use pointers in the Claims structure then we would know which were nil, or if they were explicitly set to zero in the claim section Updated MapClaim implementation to check for existance of keys before using them. If they don't exists, validation functions simply return true. --- errors.go | 1 + jwt.go | 185 ++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 167 insertions(+), 19 deletions(-) diff --git a/errors.go b/errors.go index 6acea3f..8e956a8 100644 --- a/errors.go +++ b/errors.go @@ -18,6 +18,7 @@ const ( ValidationErrorSignatureInvalid // Signature validation failed ValidationErrorExpired // Exp validation failed ValidationErrorNotValidYet // NBF validation failed + ValidationErrorIssuedAt // IAT validation failed ValidationErrorClaimsInvalid // Generic claims validation error ) diff --git a/jwt.go b/jwt.go index 19ef53b..1ecc5a2 100644 --- a/jwt.go +++ b/jwt.go @@ -21,26 +21,45 @@ type Keyfunc func(*Token) (interface{}, error) // For a type to be a Claims object, it must just have a Valid method that determines // if the token is invalid for any supported reason -type Claims interface { +type Claimer interface { Valid() error } -type MapClaim map[string]interface{} +// Structured version of Claims Section, as referenced at https://tools.ietf.org/html/rfc7519#section-4.1 +type Claims struct { + Audience string `json:"aud,omitempty"` + ExpiresAt int64 `json:"exp,omitempty"` + Id string `json:"jti,omitempty"` + IssuedAt int64 `json:"iat,omitempty"` + Issuer string `json:"iss,omitempty"` + NotBefore int64 `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` +} -func (m MapClaim) Valid() error { +func (c Claims) Valid() error { vErr := new(ValidationError) now := TimeFunc().Unix() - if exp, ok := m["exp"].(float64); ok { - if now > int64(exp) { - vErr.err = "token is expired" + // The claims below are optional, so if they are set to the default value in Go, let's not + // verify them. + + if c.ExpiresAt != 0 { + if c.VerifyExpiresAt(now) == false { + vErr.err = "Token is expired" vErr.Errors |= ValidationErrorExpired } } - if nbf, ok := m["nbf"].(float64); ok { - if now < int64(nbf) { - vErr.err = "token is not valid yet" + if c.IssuedAt != 0 { + if c.VerifyIssuedAt(now) == false { + vErr.err = "Token used before issued, clock skew issue?" + vErr.Errors |= ValidationErrorIssuedAt + } + } + + if c.NotBefore != 0 { + if c.VerifyNotBefore(now) == false { + vErr.err = "Token is not valid yet" vErr.Errors |= ValidationErrorNotValidYet } } @@ -52,13 +71,141 @@ func (m MapClaim) Valid() error { return vErr } +func (c *Claims) VerifyAudience(cmp string) bool { + return verifyAud(c.Audience, cmp) +} + +func (c *Claims) VerifyExpiresAt(cmp int64) bool { + return verifyExp(c.ExpiresAt, cmp) +} + +func (c *Claims) VerifyIssuedAt(cmp int64) bool { + return verifyIat(c.IssuedAt, cmp) +} + +func (c *Claims) VerifyIssuer(cmp string) bool { + return verifyIss(c.Issuer, cmp) +} + +func (c *Claims) VerifyNotBefore(cmp int64) bool { + return verifyNbf(c.NotBefore, cmp) +} + +type MapClaim map[string]interface{} + +func (m MapClaim) VerifyAudience(cmp string) bool { + val, exists := m["aud"] + if !exists { + return true // Don't fail validation if claim doesn't exist + } + + if aud, ok := val.(string); ok { + return verifyAud(aud, cmp) + } + return false +} + +func (m MapClaim) VerifyExpiresAt(cmp int64) bool { + val, exists := m["exp"] + if !exists { + return true + } + + if exp, ok := val.(float64); ok { + return verifyExp(int64(exp), cmp) + } + return false +} + +func (m MapClaim) VerifyIssuedAt(cmp int64) bool { + val, exists := m["iat"] + if !exists { + return true + } + + if iat, ok := val.(float64); ok { + return verifyIat(int64(iat), cmp) + } + return false +} + +func (m MapClaim) VerifyIssuer(cmp string) bool { + val, exists := m["iss"] + if !exists { + return true + } + + if iss, ok := val.(string); ok { + return verifyIss(iss, cmp) + } + return false +} + +func (m MapClaim) VerifyNotBefore(cmp int64) bool { + val, exists := m["nbf"] + if !exists { + return true + } + + if nbf, ok := val.(float64); ok { + return verifyNbf(int64(nbf), cmp) + } + return false +} + +func (m MapClaim) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + if m.VerifyExpiresAt(now) == false { + vErr.err = "Token is expired" + vErr.Errors |= ValidationErrorExpired + } + + if m.VerifyIssuedAt(now) == false { + vErr.err = "Token used before issued, clock skew issue?" + vErr.Errors |= ValidationErrorIssuedAt + } + + if m.VerifyNotBefore(now) == false { + vErr.err = "Token is not valid yet" + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} + +func verifyAud(aud string, cmp string) bool { + return aud == cmp +} + +func verifyExp(exp int64, now int64) bool { + return now <= exp +} + +func verifyIat(iat int64, now int64) bool { + return now >= iat +} + +func verifyIss(iss string, cmp string) bool { + return iss == cmp +} + +func verifyNbf(nbf int64, now int64) bool { + return now >= nbf +} + // A JWT Token. Different fields will be used depending on whether you're // creating or parsing/verifying a token. type Token struct { Raw string // The raw token. Populated when you Parse a token Method SigningMethod // The signing method used or to be used Header map[string]interface{} // The first segment of the token - Claims Claims // The second segment of the token + Claims Claimer // The second segment of the token Signature string // The third segment of the token. Populated when you Parse a token Valid bool // Is the token valid? Populated when you Parse/Verify a token } @@ -70,12 +217,12 @@ func New(method SigningMethod) *Token { "typ": "JWT", "alg": method.Alg(), }, - Claims: make(MapClaim), + Claims: Claims{}, Method: method, } } -func NewWithClaims(method SigningMethod, claims Claims) *Token { +func NewWithClaims(method SigningMethod, claims Claimer) *Token { return &Token{ Header: map[string]interface{}{ "typ": "JWT", @@ -127,10 +274,10 @@ func (t *Token) SigningString() (string, error) { // keyFunc will receive the parsed token and should return the key for validating. // If everything is kosher, err will be nil func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { - return ParseWithClaims(tokenString, keyFunc, make(MapClaim)) + return ParseWithClaims(tokenString, keyFunc, &Claims{}) } -func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token, error) { +func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claimer) (*Token, error) { parts := strings.Split(tokenString, ".") if len(parts) != 3 { return nil, &ValidationError{err: "token contains an invalid number of segments", Errors: ValidationErrorMalformed} @@ -182,13 +329,13 @@ func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token return token, &ValidationError{err: err.Error(), Errors: ValidationErrorUnverifiable} } - // Check expiration times - err = token.Claims.Valid() var vErr *ValidationError - // If the Claims Valid returned an error, check if it is a validation error, - // if not, convert it into one with a generic ClaimsInvalid flag set - if err != nil { + // Validate Claims + if err := token.Claims.Valid(); err != nil { + + // 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} } else { From a33fdf927a780e516468b80861b6e82fb80b3e10 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Fri, 17 Jul 2015 14:28:08 -0300 Subject: [PATCH 05/37] Going through and updating tests to pass. Still need to add a test that utilizes the defaults of the structured object. Update Cmdline app Update package reference for PR. Update examples --- cmd/jwt/app.go | 5 ++--- example_test.go | 15 ++++++++++----- jwt.go | 13 ++++++++----- jwt_test.go | 38 +++++++++++++++++++++----------------- 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/cmd/jwt/app.go b/cmd/jwt/app.go index 62cb9a4..f4d51e8 100644 --- a/cmd/jwt/app.go +++ b/cmd/jwt/app.go @@ -155,7 +155,7 @@ func signToken() error { } // parse the JSON of the claims - var claims map[string]interface{} + var claims jwt.MapClaim if err := json.Unmarshal(tokData, &claims); err != nil { return fmt.Errorf("Couldn't parse claims JSON: %v", err) } @@ -173,8 +173,7 @@ func signToken() error { } // create a new token - token := jwt.New(alg) - token.Claims = claims + token := jwt.NewWithClaims(alg, claims) if out, err := token.SignedString(keyData); err == nil { fmt.Println(out) diff --git a/example_test.go b/example_test.go index edb48e4..80e0f3a 100644 --- a/example_test.go +++ b/example_test.go @@ -2,8 +2,9 @@ package jwt_test import ( "fmt" - "github.com/dgrijalva/jwt-go" "time" + + "github.com/dgrijalva/jwt-go" ) func ExampleParse(myToken string, myLookupKey func(interface{}) (interface{}, error)) { @@ -19,11 +20,15 @@ func ExampleParse(myToken string, myLookupKey func(interface{}) (interface{}, er } func ExampleNew(mySigningKey []byte) (string, error) { - // Create the token - token := jwt.New(jwt.SigningMethodHS256) // Set some claims - token.Claims["foo"] = "bar" - token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix() + claim := jwt.MapClaim{ + "foo": "bar", + "exp": time.Now().Add(time.Hour * 72).Unix(), + } + + // Create the token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim) + // Sign and get the complete encoded token as a string tokenString, err := token.SignedString(mySigningKey) return tokenString, err diff --git a/jwt.go b/jwt.go index 1ecc5a2..d7ce56c 100644 --- a/jwt.go +++ b/jwt.go @@ -307,6 +307,7 @@ func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claimer) (*Toke if err = json.Unmarshal(claimBytes, &claims); err != nil { return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed} } + token.Claims = claims // Lookup signature method @@ -329,7 +330,7 @@ func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claimer) (*Toke return token, &ValidationError{err: err.Error(), Errors: ValidationErrorUnverifiable} } - var vErr *ValidationError + vErr := &ValidationError{} // Validate Claims if err := token.Claims.Valid(); err != nil { @@ -349,7 +350,7 @@ func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claimer) (*Toke vErr.Errors |= ValidationErrorSignatureInvalid } - if vErr == nil || vErr.valid() { + if vErr.valid() { token.Valid = true return token, nil } @@ -362,23 +363,25 @@ func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claimer) (*Toke // Currently, it looks in the Authorization header as well as // looking for an 'access_token' request parameter in req.Form. func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err error) { + return ParseFromRequestWithClaims(req, keyFunc, &Claims{}) +} +func ParseFromRequestWithClaims(req *http.Request, keyFunc Keyfunc, claims Claimer) (token *Token, err error) { // Look for an Authorization header if ah := req.Header.Get("Authorization"); ah != "" { // Should be a bearer token if len(ah) > 6 && strings.ToUpper(ah[0:6]) == "BEARER" { - return Parse(ah[7:], keyFunc) + return ParseWithClaims(ah[7:], keyFunc, claims) } } // Look for "access_token" parameter req.ParseMultipartForm(10e6) if tokStr := req.Form.Get("access_token"); tokStr != "" { - return Parse(tokStr, keyFunc) + return ParseWithClaims(tokStr, keyFunc, claims) } return nil, ErrNoTokenInRequest - } // Encode JWT specific base64url encoding with padding stripped diff --git a/jwt_test.go b/jwt_test.go index 9108ded..f910ebb 100644 --- a/jwt_test.go +++ b/jwt_test.go @@ -2,12 +2,13 @@ package jwt_test import ( "fmt" - "github.com/dgrijalva/jwt-go" "io/ioutil" "net/http" "reflect" "testing" "time" + + "github.com/dgrijalva/jwt-go" ) var ( @@ -22,7 +23,7 @@ var jwtTestData = []struct { name string tokenString string keyfunc jwt.Keyfunc - claims map[string]interface{} + claims jwt.MapClaim valid bool errors uint32 }{ @@ -30,7 +31,7 @@ var jwtTestData = []struct { "basic", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", defaultKeyFunc, - map[string]interface{}{"foo": "bar"}, + jwt.MapClaim{"foo": "bar"}, true, 0, }, @@ -38,7 +39,7 @@ var jwtTestData = []struct { "basic expired", "", // autogen defaultKeyFunc, - map[string]interface{}{"foo": "bar", "exp": float64(time.Now().Unix() - 100)}, + jwt.MapClaim{"foo": "bar", "exp": float64(time.Now().Unix() - 100)}, false, jwt.ValidationErrorExpired, }, @@ -46,7 +47,7 @@ var jwtTestData = []struct { "basic nbf", "", // autogen defaultKeyFunc, - map[string]interface{}{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)}, + jwt.MapClaim{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)}, false, jwt.ValidationErrorNotValidYet, }, @@ -54,7 +55,7 @@ var jwtTestData = []struct { "expired and nbf", "", // autogen defaultKeyFunc, - map[string]interface{}{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)}, + jwt.MapClaim{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)}, false, jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired, }, @@ -62,7 +63,7 @@ var jwtTestData = []struct { "basic invalid", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", defaultKeyFunc, - map[string]interface{}{"foo": "bar"}, + jwt.MapClaim{"foo": "bar"}, false, jwt.ValidationErrorSignatureInvalid, }, @@ -70,7 +71,7 @@ var jwtTestData = []struct { "basic nokeyfunc", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", nilKeyFunc, - map[string]interface{}{"foo": "bar"}, + jwt.MapClaim{"foo": "bar"}, false, jwt.ValidationErrorUnverifiable, }, @@ -78,7 +79,7 @@ var jwtTestData = []struct { "basic nokey", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", emptyKeyFunc, - map[string]interface{}{"foo": "bar"}, + jwt.MapClaim{"foo": "bar"}, false, jwt.ValidationErrorSignatureInvalid, }, @@ -86,7 +87,7 @@ var jwtTestData = []struct { "basic errorkey", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", errorKeyFunc, - map[string]interface{}{"foo": "bar"}, + jwt.MapClaim{"foo": "bar"}, false, jwt.ValidationErrorUnverifiable, }, @@ -99,14 +100,13 @@ func init() { } } -func makeSample(c map[string]interface{}) string { +func makeSample(c jwt.MapClaim) string { key, e := ioutil.ReadFile("test/sample_key") if e != nil { panic(e.Error()) } - token := jwt.New(jwt.SigningMethodRS256) - token.Claims = c + token := jwt.NewWithClaims(jwt.SigningMethodRS256, c) s, e := token.SignedString(key) if e != nil { @@ -121,17 +121,21 @@ func TestJWT(t *testing.T) { if data.tokenString == "" { data.tokenString = makeSample(data.claims) } - token, err := jwt.Parse(data.tokenString, data.keyfunc) - if !reflect.DeepEqual(data.claims, token.Claims) { + token, err := jwt.ParseWithClaims(data.tokenString, data.keyfunc, &jwt.MapClaim{}) + + if !reflect.DeepEqual(&data.claims, token.Claims) { t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims) } + if data.valid && err != nil { t.Errorf("[%v] Error while verifying token: %T:%v", data.name, err, err) } + if !data.valid && err == nil { t.Errorf("[%v] Invalid token passed validation", data.name) } + if data.errors != 0 { if err == nil { t.Errorf("[%v] Expecting error. Didn't get one.", data.name) @@ -155,13 +159,13 @@ func TestParseRequest(t *testing.T) { r, _ := http.NewRequest("GET", "/", nil) r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", data.tokenString)) - token, err := jwt.ParseFromRequest(r, data.keyfunc) + token, err := jwt.ParseFromRequestWithClaims(r, data.keyfunc, &jwt.MapClaim{}) if token == nil { t.Errorf("[%v] Token was not found: %v", data.name, err) continue } - if !reflect.DeepEqual(data.claims, token.Claims) { + if !reflect.DeepEqual(&data.claims, token.Claims) { t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims) } if data.valid && err != nil { From dfdafab9a7ee68d1a7e4b9b1478afdf02fbcc562 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Fri, 17 Jul 2015 15:55:06 -0300 Subject: [PATCH 06/37] Update, use MapClaim by default. --- jwt.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jwt.go b/jwt.go index d7ce56c..3bb8200 100644 --- a/jwt.go +++ b/jwt.go @@ -217,7 +217,7 @@ func New(method SigningMethod) *Token { "typ": "JWT", "alg": method.Alg(), }, - Claims: Claims{}, + Claims: MapClaim{}, Method: method, } } @@ -274,7 +274,7 @@ func (t *Token) SigningString() (string, error) { // keyFunc will receive the parsed token and should return the key for validating. // If everything is kosher, err will be nil func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { - return ParseWithClaims(tokenString, keyFunc, &Claims{}) + return ParseWithClaims(tokenString, keyFunc, &MapClaim{}) } func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claimer) (*Token, error) { From a6f24f4cf0a3eaa62fff79f3c57cf59bd7ce1b68 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Fri, 17 Jul 2015 15:59:18 -0300 Subject: [PATCH 07/37] Update Claimer -> Claims, update Claims struct -> StandardClaims. --- jwt.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/jwt.go b/jwt.go index 3bb8200..60546c5 100644 --- a/jwt.go +++ b/jwt.go @@ -21,12 +21,12 @@ type Keyfunc func(*Token) (interface{}, error) // For a type to be a Claims object, it must just have a Valid method that determines // if the token is invalid for any supported reason -type Claimer interface { +type Claims interface { Valid() error } // Structured version of Claims Section, as referenced at https://tools.ietf.org/html/rfc7519#section-4.1 -type Claims struct { +type StandardClaims struct { Audience string `json:"aud,omitempty"` ExpiresAt int64 `json:"exp,omitempty"` Id string `json:"jti,omitempty"` @@ -36,7 +36,7 @@ type Claims struct { Subject string `json:"sub,omitempty"` } -func (c Claims) Valid() error { +func (c StandardClaims) Valid() error { vErr := new(ValidationError) now := TimeFunc().Unix() @@ -71,23 +71,23 @@ func (c Claims) Valid() error { return vErr } -func (c *Claims) VerifyAudience(cmp string) bool { +func (c *StandardClaims) VerifyAudience(cmp string) bool { return verifyAud(c.Audience, cmp) } -func (c *Claims) VerifyExpiresAt(cmp int64) bool { +func (c *StandardClaims) VerifyExpiresAt(cmp int64) bool { return verifyExp(c.ExpiresAt, cmp) } -func (c *Claims) VerifyIssuedAt(cmp int64) bool { +func (c *StandardClaims) VerifyIssuedAt(cmp int64) bool { return verifyIat(c.IssuedAt, cmp) } -func (c *Claims) VerifyIssuer(cmp string) bool { +func (c *StandardClaims) VerifyIssuer(cmp string) bool { return verifyIss(c.Issuer, cmp) } -func (c *Claims) VerifyNotBefore(cmp int64) bool { +func (c *StandardClaims) VerifyNotBefore(cmp int64) bool { return verifyNbf(c.NotBefore, cmp) } @@ -205,7 +205,7 @@ type Token struct { Raw string // The raw token. Populated when you Parse a token Method SigningMethod // The signing method used or to be used Header map[string]interface{} // The first segment of the token - Claims Claimer // The second segment of the token + Claims Claims // The second segment of the token Signature string // The third segment of the token. Populated when you Parse a token Valid bool // Is the token valid? Populated when you Parse/Verify a token } @@ -222,7 +222,7 @@ func New(method SigningMethod) *Token { } } -func NewWithClaims(method SigningMethod, claims Claimer) *Token { +func NewWithClaims(method SigningMethod, claims Claims) *Token { return &Token{ Header: map[string]interface{}{ "typ": "JWT", @@ -277,7 +277,7 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { return ParseWithClaims(tokenString, keyFunc, &MapClaim{}) } -func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claimer) (*Token, error) { +func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token, error) { parts := strings.Split(tokenString, ".") if len(parts) != 3 { return nil, &ValidationError{err: "token contains an invalid number of segments", Errors: ValidationErrorMalformed} @@ -363,10 +363,10 @@ func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claimer) (*Toke // Currently, it looks in the Authorization header as well as // looking for an 'access_token' request parameter in req.Form. func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err error) { - return ParseFromRequestWithClaims(req, keyFunc, &Claims{}) + return ParseFromRequestWithClaims(req, keyFunc, &MapClaim{}) } -func ParseFromRequestWithClaims(req *http.Request, keyFunc Keyfunc, claims Claimer) (token *Token, err error) { +func ParseFromRequestWithClaims(req *http.Request, keyFunc Keyfunc, claims Claims) (token *Token, err error) { // Look for an Authorization header if ah := req.Header.Get("Authorization"); ah != "" { // Should be a bearer token From 3eddded2f3c65dfccfc1e86c844b3f8762fbb3ae Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Fri, 17 Jul 2015 16:40:52 -0300 Subject: [PATCH 08/37] Adding additional bits to mask for various validation errors. --- errors.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/errors.go b/errors.go index 8e956a8..b055f3b 100644 --- a/errors.go +++ b/errors.go @@ -16,10 +16,15 @@ const ( ValidationErrorMalformed uint32 = 1 << iota // Token is malformed ValidationErrorUnverifiable // Token could not be verified because of signing problems ValidationErrorSignatureInvalid // Signature validation failed - ValidationErrorExpired // Exp validation failed - ValidationErrorNotValidYet // NBF validation failed - ValidationErrorIssuedAt // IAT validation failed - ValidationErrorClaimsInvalid // Generic claims validation error + + // Standard Claim validation errors + ValidationErrorAudience // AUD validation failed + ValidationErrorExpired // EXP validation failed + ValidationErrorIssuedAt // IAT validation failed + ValidationErrorIssuer // ISS validation failed + ValidationErrorNotValidYet // NBF validation failed + ValidationErrorId // JTI validation failed + ValidationErrorClaimsInvalid // Generic claims validation error ) // The error from Parse if token is not valid From ec042acef733f1a3fdc10291d159e8e7a0b85ce6 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Mon, 20 Jul 2015 09:44:56 -0300 Subject: [PATCH 09/37] Recommended changes from PR. Plus some documentation. --- jwt.go | 267 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 139 insertions(+), 128 deletions(-) diff --git a/jwt.go b/jwt.go index 60546c5..53972c3 100644 --- a/jwt.go +++ b/jwt.go @@ -1,6 +1,7 @@ package jwt import ( + "crypto/subtle" "encoding/base64" "encoding/json" "net/http" @@ -25,7 +26,8 @@ type Claims interface { Valid() error } -// Structured version of Claims Section, as referenced at https://tools.ietf.org/html/rfc7519#section-4.1 +// Structured version of Claims Section, as referenced at +// https://tools.ietf.org/html/rfc7519#section-4.1 type StandardClaims struct { Audience string `json:"aud,omitempty"` ExpiresAt int64 `json:"exp,omitempty"` @@ -36,138 +38,27 @@ type StandardClaims struct { Subject string `json:"sub,omitempty"` } +// Validates time based claims "exp, iat, nbf". +// There is no accounting for clock skew. +// As well, if any of the above claims are not in the token, it will still +// be considered a valid claim. func (c StandardClaims) Valid() error { vErr := new(ValidationError) now := TimeFunc().Unix() - // The claims below are optional, so if they are set to the default value in Go, let's not - // verify them. - - if c.ExpiresAt != 0 { - if c.VerifyExpiresAt(now) == false { - vErr.err = "Token is expired" - vErr.Errors |= ValidationErrorExpired - } - } - - if c.IssuedAt != 0 { - if c.VerifyIssuedAt(now) == false { - vErr.err = "Token used before issued, clock skew issue?" - vErr.Errors |= ValidationErrorIssuedAt - } - } - - if c.NotBefore != 0 { - if c.VerifyNotBefore(now) == false { - vErr.err = "Token is not valid yet" - vErr.Errors |= ValidationErrorNotValidYet - } - } - - if vErr.valid() { - return nil - } - - return vErr -} - -func (c *StandardClaims) VerifyAudience(cmp string) bool { - return verifyAud(c.Audience, cmp) -} - -func (c *StandardClaims) VerifyExpiresAt(cmp int64) bool { - return verifyExp(c.ExpiresAt, cmp) -} - -func (c *StandardClaims) VerifyIssuedAt(cmp int64) bool { - return verifyIat(c.IssuedAt, cmp) -} - -func (c *StandardClaims) VerifyIssuer(cmp string) bool { - return verifyIss(c.Issuer, cmp) -} - -func (c *StandardClaims) VerifyNotBefore(cmp int64) bool { - return verifyNbf(c.NotBefore, cmp) -} - -type MapClaim map[string]interface{} - -func (m MapClaim) VerifyAudience(cmp string) bool { - val, exists := m["aud"] - if !exists { - return true // Don't fail validation if claim doesn't exist - } - - if aud, ok := val.(string); ok { - return verifyAud(aud, cmp) - } - return false -} - -func (m MapClaim) VerifyExpiresAt(cmp int64) bool { - val, exists := m["exp"] - if !exists { - return true - } - - if exp, ok := val.(float64); ok { - return verifyExp(int64(exp), cmp) - } - return false -} - -func (m MapClaim) VerifyIssuedAt(cmp int64) bool { - val, exists := m["iat"] - if !exists { - return true - } - - if iat, ok := val.(float64); ok { - return verifyIat(int64(iat), cmp) - } - return false -} - -func (m MapClaim) VerifyIssuer(cmp string) bool { - val, exists := m["iss"] - if !exists { - return true - } - - if iss, ok := val.(string); ok { - return verifyIss(iss, cmp) - } - return false -} - -func (m MapClaim) VerifyNotBefore(cmp int64) bool { - val, exists := m["nbf"] - if !exists { - return true - } - - if nbf, ok := val.(float64); ok { - return verifyNbf(int64(nbf), cmp) - } - return false -} - -func (m MapClaim) Valid() error { - vErr := new(ValidationError) - now := TimeFunc().Unix() - - if m.VerifyExpiresAt(now) == false { + // 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" vErr.Errors |= ValidationErrorExpired } - if m.VerifyIssuedAt(now) == false { + if c.VerifyIssuedAt(now, false) == false { vErr.err = "Token used before issued, clock skew issue?" vErr.Errors |= ValidationErrorIssuedAt } - if m.VerifyNotBefore(now) == false { + if c.VerifyNotBefore(now, false) == false { vErr.err = "Token is not valid yet" vErr.Errors |= ValidationErrorNotValidYet } @@ -179,23 +70,143 @@ func (m MapClaim) Valid() error { return vErr } -func verifyAud(aud string, cmp string) bool { - return aud == cmp +// Compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { + return verifyAud(c.Audience, cmp, req) } -func verifyExp(exp int64, now int64) bool { +// Compares the exp claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { + return verifyExp(c.ExpiresAt, cmp, req) +} + +// Compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { + return verifyIat(c.IssuedAt, cmp, req) +} + +// Compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool { + return verifyIss(c.Issuer, cmp, req) +} + +// Compares the nbf claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { + return verifyNbf(c.NotBefore, cmp, req) +} + +type MapClaim map[string]interface{} + +// Compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyAudience(cmp string, req bool) bool { + aud, _ := m["aud"].(string) + return verifyAud(aud, cmp, req) +} + +// Compares the exp claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyExpiresAt(cmp int64, req bool) bool { + exp, _ := m["exp"].(float64) + return verifyExp(int64(exp), cmp, req) +} + +// Compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyIssuedAt(cmp int64, req bool) bool { + iat, _ := m["iat"].(float64) + return verifyIat(int64(iat), cmp, req) +} + +// Compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyIssuer(cmp string, req bool) bool { + iss, _ := m["iss"].(string) + return verifyIss(iss, cmp, req) +} + +// Compares the nbf claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyNotBefore(cmp int64, req bool) bool { + nbf, _ := m["nbf"].(float64) + return verifyNbf(int64(nbf), cmp, req) +} + +// Validates time based claims "exp, iat, nbf". +// There is no accounting for clock skew. +// As well, if any of the above claims are not in the token, it will still +// be considered a valid claim. +func (m MapClaim) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + if m.VerifyExpiresAt(now, false) == false { + vErr.err = "Token is expired" + vErr.Errors |= ValidationErrorExpired + } + + if m.VerifyIssuedAt(now, false) == false { + vErr.err = "Token used before issued, clock skew issue?" + vErr.Errors |= ValidationErrorIssuedAt + } + + if m.VerifyNotBefore(now, false) == false { + vErr.err = "Token is not valid yet" + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} + +func verifyAud(aud string, cmp string, required bool) bool { + if aud == "" { + return !required + } + if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 { + return true + } else { + return false + } +} + +func verifyExp(exp int64, now int64, required bool) bool { + if exp == 0 { + return !required + } return now <= exp } -func verifyIat(iat int64, now int64) bool { +func verifyIat(iat int64, now int64, required bool) bool { + if iat == 0 { + return !required + } return now >= iat } -func verifyIss(iss string, cmp string) bool { - return iss == cmp +func verifyIss(iss string, cmp string, required bool) bool { + if iss == "" { + return !required + } + if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 { + return true + } else { + return false + } } -func verifyNbf(nbf int64, now int64) bool { +func verifyNbf(nbf int64, now int64, required bool) bool { + if nbf == 0 { + return !required + } return now >= nbf } From b00e282378b10a57484227166ecf7bb7ebde8ae1 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Mon, 20 Jul 2015 13:20:18 -0300 Subject: [PATCH 10/37] Update README with some migration information. --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++------ example_test.go | 25 +++++++++++++++-------- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1435ddb..bda890b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,48 @@ A [go](http://www.golang.org) (or 'golang' for search engine friendliness) imple **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. +## Migration Guide from v2 -> v3 + +Added the ability to supply a typed object for the claims section of the token. + +Unfortunately this requires a breaking change. A few new methods were added to support this, +and the old default of `map[string]interface{}` was changed to `jwt.MapClaim`. + +The old example for creating a token looked like this.. + +```go + token := jwt.New(jwt.SigningMethodHS256) + token.Claims["foo"] = "bar" + token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix() +``` + +is now directly mapped to... + +```go + token := jwt.New(jwt.SigningMethodHS256) + claims := token.Claims.(jwt.MapClaim) + claims["foo"] = "bar" + claims["exp"] = time.Now().Add(time.Hour * 72).Unix() +``` + +However, we added a helper `jwt.NewWithClaims` which accepts a claims object. + +Any type can now be used as the claim object for inside a token so long as it implements the interface `jwt.Claims`. + +So, we added an additional claim type `jwt.StandardClaims` was added. +This is intended to be used as a base for creating your own types from, +and includes a few helper functions for verifying the claims defined [here](https://tools.ietf.org/html/rfc7519#section-4.1). + +```go + claims := jwt.StandardClaims{ + Audience: "myapi" + ExpiresAt: time.Now().Add(time.Hour * 72).Unix(), + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) +``` + +On the other end of usage all of the `jwt.Parse` and friends got a `WithClaims` suffix added to them. + ## What the heck is a JWT? In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way. @@ -35,18 +77,18 @@ Parsing and verifying tokens is pretty straight forward. You pass in the token deliverUtterRejection(":(") } ``` - + ## Create a token ```go // Create the token - token := jwt.New(jwt.SigningMethodHS256) - // Set some claims - token.Claims["foo"] = "bar" - token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix() + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaim{ + "foo": "bar", + "exp": time.Now().Add(time.Hour * 72).Unix(), + }) // Sign and get the complete encoded token as a string tokenString, err := token.SignedString(mySigningKey) -``` +``` ## Project Status & Versioning diff --git a/example_test.go b/example_test.go index 80e0f3a..6ffa8c0 100644 --- a/example_test.go +++ b/example_test.go @@ -2,7 +2,6 @@ package jwt_test import ( "fmt" - "time" "github.com/dgrijalva/jwt-go" ) @@ -20,20 +19,30 @@ func ExampleParse(myToken string, myLookupKey func(interface{}) (interface{}, er } func ExampleNew(mySigningKey []byte) (string, error) { - // Set some claims - claim := jwt.MapClaim{ - "foo": "bar", - "exp": time.Now().Add(time.Hour * 72).Unix(), - } - // Create the token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim) + token := jwt.New(jwt.SigningMethodRS256) + + // Set some claims + claims := token.Claims.(jwt.MapClaim) + claims["foo"] = "bar" + claims["exp"] = 15000 // Sign and get the complete encoded token as a string tokenString, err := token.SignedString(mySigningKey) return tokenString, err } +func ExampleNewWithClaims(mySigningKey []byte) (string, error) { + // Create the Claims + claims := jwt.StandardClaims{ + ExpiresAt: 15000, + Issuer: "test", + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + return token.SignedString(mySigningKey) +} + func ExampleParse_errorChecking(myToken string, myLookupKey func(interface{}) (interface{}, error)) { token, err := jwt.Parse(myToken, func(token *jwt.Token) (interface{}, error) { return myLookupKey(token.Header["kid"]) From 15b4825280850476bfd071ac87e8af46b8eda4f1 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Mon, 20 Jul 2015 13:25:55 -0300 Subject: [PATCH 11/37] Add example to migration showing new usage of Parse(WithClaims) --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index bda890b..7907c0b 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,21 @@ and includes a few helper functions for verifying the claims defined [here](http On the other end of usage all of the `jwt.Parse` and friends got a `WithClaims` suffix added to them. +```go + token, err := jwt.Parse(token, keyFunc) + claims := token.Claims.(jwt.MapClaim) + //like you used to.. + claims["foo"] + claims["bar"] +``` + +New method usage: +```go + token, err := jwt.ParseWithClaims(token, keyFunc, &jwt.StandardClaims{}) + claims := token.Claims.(jwt.StandardClaims) + fmt.Println(claims.IssuedAt) +``` + ## What the heck is a JWT? In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way. From 011d5bb935b261a904b037c7bc1d1f266af43b51 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Mon, 20 Jul 2015 10:23:11 -0700 Subject: [PATCH 12/37] drop support for []byte keys in RSA signing methods --- jwt_test.go | 16 ++++++++++++---- rsa.go | 28 +++++++--------------------- rsa_test.go | 6 ++++-- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/jwt_test.go b/jwt_test.go index 9108ded..15aeb30 100644 --- a/jwt_test.go +++ b/jwt_test.go @@ -1,6 +1,7 @@ package jwt_test import ( + "crypto/rsa" "fmt" "github.com/dgrijalva/jwt-go" "io/ioutil" @@ -11,7 +12,7 @@ import ( ) var ( - jwtTestDefaultKey []byte + 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") } @@ -93,14 +94,21 @@ var jwtTestData = []struct { } func init() { - var e error - if jwtTestDefaultKey, e = ioutil.ReadFile("test/sample_key.pub"); e != nil { + if keyData, e := ioutil.ReadFile("test/sample_key.pub"); e == nil { + if jwtTestDefaultKey, e = jwt.ParseRSAPublicKeyFromPEM(keyData); e != nil { + panic(e) + } + } else { panic(e) } } func makeSample(c map[string]interface{}) string { - key, e := ioutil.ReadFile("test/sample_key") + keyData, e := ioutil.ReadFile("test/sample_key") + if e != nil { + panic(e.Error()) + } + key, e := jwt.ParseRSAPrivateKeyFromPEM(keyData) if e != nil { panic(e.Error()) } diff --git a/rsa.go b/rsa.go index cddffce..ceb80f6 100644 --- a/rsa.go +++ b/rsa.go @@ -44,8 +44,7 @@ func (m *SigningMethodRSA) Alg() string { } // Implements the Verify method from SigningMethod -// For this signing method, must be either a PEM encoded PKCS1 or PKCS8 RSA public key as -// []byte, or an rsa.PublicKey structure. +// For this signing method, must be an rsa.PublicKey structure. func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error { var err error @@ -56,15 +55,9 @@ func (m *SigningMethodRSA) Verify(signingString, signature string, key interface } var rsaKey *rsa.PublicKey + var ok bool - switch k := key.(type) { - case []byte: - if rsaKey, err = ParseRSAPublicKeyFromPEM(k); err != nil { - return err - } - case *rsa.PublicKey: - rsaKey = k - default: + if rsaKey, ok = key.(*rsa.PublicKey); !ok { return ErrInvalidKey } @@ -80,20 +73,13 @@ func (m *SigningMethodRSA) Verify(signingString, signature string, key interface } // Implements the Sign method from SigningMethod -// For this signing method, must be either a PEM encoded PKCS1 or PKCS8 RSA private key as -// []byte, or an rsa.PrivateKey structure. +// For this signing method, must be an rsa.PrivateKey structure. func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) { - var err error var rsaKey *rsa.PrivateKey + var ok bool - switch k := key.(type) { - case []byte: - if rsaKey, err = ParseRSAPrivateKeyFromPEM(k); err != nil { - return "", err - } - case *rsa.PrivateKey: - rsaKey = k - default: + // Validate type of key + if rsaKey, ok = key.(*rsa.PrivateKey); !ok { return "", ErrInvalidKey } diff --git a/rsa_test.go b/rsa_test.go index 13ba1fc..2e0f785 100644 --- a/rsa_test.go +++ b/rsa_test.go @@ -45,7 +45,8 @@ var rsaTestData = []struct { } func TestRSAVerify(t *testing.T) { - key, _ := ioutil.ReadFile("test/sample_key.pub") + keyData, _ := ioutil.ReadFile("test/sample_key.pub") + key, _ := jwt.ParseRSAPublicKeyFromPEM(keyData) for _, data := range rsaTestData { parts := strings.Split(data.tokenString, ".") @@ -62,7 +63,8 @@ func TestRSAVerify(t *testing.T) { } func TestRSASign(t *testing.T) { - key, _ := ioutil.ReadFile("test/sample_key") + keyData, _ := ioutil.ReadFile("test/sample_key") + key, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData) for _, data := range rsaTestData { if data.valid { From bcde77e55b4bbc68170f0426f93c0e49876fa0c2 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Mon, 20 Jul 2015 10:25:27 -0700 Subject: [PATCH 13/37] added rsa change to version history --- VERSION_HISTORY.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/VERSION_HISTORY.md b/VERSION_HISTORY.md index 5aa3b13..ac15a36 100644 --- a/VERSION_HISTORY.md +++ b/VERSION_HISTORY.md @@ -1,5 +1,9 @@ ## `jwt-go` Version History +#### 3.0.0 + +* Dropped support for `[]byte` keys when using RSA signing methods. This convenience feature could contribute to security vulnerabilities involving mismatched key types with signing methods. + #### 2.3.0 * Added support for ECDSA signing methods From 22cba446996f2de4449f6c0c18984d8c429c04c8 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Mon, 20 Jul 2015 14:38:26 -0300 Subject: [PATCH 14/37] Moving claim information into claims.go --- claims.go | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ jwt.go | 191 ----------------------------------------------------- 2 files changed, 193 insertions(+), 191 deletions(-) create mode 100644 claims.go diff --git a/claims.go b/claims.go new file mode 100644 index 0000000..a9bc533 --- /dev/null +++ b/claims.go @@ -0,0 +1,193 @@ +package jwt + +import "crypto/subtle" + +// For a type to be a Claims object, it must just have a Valid method that determines +// if the token is invalid for any supported reason +type Claims interface { + Valid() error +} + +// Structured version of Claims Section, as referenced at +// https://tools.ietf.org/html/rfc7519#section-4.1 +type StandardClaims struct { + Audience string `json:"aud,omitempty"` + ExpiresAt int64 `json:"exp,omitempty"` + Id string `json:"jti,omitempty"` + IssuedAt int64 `json:"iat,omitempty"` + Issuer string `json:"iss,omitempty"` + NotBefore int64 `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` +} + +// Validates time based claims "exp, iat, nbf". +// There is no accounting for clock skew. +// As well, if any of the above claims are not in the token, it will still +// be considered a valid claim. +func (c StandardClaims) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + // 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" + vErr.Errors |= ValidationErrorExpired + } + + if c.VerifyIssuedAt(now, false) == false { + vErr.err = "Token used before issued, clock skew issue?" + vErr.Errors |= ValidationErrorIssuedAt + } + + if c.VerifyNotBefore(now, false) == false { + vErr.err = "Token is not valid yet" + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} + +// Compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { + return verifyAud(c.Audience, cmp, req) +} + +// Compares the exp claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { + return verifyExp(c.ExpiresAt, cmp, req) +} + +// Compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { + return verifyIat(c.IssuedAt, cmp, req) +} + +// Compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool { + return verifyIss(c.Issuer, cmp, req) +} + +// Compares the nbf claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { + return verifyNbf(c.NotBefore, cmp, req) +} + +type MapClaim map[string]interface{} + +// Compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyAudience(cmp string, req bool) bool { + aud, _ := m["aud"].(string) + return verifyAud(aud, cmp, req) +} + +// Compares the exp claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyExpiresAt(cmp int64, req bool) bool { + exp, _ := m["exp"].(float64) + return verifyExp(int64(exp), cmp, req) +} + +// Compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyIssuedAt(cmp int64, req bool) bool { + iat, _ := m["iat"].(float64) + return verifyIat(int64(iat), cmp, req) +} + +// Compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyIssuer(cmp string, req bool) bool { + iss, _ := m["iss"].(string) + return verifyIss(iss, cmp, req) +} + +// Compares the nbf claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyNotBefore(cmp int64, req bool) bool { + nbf, _ := m["nbf"].(float64) + return verifyNbf(int64(nbf), cmp, req) +} + +// Validates time based claims "exp, iat, nbf". +// There is no accounting for clock skew. +// As well, if any of the above claims are not in the token, it will still +// be considered a valid claim. +func (m MapClaim) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + if m.VerifyExpiresAt(now, false) == false { + vErr.err = "Token is expired" + vErr.Errors |= ValidationErrorExpired + } + + if m.VerifyIssuedAt(now, false) == false { + vErr.err = "Token used before issued, clock skew issue?" + vErr.Errors |= ValidationErrorIssuedAt + } + + if m.VerifyNotBefore(now, false) == false { + vErr.err = "Token is not valid yet" + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} + +func verifyAud(aud string, cmp string, required bool) bool { + if aud == "" { + return !required + } + if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 { + return true + } else { + return false + } +} + +func verifyExp(exp int64, now int64, required bool) bool { + if exp == 0 { + return !required + } + return now <= exp +} + +func verifyIat(iat int64, now int64, required bool) bool { + if iat == 0 { + return !required + } + return now >= iat +} + +func verifyIss(iss string, cmp string, required bool) bool { + if iss == "" { + return !required + } + if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 { + return true + } else { + return false + } +} + +func verifyNbf(nbf int64, now int64, required bool) bool { + if nbf == 0 { + return !required + } + return now >= nbf +} diff --git a/jwt.go b/jwt.go index 53972c3..cfe2d6b 100644 --- a/jwt.go +++ b/jwt.go @@ -1,7 +1,6 @@ package jwt import ( - "crypto/subtle" "encoding/base64" "encoding/json" "net/http" @@ -20,196 +19,6 @@ var TimeFunc = time.Now // Header of the token (such as `kid`) to identify which key to use. type Keyfunc func(*Token) (interface{}, error) -// For a type to be a Claims object, it must just have a Valid method that determines -// if the token is invalid for any supported reason -type Claims interface { - Valid() error -} - -// Structured version of Claims Section, as referenced at -// https://tools.ietf.org/html/rfc7519#section-4.1 -type StandardClaims struct { - Audience string `json:"aud,omitempty"` - ExpiresAt int64 `json:"exp,omitempty"` - Id string `json:"jti,omitempty"` - IssuedAt int64 `json:"iat,omitempty"` - Issuer string `json:"iss,omitempty"` - NotBefore int64 `json:"nbf,omitempty"` - Subject string `json:"sub,omitempty"` -} - -// Validates time based claims "exp, iat, nbf". -// There is no accounting for clock skew. -// As well, if any of the above claims are not in the token, it will still -// be considered a valid claim. -func (c StandardClaims) Valid() error { - vErr := new(ValidationError) - now := TimeFunc().Unix() - - // 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" - vErr.Errors |= ValidationErrorExpired - } - - if c.VerifyIssuedAt(now, false) == false { - vErr.err = "Token used before issued, clock skew issue?" - vErr.Errors |= ValidationErrorIssuedAt - } - - if c.VerifyNotBefore(now, false) == false { - vErr.err = "Token is not valid yet" - vErr.Errors |= ValidationErrorNotValidYet - } - - if vErr.valid() { - return nil - } - - return vErr -} - -// Compares the aud claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { - return verifyAud(c.Audience, cmp, req) -} - -// Compares the exp claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { - return verifyExp(c.ExpiresAt, cmp, req) -} - -// Compares the iat claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { - return verifyIat(c.IssuedAt, cmp, req) -} - -// Compares the iss claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool { - return verifyIss(c.Issuer, cmp, req) -} - -// Compares the nbf claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { - return verifyNbf(c.NotBefore, cmp, req) -} - -type MapClaim map[string]interface{} - -// Compares the aud claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaim) VerifyAudience(cmp string, req bool) bool { - aud, _ := m["aud"].(string) - return verifyAud(aud, cmp, req) -} - -// Compares the exp claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaim) VerifyExpiresAt(cmp int64, req bool) bool { - exp, _ := m["exp"].(float64) - return verifyExp(int64(exp), cmp, req) -} - -// Compares the iat claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaim) VerifyIssuedAt(cmp int64, req bool) bool { - iat, _ := m["iat"].(float64) - return verifyIat(int64(iat), cmp, req) -} - -// Compares the iss claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaim) VerifyIssuer(cmp string, req bool) bool { - iss, _ := m["iss"].(string) - return verifyIss(iss, cmp, req) -} - -// Compares the nbf claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaim) VerifyNotBefore(cmp int64, req bool) bool { - nbf, _ := m["nbf"].(float64) - return verifyNbf(int64(nbf), cmp, req) -} - -// Validates time based claims "exp, iat, nbf". -// There is no accounting for clock skew. -// As well, if any of the above claims are not in the token, it will still -// be considered a valid claim. -func (m MapClaim) Valid() error { - vErr := new(ValidationError) - now := TimeFunc().Unix() - - if m.VerifyExpiresAt(now, false) == false { - vErr.err = "Token is expired" - vErr.Errors |= ValidationErrorExpired - } - - if m.VerifyIssuedAt(now, false) == false { - vErr.err = "Token used before issued, clock skew issue?" - vErr.Errors |= ValidationErrorIssuedAt - } - - if m.VerifyNotBefore(now, false) == false { - vErr.err = "Token is not valid yet" - vErr.Errors |= ValidationErrorNotValidYet - } - - if vErr.valid() { - return nil - } - - return vErr -} - -func verifyAud(aud string, cmp string, required bool) bool { - if aud == "" { - return !required - } - if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 { - return true - } else { - return false - } -} - -func verifyExp(exp int64, now int64, required bool) bool { - if exp == 0 { - return !required - } - return now <= exp -} - -func verifyIat(iat int64, now int64, required bool) bool { - if iat == 0 { - return !required - } - return now >= iat -} - -func verifyIss(iss string, cmp string, required bool) bool { - if iss == "" { - return !required - } - if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 { - return true - } else { - return false - } -} - -func verifyNbf(nbf int64, now int64, required bool) bool { - if nbf == 0 { - return !required - } - return now >= nbf -} - // A JWT Token. Different fields will be used depending on whether you're // creating or parsing/verifying a token. type Token struct { From aa6ac13a18a7ee039c2e2fa502c097919900782d Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Mon, 20 Jul 2015 14:45:40 -0300 Subject: [PATCH 15/37] Changed default "New" to use NewWithClaims internally. --- jwt.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/jwt.go b/jwt.go index cfe2d6b..b76c873 100644 --- a/jwt.go +++ b/jwt.go @@ -32,14 +32,7 @@ type Token struct { // Create a new Token. Takes a signing method func New(method SigningMethod) *Token { - return &Token{ - Header: map[string]interface{}{ - "typ": "JWT", - "alg": method.Alg(), - }, - Claims: MapClaim{}, - Method: method, - } + return NewWithClaims(method, MapClaim{}) } func NewWithClaims(method SigningMethod, claims Claims) *Token { From 6f536a0d2dd1fc9f74b6085bbfde825a78fc8614 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Wed, 22 Jul 2015 13:48:42 -0300 Subject: [PATCH 16/37] Changed example to use Output test. --- example_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/example_test.go b/example_test.go index 6ffa8c0..4621ea0 100644 --- a/example_test.go +++ b/example_test.go @@ -2,6 +2,7 @@ package jwt_test import ( "fmt" + "time" "github.com/dgrijalva/jwt-go" ) @@ -18,18 +19,17 @@ func ExampleParse(myToken string, myLookupKey func(interface{}) (interface{}, er } } -func ExampleNew(mySigningKey []byte) (string, error) { +func ExampleNew() { // Create the token token := jwt.New(jwt.SigningMethodRS256) // Set some claims claims := token.Claims.(jwt.MapClaim) claims["foo"] = "bar" - claims["exp"] = 15000 + claims["exp"] = time.Unix(0, 0).Add(time.Hour * 1).Unix() - // Sign and get the complete encoded token as a string - tokenString, err := token.SignedString(mySigningKey) - return tokenString, err + fmt.Printf("%v\n", claims) + //Output: map[foo:bar exp:3600] } func ExampleNewWithClaims(mySigningKey []byte) (string, error) { From ddfa84b39799771f1074c86f44a5c72e71347362 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Wed, 22 Jul 2015 13:53:37 -0300 Subject: [PATCH 17/37] Changed test to explicitly show that you can change the map without type asserting every call. --- example_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_test.go b/example_test.go index 4621ea0..224cd91 100644 --- a/example_test.go +++ b/example_test.go @@ -28,7 +28,7 @@ func ExampleNew() { claims["foo"] = "bar" claims["exp"] = time.Unix(0, 0).Add(time.Hour * 1).Unix() - fmt.Printf("%v\n", claims) + fmt.Printf("%v\n", token.Claims) //Output: map[foo:bar exp:3600] } From 2b0327edf60cd8e04d7ee6670b165c9580a42392 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Wed, 22 Jul 2015 13:47:56 -0700 Subject: [PATCH 18/37] added support for none, hopefully in a way you can't use accidentally --- none.go | 54 +++++++++++++++++++++++++++++++++++++++ none_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 none.go create mode 100644 none_test.go diff --git a/none.go b/none.go new file mode 100644 index 0000000..d697578 --- /dev/null +++ b/none.go @@ -0,0 +1,54 @@ +package jwt + +// Implements the none signing method. This is required by the spec +// but you probably should never use it. +var SigningMethodNone *signingMethodNone + +const UnsafeAllowNoneSignatureType unsafeNoneMagicConstant = "none signing method allowed" + +var NoneSignatureTypeDisallowedError error + +type signingMethodNone struct{} +type unsafeNoneMagicConstant string + +func init() { + SigningMethodNone = &signingMethodNone{} + NoneSignatureTypeDisallowedError = &ValidationError{ + "'none' signature type is not allowed", + ValidationErrorSignatureInvalid, + } + RegisterSigningMethod(SigningMethodNone.Alg(), func() SigningMethod { + return SigningMethodNone + }) +} + +func (m *signingMethodNone) Alg() string { + return "none" +} + +// Only allow 'none' alg type if UnsafeAllowNoneSignatureType is specified as the key +func (m *signingMethodNone) Verify(signingString, signature string, key interface{}) (err error) { + // Key must be UnsafeAllowNoneSignatureType to prevent accidentally + // accepting 'none' signing method + if _, ok := key.(unsafeNoneMagicConstant); !ok { + return NoneSignatureTypeDisallowedError + } + // If signing method is none, signature must be an empty string + if signature != "" { + return &ValidationError{ + "'none' signing method with non-empty signature", + ValidationErrorSignatureInvalid, + } + } + + // Accept 'none' signing method. + return nil +} + +// Only allow 'none' signing if UnsafeAllowNoneSignatureType is specified as the key +func (m *signingMethodNone) Sign(signingString string, key interface{}) (string, error) { + if _, ok := key.(unsafeNoneMagicConstant); ok { + return "", nil + } + return "", NoneSignatureTypeDisallowedError +} diff --git a/none_test.go b/none_test.go new file mode 100644 index 0000000..29a69ef --- /dev/null +++ b/none_test.go @@ -0,0 +1,72 @@ +package jwt_test + +import ( + "github.com/dgrijalva/jwt-go" + "strings" + "testing" +) + +var noneTestData = []struct { + name string + tokenString string + alg string + key interface{} + claims map[string]interface{} + valid bool +}{ + { + "Basic", + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.", + "none", + jwt.UnsafeAllowNoneSignatureType, + map[string]interface{}{"foo": "bar"}, + true, + }, + { + "Basic - no key", + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.", + "none", + nil, + map[string]interface{}{"foo": "bar"}, + false, + }, + { + "Signed", + "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.W-jEzRfBigtCWsinvVVuldiuilzVdU5ty0MvpLaSaqK9PlAWWlDQ1VIQ_qSKzwL5IXaZkvZFJXT3yL3n7OUVu7zCNJzdwznbC8Z-b0z2lYvcklJYi2VOFRcGbJtXUqgjk2oGsiqUMUMOLP70TTefkpsgqDxbRh9CDUfpOJgW-dU7cmgaoswe3wjUAUi6B6G2YEaiuXC0XScQYSYVKIzgKXJV8Zw-7AN_DBUI4GkTpsvQ9fVVjZM9csQiEXhYekyrKu1nu_POpQonGd8yqkIyXPECNmmqH5jH4sFiF67XhD7_JpkvLziBpI-uh86evBUadmHhb9Otqw3uV3NTaXLzJw", + "none", + jwt.UnsafeAllowNoneSignatureType, + map[string]interface{}{"foo": "bar"}, + false, + }, +} + +func TestNoneVerify(t *testing.T) { + for _, data := range noneTestData { + parts := strings.Split(data.tokenString, ".") + + method := jwt.GetSigningMethod(data.alg) + err := method.Verify(strings.Join(parts[0:2], "."), parts[2], data.key) + if data.valid && err != nil { + t.Errorf("[%v] Error while verifying key: %v", data.name, err) + } + if !data.valid && err == nil { + t.Errorf("[%v] Invalid key passed validation", data.name) + } + } +} + +func TestNoneSign(t *testing.T) { + for _, data := range noneTestData { + if data.valid { + parts := strings.Split(data.tokenString, ".") + method := jwt.GetSigningMethod(data.alg) + sig, err := method.Sign(strings.Join(parts[0:2], "."), data.key) + if err != nil { + t.Errorf("[%v] Error signing token: %v", data.name, err) + } + if sig != parts[2] { + t.Errorf("[%v] Incorrect signature.\nwas:\n%v\nexpecting:\n%v", data.name, sig, parts[2]) + } + } + } +} From a1fc9b87e687ea5a29fe8bae2538ca4b9b393805 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 18 Aug 2015 09:45:50 -0700 Subject: [PATCH 19/37] fixed ordering of map output in example test --- example_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example_test.go b/example_test.go index 224cd91..75f5309 100644 --- a/example_test.go +++ b/example_test.go @@ -28,8 +28,8 @@ func ExampleNew() { claims["foo"] = "bar" claims["exp"] = time.Unix(0, 0).Add(time.Hour * 1).Unix() - fmt.Printf("%v\n", token.Claims) - //Output: map[foo:bar exp:3600] + fmt.Printf("<%T> foo:%v exp:%v\n", token.Claims, claims["foo"], claims["exp"]) + //Output: foo:bar exp:3600 } func ExampleNewWithClaims(mySigningKey []byte) (string, error) { From dcda21cf2c15fc6fd9acd4ecb45c72efb80bfb09 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 18 Aug 2015 09:52:44 -0700 Subject: [PATCH 20/37] documentation update --- README.md | 105 +++++++++++++++++++++++---------------------- VERSION_HISTORY.md | 3 ++ 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 7907c0b..6b61bd4 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,59 @@ A [go](http://www.golang.org) (or 'golang' for search engine friendliness) imple **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. + +## What the heck is a JWT? + +In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way. + +The first part is called the header. It contains the necessary information for verifying the last part, the signature. For example, which encryption method was used for signing and what key was used. + +The part in the middle is the interesting bit. It's called the Claims and contains the actual stuff you care about. Refer to [the RFC](http://self-issued.info/docs/draft-jones-json-web-token.html) for information about reserved keys and the proper way to add your own. + +## What's in the box? + +This library supports the parsing and verification as well as the generation and signing of JWTs. Current supported signing algorithms are RSA256 and HMAC SHA256, though hooks are present for adding your own. + +## Parse and Verify + +Parsing and verifying tokens is pretty straight forward. You pass in the token and a function for looking up the key. This is done as a callback since you may need to parse the token to find out what signing method and key was used. + +```go + token, err := jwt.Parse(myToken, func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + return myLookupKey(token.Header["kid"]) + }) + + if err == nil && token.Valid { + deliverGoodness("!") + } else { + deliverUtterRejection(":(") + } +``` + +## Create a token + +```go + // Create the token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaim{ + "foo": "bar", + "exp": time.Now().Add(time.Hour * 72).Unix(), + }) + // Sign and get the complete encoded token as a string + tokenString, err := token.SignedString(mySigningKey) +``` + +## Project Status & Versioning + +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 good reason). + +This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases). + +While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v2`. It will do the right thing WRT semantic versioning. + ## Migration Guide from v2 -> v3 Added the ability to supply a typed object for the claims section of the token. @@ -61,58 +114,6 @@ New method usage: fmt.Println(claims.IssuedAt) ``` -## What the heck is a JWT? - -In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way. - -The first part is called the header. It contains the necessary information for verifying the last part, the signature. For example, which encryption method was used for signing and what key was used. - -The part in the middle is the interesting bit. It's called the Claims and contains the actual stuff you care about. Refer to [the RFC](http://self-issued.info/docs/draft-jones-json-web-token.html) for information about reserved keys and the proper way to add your own. - -## What's in the box? - -This library supports the parsing and verification as well as the generation and signing of JWTs. Current supported signing algorithms are RSA256 and HMAC SHA256, though hooks are present for adding your own. - -## Parse and Verify - -Parsing and verifying tokens is pretty straight forward. You pass in the token and a function for looking up the key. This is done as a callback since you may need to parse the token to find out what signing method and key was used. - -```go - token, err := jwt.Parse(myToken, func(token *jwt.Token) (interface{}, error) { - // Don't forget to validate the alg is what you expect: - if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) - } - return myLookupKey(token.Header["kid"]) - }) - - if err == nil && token.Valid { - deliverGoodness("!") - } else { - deliverUtterRejection(":(") - } -``` - -## Create a token - -```go - // Create the token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaim{ - "foo": "bar", - "exp": time.Now().Add(time.Hour * 72).Unix(), - }) - // Sign and get the complete encoded token as a string - tokenString, err := token.SignedString(mySigningKey) -``` - -## Project Status & Versioning - -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 good reason). - -This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases). - -While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v2`. It will do the right thing WRT semantic versioning. - ## More Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go). diff --git a/VERSION_HISTORY.md b/VERSION_HISTORY.md index ac15a36..25833ac 100644 --- a/VERSION_HISTORY.md +++ b/VERSION_HISTORY.md @@ -3,6 +3,9 @@ #### 3.0.0 * Dropped support for `[]byte` keys when using RSA signing methods. This convenience feature could contribute to security vulnerabilities involving mismatched key types with signing methods. +* Added `Claims` interface type to allow users to decode the claims into a custom type +* The `Claims` property on `Token` is now type `Claims` instead of `map[string]interface{}`. The default value is type `MapClaims`, which is an alias to `map[string]interface{}`. This makes it possible to use a custom type when decoding claims. +* Added `ParseWithClaims`, which takes a third argument of type `Claims`. Use this function instead of `Parse` if you have a custom type you'd like to decode into. #### 2.3.0 From e0b58f172443d8f2fa76c2b8f5cef2179f17b73b Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 18 Aug 2015 10:18:57 -0700 Subject: [PATCH 21/37] MapClaim -> MapClaims --- claims.go | 14 +++++++------- example_test.go | 4 ++-- jwt.go | 6 +++--- jwt_test.go | 24 ++++++++++++------------ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/claims.go b/claims.go index a9bc533..be1e79a 100644 --- a/claims.go +++ b/claims.go @@ -82,39 +82,39 @@ func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { return verifyNbf(c.NotBefore, cmp, req) } -type MapClaim map[string]interface{} +type MapClaims map[string]interface{} // Compares the aud claim against cmp. // If required is false, this method will return true if the value matches or is unset -func (m MapClaim) VerifyAudience(cmp string, req bool) bool { +func (m MapClaims) VerifyAudience(cmp string, req bool) bool { aud, _ := m["aud"].(string) return verifyAud(aud, cmp, req) } // Compares the exp claim against cmp. // If required is false, this method will return true if the value matches or is unset -func (m MapClaim) VerifyExpiresAt(cmp int64, req bool) bool { +func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { exp, _ := m["exp"].(float64) return verifyExp(int64(exp), cmp, req) } // Compares the iat claim against cmp. // If required is false, this method will return true if the value matches or is unset -func (m MapClaim) VerifyIssuedAt(cmp int64, req bool) bool { +func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { iat, _ := m["iat"].(float64) return verifyIat(int64(iat), cmp, req) } // Compares the iss claim against cmp. // If required is false, this method will return true if the value matches or is unset -func (m MapClaim) VerifyIssuer(cmp string, req bool) bool { +func (m MapClaims) VerifyIssuer(cmp string, req bool) bool { iss, _ := m["iss"].(string) return verifyIss(iss, cmp, req) } // Compares the nbf claim against cmp. // If required is false, this method will return true if the value matches or is unset -func (m MapClaim) VerifyNotBefore(cmp int64, req bool) bool { +func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool { nbf, _ := m["nbf"].(float64) return verifyNbf(int64(nbf), cmp, req) } @@ -123,7 +123,7 @@ func (m MapClaim) VerifyNotBefore(cmp int64, req bool) bool { // There is no accounting for clock skew. // As well, if any of the above claims are not in the token, it will still // be considered a valid claim. -func (m MapClaim) Valid() error { +func (m MapClaims) Valid() error { vErr := new(ValidationError) now := TimeFunc().Unix() diff --git a/example_test.go b/example_test.go index 75f5309..9bc8d4b 100644 --- a/example_test.go +++ b/example_test.go @@ -24,12 +24,12 @@ func ExampleNew() { token := jwt.New(jwt.SigningMethodRS256) // Set some claims - claims := token.Claims.(jwt.MapClaim) + claims := token.Claims.(jwt.MapClaims) claims["foo"] = "bar" claims["exp"] = time.Unix(0, 0).Add(time.Hour * 1).Unix() fmt.Printf("<%T> foo:%v exp:%v\n", token.Claims, claims["foo"], claims["exp"]) - //Output: foo:bar exp:3600 + //Output: foo:bar exp:3600 } func ExampleNewWithClaims(mySigningKey []byte) (string, error) { diff --git a/jwt.go b/jwt.go index b76c873..717a44e 100644 --- a/jwt.go +++ b/jwt.go @@ -32,7 +32,7 @@ type Token struct { // Create a new Token. Takes a signing method func New(method SigningMethod) *Token { - return NewWithClaims(method, MapClaim{}) + return NewWithClaims(method, MapClaims{}) } func NewWithClaims(method SigningMethod, claims Claims) *Token { @@ -87,7 +87,7 @@ func (t *Token) SigningString() (string, error) { // keyFunc will receive the parsed token and should return the key for validating. // If everything is kosher, err will be nil func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { - return ParseWithClaims(tokenString, keyFunc, &MapClaim{}) + return ParseWithClaims(tokenString, keyFunc, &MapClaims{}) } func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token, error) { @@ -176,7 +176,7 @@ func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token // Currently, it looks in the Authorization header as well as // looking for an 'access_token' request parameter in req.Form. func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err error) { - return ParseFromRequestWithClaims(req, keyFunc, &MapClaim{}) + return ParseFromRequestWithClaims(req, keyFunc, &MapClaims{}) } func ParseFromRequestWithClaims(req *http.Request, keyFunc Keyfunc, claims Claims) (token *Token, err error) { diff --git a/jwt_test.go b/jwt_test.go index 43d0d25..66c148a 100644 --- a/jwt_test.go +++ b/jwt_test.go @@ -24,7 +24,7 @@ var jwtTestData = []struct { name string tokenString string keyfunc jwt.Keyfunc - claims jwt.MapClaim + claims jwt.MapClaims valid bool errors uint32 }{ @@ -32,7 +32,7 @@ var jwtTestData = []struct { "basic", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", defaultKeyFunc, - jwt.MapClaim{"foo": "bar"}, + jwt.MapClaims{"foo": "bar"}, true, 0, }, @@ -40,7 +40,7 @@ var jwtTestData = []struct { "basic expired", "", // autogen defaultKeyFunc, - jwt.MapClaim{"foo": "bar", "exp": float64(time.Now().Unix() - 100)}, + jwt.MapClaims{"foo": "bar", "exp": float64(time.Now().Unix() - 100)}, false, jwt.ValidationErrorExpired, }, @@ -48,7 +48,7 @@ var jwtTestData = []struct { "basic nbf", "", // autogen defaultKeyFunc, - jwt.MapClaim{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)}, + jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)}, false, jwt.ValidationErrorNotValidYet, }, @@ -56,7 +56,7 @@ var jwtTestData = []struct { "expired and nbf", "", // autogen defaultKeyFunc, - jwt.MapClaim{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)}, + jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)}, false, jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired, }, @@ -64,7 +64,7 @@ var jwtTestData = []struct { "basic invalid", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", defaultKeyFunc, - jwt.MapClaim{"foo": "bar"}, + jwt.MapClaims{"foo": "bar"}, false, jwt.ValidationErrorSignatureInvalid, }, @@ -72,7 +72,7 @@ var jwtTestData = []struct { "basic nokeyfunc", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", nilKeyFunc, - jwt.MapClaim{"foo": "bar"}, + jwt.MapClaims{"foo": "bar"}, false, jwt.ValidationErrorUnverifiable, }, @@ -80,7 +80,7 @@ var jwtTestData = []struct { "basic nokey", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", emptyKeyFunc, - jwt.MapClaim{"foo": "bar"}, + jwt.MapClaims{"foo": "bar"}, false, jwt.ValidationErrorSignatureInvalid, }, @@ -88,7 +88,7 @@ var jwtTestData = []struct { "basic errorkey", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", errorKeyFunc, - jwt.MapClaim{"foo": "bar"}, + jwt.MapClaims{"foo": "bar"}, false, jwt.ValidationErrorUnverifiable, }, @@ -104,7 +104,7 @@ func init() { } } -func makeSample(c jwt.MapClaim) string { +func makeSample(c jwt.MapClaims) string { keyData, e := ioutil.ReadFile("test/sample_key") if e != nil { panic(e.Error()) @@ -130,7 +130,7 @@ func TestJWT(t *testing.T) { data.tokenString = makeSample(data.claims) } - token, err := jwt.ParseWithClaims(data.tokenString, data.keyfunc, &jwt.MapClaim{}) + token, err := jwt.ParseWithClaims(data.tokenString, data.keyfunc, &jwt.MapClaims{}) if !reflect.DeepEqual(&data.claims, token.Claims) { t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims) @@ -167,7 +167,7 @@ func TestParseRequest(t *testing.T) { r, _ := http.NewRequest("GET", "/", nil) r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", data.tokenString)) - token, err := jwt.ParseFromRequestWithClaims(r, data.keyfunc, &jwt.MapClaim{}) + token, err := jwt.ParseFromRequestWithClaims(r, data.keyfunc, &jwt.MapClaims{}) if token == nil { t.Errorf("[%v] Token was not found: %v", data.name, err) From e4a56deada5a16d88f6fa9268f6668e7e05e0651 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 18 Aug 2015 10:58:37 -0700 Subject: [PATCH 22/37] don't need that --- jwt.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jwt.go b/jwt.go index 717a44e..ed1b00b 100644 --- a/jwt.go +++ b/jwt.go @@ -87,7 +87,7 @@ func (t *Token) SigningString() (string, error) { // keyFunc will receive the parsed token and should return the key for validating. // If everything is kosher, err will be nil func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { - return ParseWithClaims(tokenString, keyFunc, &MapClaims{}) + return ParseWithClaims(tokenString, keyFunc, MapClaims{}) } func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token, error) { @@ -176,7 +176,7 @@ func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token // Currently, it looks in the Authorization header as well as // looking for an 'access_token' request parameter in req.Form. func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err error) { - return ParseFromRequestWithClaims(req, keyFunc, &MapClaims{}) + return ParseFromRequestWithClaims(req, keyFunc, MapClaims{}) } func ParseFromRequestWithClaims(req *http.Request, keyFunc Keyfunc, claims Claims) (token *Token, err error) { From 69c78c36528b07818ae9f92ac2a60e456fcc73f0 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 18 Aug 2015 13:03:45 -0700 Subject: [PATCH 23/37] missed one in the rename --- cmd/jwt/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/jwt/app.go b/cmd/jwt/app.go index f4d51e8..e8225dc 100644 --- a/cmd/jwt/app.go +++ b/cmd/jwt/app.go @@ -155,7 +155,7 @@ func signToken() error { } // parse the JSON of the claims - var claims jwt.MapClaim + var claims jwt.MapClaims if err := json.Unmarshal(tokData, &claims); err != nil { return fmt.Errorf("Couldn't parse claims JSON: %v", err) } From 9c00ec7ce73b11dff249367736095a3608f2c442 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 18 Aug 2015 13:28:52 -0700 Subject: [PATCH 24/37] more/simpler examples --- example_test.go | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/example_test.go b/example_test.go index 9bc8d4b..4321f14 100644 --- a/example_test.go +++ b/example_test.go @@ -32,15 +32,42 @@ func ExampleNew() { //Output: foo:bar exp:3600 } -func ExampleNewWithClaims(mySigningKey []byte) (string, error) { +func ExampleNewWithClaims() { + mySigningKey := []byte("AllYourBase") + // Create the Claims claims := jwt.StandardClaims{ ExpiresAt: 15000, Issuer: "test", } - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - return token.SignedString(mySigningKey) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + ss, err := token.SignedString(mySigningKey) + fmt.Printf("%v %v", ss, err) + //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.QsODzZu3lUZMVdhbO76u3Jv02iYCvEHcYVUI1kOWEU0 +} + +func ExampleNewWithClaims_customType() { + mySigningKey := []byte("AllYourBase") + + type MyCustomClaims struct { + Foo string `json:"foo"` + jwt.StandardClaims + } + + // Create the Claims + claims := MyCustomClaims{ + "bar", + jwt.StandardClaims{ + ExpiresAt: 15000, + Issuer: "test", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + ss, err := token.SignedString(mySigningKey) + fmt.Printf("%v %v", ss, err) + //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c } func ExampleParse_errorChecking(myToken string, myLookupKey func(interface{}) (interface{}, error)) { From eeccfeb20d40c080b101b8ad13f7bf20afe539f8 Mon Sep 17 00:00:00 2001 From: wizzie Date: Thu, 1 Oct 2015 20:16:16 +0200 Subject: [PATCH 25/37] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6b61bd4..0245f17 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Parsing and verifying tokens is pretty straight forward. You pass in the token ```go // Create the token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaim{ + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "foo": "bar", "exp": time.Now().Add(time.Hour * 72).Unix(), }) @@ -62,7 +62,7 @@ While we try to make it obvious when we make breaking changes, there isn't a gre Added the ability to supply a typed object for the claims section of the token. Unfortunately this requires a breaking change. A few new methods were added to support this, -and the old default of `map[string]interface{}` was changed to `jwt.MapClaim`. +and the old default of `map[string]interface{}` was changed to `jwt.MapClaims`. The old example for creating a token looked like this.. @@ -76,7 +76,7 @@ is now directly mapped to... ```go token := jwt.New(jwt.SigningMethodHS256) - claims := token.Claims.(jwt.MapClaim) + claims := token.Claims.(jwt.MapClaims) claims["foo"] = "bar" claims["exp"] = time.Now().Add(time.Hour * 72).Unix() ``` From b863883b96690cf7ab0974ab7f48b3ae51524878 Mon Sep 17 00:00:00 2001 From: Snorre lothar von Gohren Edwin Date: Sat, 19 Dec 2015 23:49:37 +0100 Subject: [PATCH 26/37] token.go: did some changes to the checks so that it will give better error feedback for noobs who write the authorization bearer value wrong --- token.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/token.go b/token.go index d35aaa4..4751db7 100644 --- a/token.go +++ b/token.go @@ -84,6 +84,9 @@ func (t *Token) SigningString() (string, error) { // keyFunc will receive the parsed token and should return the key for validating. // If everything is kosher, err will be nil func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { + if strings.Contains(strings.ToLower(tokenString), "bearer") { + return &ValidationError{err: "tokenstring should not contain bearer", Errors: ValidationErrorMalformed} + } return new(Parser).Parse(tokenString, keyFunc) } @@ -94,9 +97,10 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err error) { // Look for an Authorization header + _ = "breakpoint" if ah := req.Header.Get("Authorization"); ah != "" { // Should be a bearer token - if len(ah) > 6 && strings.ToUpper(ah[0:6]) == "BEARER" { + if len(ah) > 6 && strings.ToUpper(ah[0:7]) == "BEARER " { return Parse(ah[7:], keyFunc) } } From 5d11392aac6e6e2aa29e65a5549c22246e4e63f8 Mon Sep 17 00:00:00 2001 From: Snorre lothar von Gohren Edwin Date: Sat, 19 Dec 2015 23:58:27 +0100 Subject: [PATCH 27/37] no breakpoints --- token.go | 1 - 1 file changed, 1 deletion(-) diff --git a/token.go b/token.go index 4751db7..1bca664 100644 --- a/token.go +++ b/token.go @@ -97,7 +97,6 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err error) { // Look for an Authorization header - _ = "breakpoint" if ah := req.Header.Get("Authorization"); ah != "" { // Should be a bearer token if len(ah) > 6 && strings.ToUpper(ah[0:7]) == "BEARER " { From 1f970af1f874086e7c4482b7ec92f9866c898280 Mon Sep 17 00:00:00 2001 From: Snorre lothar von Gohren Edwin Date: Sun, 20 Dec 2015 09:25:50 +0100 Subject: [PATCH 28/37] added right amount of return --- token.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token.go b/token.go index 1bca664..e15ce82 100644 --- a/token.go +++ b/token.go @@ -85,7 +85,7 @@ func (t *Token) SigningString() (string, error) { // If everything is kosher, err will be nil func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { if strings.Contains(strings.ToLower(tokenString), "bearer") { - return &ValidationError{err: "tokenstring should not contain bearer", Errors: ValidationErrorMalformed} + return nil, &ValidationError{err: "tokenstring should not contain bearer", Errors: ValidationErrorMalformed} } return new(Parser).Parse(tokenString, keyFunc) } From 57b1269c416b235474f6481bbe89858279522888 Mon Sep 17 00:00:00 2001 From: Snorre lothar von Gohren Edwin Date: Tue, 22 Dec 2015 15:30:57 +0100 Subject: [PATCH 29/37] modifications on PR. Added a space in the bearer string check so that we unexpectly dont experience an base64url encoding because bearer is technically part of a valid endcoding, we think. Also moved it into a failed decoding to get a better feedback for the developer, but not do unessecary amount of string checks --- parser.go | 3 +++ token.go | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/parser.go b/parser.go index 3fc27bf..1659ad2 100644 --- a/parser.go +++ b/parser.go @@ -26,6 +26,9 @@ func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { // parse Header var headerBytes []byte if headerBytes, err = DecodeSegment(parts[0]); err != nil { + if strings.Contains(strings.ToLower(tokenString), "bearer ") { + return token, &ValidationError{err: "tokenstring should not contain bearer", Errors: ValidationErrorMalformed} + } return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed} } if err = json.Unmarshal(headerBytes, &token.Header); err != nil { diff --git a/token.go b/token.go index e15ce82..1cf267d 100644 --- a/token.go +++ b/token.go @@ -84,9 +84,6 @@ func (t *Token) SigningString() (string, error) { // keyFunc will receive the parsed token and should return the key for validating. // If everything is kosher, err will be nil func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { - if strings.Contains(strings.ToLower(tokenString), "bearer") { - return nil, &ValidationError{err: "tokenstring should not contain bearer", Errors: ValidationErrorMalformed} - } return new(Parser).Parse(tokenString, keyFunc) } From b47bdbc660b24298ab475498ab94f387c60cf373 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 22 Dec 2015 13:27:59 -0800 Subject: [PATCH 30/37] Added some clarification and (hopefully) helpful documentation --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 001c0a3..8c83780 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,35 @@ This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v2`. It will do the right thing WRT semantic versioning. +## Usage Tips + +### Signing vs Encryption + +A token is simply a JSON object that is signed by its author. this tells you exactly two things about the data: + +* The author of the token was in the possession of the signing secret +* The data has not been modified since it was signed + +It's important to know that JWT does not provide encryption, which means anyone who has access to the token can read its contents. If you need to protect (encrypt) the data, there is a companion spec, `JWE`, that provides this functionality. JWE is currently outside the scope of this library. + +### Choosing a Signing Method + +There are several signing methods available, and you should probably take the time to learn about the various options before choosing one. The principal design decision is most likely going to be symmetric vs asymmetric. + +Symmetric signing methods, such as HSA, use only a single secret. This is probably the simplest signing method to use since any `[]byte` can be used as a valid secret. They are also slightly computationally faster to use, though this rarely is enough to matter. Symmetric signing methods work the best when both producers and consumers of tokens are trusted, or even the same system. Since the same secret is used to both sign and validate tokens, you can't easily distribute the key for validation. + +Asymmetric signing methods, such as RSA, use different keys for signing and verifying tokens. This makes it possible to produce tokens with a private key, and allow any consumer to access the public key for verification. + +### JWT and OAuth + +It's worth mentioning that OAuth and JWT are not the same thing. A JWT token is simply a signed JSON object. It can be used anywhere such a thing is useful. There is some confusion, though, as JWT is the most common type of bearer token used in OAuth2 authentication. + +Without going too far down the rabbit hole, here's a description of the interaction of these technologies: + +* OAuth is a protocol for allowing an identity provider to be separate from the service a user is logging in to. For example, whenever you use Facebook to log into a different service (Yelp, Spotify, etc), you are using OAuth. +* OAuth defines several options for passing around authentication data. One popular method is called a "bearer token". A bearer token is simply a string that _should_ only be held by an authenticated user. Thus, simply presenting this token proves your identity. You can probably derive from here why a JWT might make a good bearer token. +* Because bearer tokens are used for authentication, it's important they're kept secret. This is why transactions that use bearer tokens typically happen over SSL. + ## More Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go). From 2240de772c17d0e303c9bbc04bca67ffdfef32c4 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 22 Dec 2015 13:53:19 -0800 Subject: [PATCH 31/37] added supported signing methods --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c83780..6be8590 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The part in the middle is the interesting bit. It's called the Claims and conta ## What's in the box? -This library supports the parsing and verification as well as the generation and signing of JWTs. Current supported signing algorithms are RSA256 and HMAC SHA256, though hooks are present for adding your own. +This library supports the parsing and verification as well as the generation and signing of JWTs. Current supported signing algorithms are HMAC SHA, RSA, RSA-PSS, and ECDSA, though hooks are present for adding your own. ## Parse and Verify From ca46641b15f1c6132c47599e44af5851fd684e4f Mon Sep 17 00:00:00 2001 From: Snorre lothar von Gohren Edwin Date: Wed, 23 Dec 2015 09:43:00 +0100 Subject: [PATCH 32/37] PR updated, faster string method and more reasonable message feedback --- parser.go | 4 ++-- token.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/parser.go b/parser.go index 1659ad2..a078404 100644 --- a/parser.go +++ b/parser.go @@ -26,8 +26,8 @@ func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { // parse Header var headerBytes []byte if headerBytes, err = DecodeSegment(parts[0]); err != nil { - if strings.Contains(strings.ToLower(tokenString), "bearer ") { - return token, &ValidationError{err: "tokenstring should not contain bearer", Errors: ValidationErrorMalformed} + if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") { + return token, &ValidationError{err: "tokenstring should not contain 'bearer '", Errors: ValidationErrorMalformed} } return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed} } diff --git a/token.go b/token.go index 1cf267d..fde0116 100644 --- a/token.go +++ b/token.go @@ -97,7 +97,7 @@ func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err err if ah := req.Header.Get("Authorization"); ah != "" { // Should be a bearer token if len(ah) > 6 && strings.ToUpper(ah[0:7]) == "BEARER " { - return Parse(ah[7:], keyFunc) + return Parse(ah, keyFunc) } } From fea509ebfec2fd8f0b61fa9a0df05695a830d78b Mon Sep 17 00:00:00 2001 From: Snorre lothar von Gohren Edwin Date: Wed, 23 Dec 2015 09:45:17 +0100 Subject: [PATCH 33/37] pushed a test change --- token.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token.go b/token.go index fde0116..1cf267d 100644 --- a/token.go +++ b/token.go @@ -97,7 +97,7 @@ func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err err if ah := req.Header.Get("Authorization"); ah != "" { // Should be a bearer token if len(ah) > 6 && strings.ToUpper(ah[0:7]) == "BEARER " { - return Parse(ah, keyFunc) + return Parse(ah[7:], keyFunc) } } From 574cd7c2458b12e41ec4bff5b79b6eb9fa22b914 Mon Sep 17 00:00:00 2001 From: matm Date: Wed, 30 Dec 2015 18:50:00 +0100 Subject: [PATCH 34/37] README: fix return arguments --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6be8590..bf0100f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Parsing and verifying tokens is pretty straight forward. You pass in the token if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } - return myLookupKey(token.Header["kid"]) + return myLookupKey(token.Header["kid"]), nil }) if err == nil && token.Valid { From 9a4b9f2ac1f7685147a5c01544e3b922dbc1f4a0 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Mon, 7 Mar 2016 15:28:38 -0800 Subject: [PATCH 35/37] add 1.6 to travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d608914..5d3b205 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,5 @@ go: - 1.3.3 - 1.4.2 - 1.5 + - 1.6 - tip From b7d25eabfbee3d330c9893172d8195e0b76d2939 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Tue, 22 Mar 2016 09:07:45 +0800 Subject: [PATCH 36/37] Test latest 1.4.x and 1.3.x version. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5d3b205..bde823d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: go go: - - 1.3.3 - - 1.4.2 + - 1.3 + - 1.4 - 1.5 - 1.6 - tip From b446e078d613dc395bf50b08884acbeafb19ce50 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Thu, 31 Mar 2016 11:13:52 -0700 Subject: [PATCH 37/37] version history update --- VERSION_HISTORY.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/VERSION_HISTORY.md b/VERSION_HISTORY.md index 9eb7ff9..14cf1a8 100644 --- a/VERSION_HISTORY.md +++ b/VERSION_HISTORY.md @@ -1,5 +1,13 @@ ## `jwt-go` Version History +#### 2.5.0 + +This will likely be the last backwards compatible release before 3.0.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 ` + #### 2.4.0 * Added new type, Parser, to allow for configuration of various parsing parameters