From 94cd619027da432a792f92887ea66261e1f9e41d Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Mon, 13 Jul 2015 16:14:12 -0300 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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 22cba446996f2de4449f6c0c18984d8c429c04c8 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Mon, 20 Jul 2015 14:38:26 -0300 Subject: [PATCH 12/15] 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 13/15] 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 14/15] 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 15/15] 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] }