From 94cd619027da432a792f92887ea66261e1f9e41d Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Mon, 13 Jul 2015 16:14:12 -0300 Subject: [PATCH 01/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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 a1fc9b87e687ea5a29fe8bae2538ca4b9b393805 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 18 Aug 2015 09:45:50 -0700 Subject: [PATCH 18/51] 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 19/51] 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 20/51] 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 21/51] 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 22/51] 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 23/51] 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 24/51] 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 f7288992d27b8f629462eb45c8b9dd3e9face204 Mon Sep 17 00:00:00 2001 From: Martin Lindhe Date: Tue, 29 Dec 2015 22:51:54 +0100 Subject: [PATCH 25/51] split ErrInvalidKey into ErrInvalidType and ErrInvalidKey --- ecdsa.go | 4 ++-- errors.go | 3 ++- hmac.go | 2 +- rsa.go | 2 +- rsa_pss.go | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ecdsa.go b/ecdsa.go index 0518ed1..ed131ef 100644 --- a/ecdsa.go +++ b/ecdsa.go @@ -69,7 +69,7 @@ func (m *SigningMethodECDSA) Verify(signingString, signature string, key interfa case *ecdsa.PublicKey: ecdsaKey = k default: - return ErrInvalidKey + return ErrInvalidType } if len(sig) != 2*m.KeySize { @@ -103,7 +103,7 @@ func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) (string case *ecdsa.PrivateKey: ecdsaKey = k default: - return "", ErrInvalidKey + return "", ErrInvalidType } // Create the hasher diff --git a/errors.go b/errors.go index e9e788f..1dabcbd 100644 --- a/errors.go +++ b/errors.go @@ -6,7 +6,8 @@ import ( // Error constants var ( - ErrInvalidKey = errors.New("key is invalid or of invalid type") + ErrInvalidKey = errors.New("key is invalid") + ErrInvalidType = errors.New("key is of invalid type") ErrHashUnavailable = errors.New("the requested hash function is unavailable") ErrNoTokenInRequest = errors.New("no token present in request") ) diff --git a/hmac.go b/hmac.go index 192e625..ed77b7b 100644 --- a/hmac.go +++ b/hmac.go @@ -49,7 +49,7 @@ func (m *SigningMethodHMAC) Verify(signingString, signature string, key interfac // Verify the key is the right type keyBytes, ok := key.([]byte) if !ok { - return ErrInvalidKey + return ErrInvalidType } // Decode signature, for comparison diff --git a/rsa.go b/rsa.go index cddffce..4db0b49 100644 --- a/rsa.go +++ b/rsa.go @@ -65,7 +65,7 @@ func (m *SigningMethodRSA) Verify(signingString, signature string, key interface case *rsa.PublicKey: rsaKey = k default: - return ErrInvalidKey + return ErrInvalidType } // Create hasher diff --git a/rsa_pss.go b/rsa_pss.go index b5b7073..5f35b37 100644 --- a/rsa_pss.go +++ b/rsa_pss.go @@ -106,7 +106,7 @@ func (m *SigningMethodRSAPSS) Sign(signingString string, key interface{}) (strin case *rsa.PrivateKey: rsaKey = k default: - return "", ErrInvalidKey + return "", ErrInvalidType } // Create the hasher From e0e3b433f52c711b97710c859142dafafa88df9f Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Thu, 14 Jan 2016 14:09:27 -0800 Subject: [PATCH 26/51] WIP on migrating request parsing stuff --- parser_test.go | 33 --------------------------------- request/request.go | 30 ++++++++++++++++++++++++++++++ request/request_test.go | 34 ++++++++++++++++++++++++++++++++++ token.go | 25 ------------------------- 4 files changed, 64 insertions(+), 58 deletions(-) create mode 100644 request/request.go create mode 100644 request/request_test.go diff --git a/parser_test.go b/parser_test.go index 9115017..0d7f229 100644 --- a/parser_test.go +++ b/parser_test.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/dgrijalva/jwt-go" "io/ioutil" - "net/http" "reflect" "testing" "time" @@ -192,38 +191,6 @@ func TestParser_Parse(t *testing.T) { } } -func TestParseRequest(t *testing.T) { - // Bearer token request - for _, data := range jwtTestData { - // FIXME: custom parsers are not supported by this helper. skip tests that require them - if data.parser != nil { - t.Logf("Skipping [%v]. Custom parsers are not supported by ParseRequest", data.name) - continue - } - - if data.tokenString == "" { - data.tokenString = makeSample(data.claims) - } - - r, _ := http.NewRequest("GET", "/", nil) - r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", data.tokenString)) - token, err := jwt.ParseFromRequest(r, data.keyfunc) - - if token == nil { - t.Errorf("[%v] Token was not found: %v", data.name, err) - continue - } - 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: %v", data.name, err) - } - if !data.valid && err == nil { - t.Errorf("[%v] Invalid token passed validation", data.name) - } - } -} // Helper method for benchmarking various methods func benchmarkSigning(b *testing.B, method jwt.SigningMethod, key interface{}) { diff --git a/request/request.go b/request/request.go new file mode 100644 index 0000000..33fc5fb --- /dev/null +++ b/request/request.go @@ -0,0 +1,30 @@ +package request + +import ( + "github.com/dgrijalva/jwt-go" + "strings" + "net/http" +) + +// Try to find the token in an http.Request. +// This method will call ParseMultipartForm if there's no token in the header. +// 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 jwt.Keyfunc) (token *jwt.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 jwt.Parse(ah[7:], keyFunc) + } + } + + // Look for "access_token" parameter + req.ParseMultipartForm(10e6) + if tokStr := req.Form.Get("access_token"); tokStr != "" { + return jwt.Parse(tokStr, keyFunc) + } + + return nil, jwt.ErrNoTokenInRequest +} diff --git a/request/request_test.go b/request/request_test.go new file mode 100644 index 0000000..6e6ff6c --- /dev/null +++ b/request/request_test.go @@ -0,0 +1,34 @@ +package request + +// func TestParseRequest(t *testing.T) { +// // Bearer token request +// for _, data := range jwtTestData { +// // FIXME: custom parsers are not supported by this helper. skip tests that require them +// if data.parser != nil { +// t.Logf("Skipping [%v]. Custom parsers are not supported by ParseRequest", data.name) +// continue +// } +// +// if data.tokenString == "" { +// data.tokenString = makeSample(data.claims) +// } +// +// r, _ := http.NewRequest("GET", "/", nil) +// r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", data.tokenString)) +// token, err := jwt.ParseFromRequest(r, data.keyfunc) +// +// if token == nil { +// t.Errorf("[%v] Token was not found: %v", data.name, err) +// continue +// } +// 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: %v", data.name, err) +// } +// if !data.valid && err == nil { +// t.Errorf("[%v] Invalid token passed validation", data.name) +// } +// } +// } diff --git a/token.go b/token.go index d35aaa4..9cd59bf 100644 --- a/token.go +++ b/token.go @@ -3,7 +3,6 @@ package jwt import ( "encoding/base64" "encoding/json" - "net/http" "strings" "time" ) @@ -87,30 +86,6 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { return new(Parser).Parse(tokenString, keyFunc) } -// Try to find the token in an http.Request. -// This method will call ParseMultipartForm if there's no token in the header. -// 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) { - - // 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) - } - } - - // Look for "access_token" parameter - req.ParseMultipartForm(10e6) - if tokStr := req.Form.Get("access_token"); tokStr != "" { - return Parse(tokStr, keyFunc) - } - - return nil, ErrNoTokenInRequest - -} - // Encode JWT specific base64url encoding with padding stripped func EncodeSegment(seg []byte) string { return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=") From e3cd52238a5f930e5ed1dd453153b6a403f92ca4 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Fri, 8 Apr 2016 13:01:55 -0700 Subject: [PATCH 27/51] moved some of the test helpers into a shared location so they can be reused --- parser_test.go | 40 ++++++++-------------------------------- test/helpers.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 32 deletions(-) create mode 100644 test/helpers.go diff --git a/parser_test.go b/parser_test.go index 58efa37..11cc82c 100644 --- a/parser_test.go +++ b/parser_test.go @@ -4,12 +4,12 @@ import ( "crypto/rsa" "encoding/json" "fmt" - "io/ioutil" "reflect" "testing" "time" "github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go/test" ) var ( @@ -20,6 +20,10 @@ var ( nilKeyFunc jwt.Keyfunc = nil ) +func init() { + jwtTestDefaultKey = test.LoadRSAPublicKeyFromDisk("test/sample_key.pub") +} + var jwtTestData = []struct { name string tokenString string @@ -130,40 +134,12 @@ var jwtTestData = []struct { }, } -func init() { - 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 jwt.MapClaims) string { - keyData, e := ioutil.ReadFile("test/sample_key") - if e != nil { - panic(e.Error()) - } - key, e := jwt.ParseRSAPrivateKeyFromPEM(keyData) - if e != nil { - panic(e.Error()) - } - - token := jwt.NewWithClaims(jwt.SigningMethodRS256, c) - s, e := token.SignedString(key) - - if e != nil { - panic(e.Error()) - } - - return s -} - func TestParser_Parse(t *testing.T) { + privateKey := test.LoadRSAPrivateKeyFromDisk("test/sample_key") + for _, data := range jwtTestData { if data.tokenString == "" { - data.tokenString = makeSample(data.claims) + data.tokenString = test.MakeSampleToken(data.claims, privateKey) } var token *jwt.Token diff --git a/test/helpers.go b/test/helpers.go new file mode 100644 index 0000000..39b5208 --- /dev/null +++ b/test/helpers.go @@ -0,0 +1,42 @@ +package test + +import ( + "crypto/rsa" + "github.com/dgrijalva/jwt-go" + "io/ioutil" +) + +func LoadRSAPrivateKeyFromDisk(location string) *rsa.PrivateKey { + keyData, e := ioutil.ReadFile(location) + if e != nil { + panic(e.Error()) + } + key, e := jwt.ParseRSAPrivateKeyFromPEM(keyData) + if e != nil { + panic(e.Error()) + } + return key +} + +func LoadRSAPublicKeyFromDisk(location string) *rsa.PublicKey { + keyData, e := ioutil.ReadFile(location) + if e != nil { + panic(e.Error()) + } + key, e := jwt.ParseRSAPublicKeyFromPEM(keyData) + if e != nil { + panic(e.Error()) + } + return key +} + +func MakeSampleToken(c jwt.MapClaims, key interface{}) string { + token := jwt.NewWithClaims(jwt.SigningMethodRS256, c) + s, e := token.SignedString(key) + + if e != nil { + panic(e.Error()) + } + + return s +} From 288cee4336ae43c0f95d491a273714a261c4c319 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Fri, 8 Apr 2016 13:02:18 -0700 Subject: [PATCH 28/51] added a couple tests for request parsing that don't rely on giant parser test dataset --- request/request_test.go | 122 +++++++++++++++++++++++++++------------- 1 file changed, 82 insertions(+), 40 deletions(-) diff --git a/request/request_test.go b/request/request_test.go index 311f8e9..8306912 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -1,42 +1,84 @@ package request -// import ( -// "fmt" -// "github.com/dgrijalva/jwt-go" -// "net/http" -// "reflect" -// "testing" -// ) -// -// func TestParseRequest(t *testing.T) { -// // Bearer token request -// for _, data := range jwtTestData { -// // FIXME: custom parsers are not supported by this helper. skip tests that require them -// if data.parser != nil { -// t.Logf("Skipping [%v]. Custom parsers are not supported by ParseRequest", data.name) -// continue -// } -// -// if data.tokenString == "" { -// data.tokenString = makeSample(data.claims) -// } -// -// r, _ := http.NewRequest("GET", "/", nil) -// r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", data.tokenString)) -// token, err := ParseFromRequestWithClaims(r, data.keyfunc, &jwt.MapClaims{}) -// -// if token == nil { -// t.Errorf("[%v] Token was not found: %v", data.name, err) -// continue -// } -// 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: %v", data.name, err) -// } -// if !data.valid && err == nil { -// t.Errorf("[%v] Invalid token passed validation", data.name) -// } -// } -// } +import ( + "fmt" + "github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go/test" + "net/http" + "net/url" + "reflect" + "strings" + "testing" +) + +var requestTestData = []struct { + name string + claims jwt.MapClaims + headers map[string]string + query url.Values + valid bool +}{ + { + "oauth bearer token - header", + jwt.MapClaims{"foo": "bar"}, + map[string]string{"Authorization": "Bearer %v"}, + url.Values{}, + true, + }, + { + "oauth bearer token - url", + jwt.MapClaims{"foo": "bar"}, + map[string]string{}, + url.Values{"access_token": {"%v"}}, + true, + }, +} + +func TestParseRequest(t *testing.T) { + // load keys from disk + privateKey := test.LoadRSAPrivateKeyFromDisk("../test/sample_key") + publicKey := test.LoadRSAPublicKeyFromDisk("../test/sample_key.pub") + keyfunc := func(*jwt.Token) (interface{}, error) { + return publicKey, nil + } + + // Bearer token request + for _, data := range requestTestData { + // Make token from claims + tokenString := test.MakeSampleToken(data.claims, privateKey) + + // Make query string + for k, vv := range data.query { + for i, v := range vv { + if strings.Contains(v, "%v") { + data.query[k][i] = fmt.Sprintf(v, tokenString) + } + } + } + + // Make request from test struct + r, _ := http.NewRequest("GET", fmt.Sprintf("/?%v", data.query.Encode()), nil) + for k, v := range data.headers { + if strings.Contains(v, "%v") { + r.Header.Set(k, fmt.Sprintf(v, tokenString)) + } else { + r.Header.Set(k, tokenString) + } + } + token, err := ParseFromRequestWithClaims(r, keyfunc, &jwt.MapClaims{}) + + if token == nil { + t.Errorf("[%v] Token was not found: %v", data.name, err) + continue + } + 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: %v", data.name, err) + } + if !data.valid && err == nil { + t.Errorf("[%v] Invalid token passed validation", data.name) + } + } +} From 070a4bdd003267a775ae94172aeb3fba45f1b309 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Fri, 8 Apr 2016 13:58:29 -0700 Subject: [PATCH 29/51] moved request related error int request subpackage --- errors.go | 5 ++--- request/request.go | 8 +++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/errors.go b/errors.go index b055f3b..6a60e91 100644 --- a/errors.go +++ b/errors.go @@ -6,9 +6,8 @@ import ( // Error constants var ( - ErrInvalidKey = errors.New("key is invalid or of invalid type") - ErrHashUnavailable = errors.New("the requested hash function is unavailable") - ErrNoTokenInRequest = errors.New("no token present in request") + ErrInvalidKey = errors.New("key is invalid or of invalid type") + ErrHashUnavailable = errors.New("the requested hash function is unavailable") ) // The errors that might occur when parsing and validating a token diff --git a/request/request.go b/request/request.go index c1ba852..c812488 100644 --- a/request/request.go +++ b/request/request.go @@ -1,11 +1,17 @@ package request import ( + "errors" "github.com/dgrijalva/jwt-go" "net/http" "strings" ) +// Errors +var ( + ErrNoTokenInRequest = errors.New("no token present in request") +) + // Try to find the token in an http.Request. // This method will call ParseMultipartForm if there's no token in the header. // Currently, it looks in the Authorization header as well as @@ -29,5 +35,5 @@ func ParseFromRequestWithClaims(req *http.Request, keyFunc jwt.Keyfunc, claims j return jwt.ParseWithClaims(tokStr, keyFunc, claims) } - return nil, jwt.ErrNoTokenInRequest + return nil, ErrNoTokenInRequest } From c466333a1be062693ad85fd22c0014c20e6dcf8b Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 12 Apr 2016 13:18:31 -0700 Subject: [PATCH 30/51] expanded usefulness and correctness of examples --- example_test.go | 51 +++++++++++++---------------------- hmac_example_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 33 deletions(-) create mode 100644 hmac_example_test.go diff --git a/example_test.go b/example_test.go index 4321f14..dff2855 100644 --- a/example_test.go +++ b/example_test.go @@ -2,41 +2,19 @@ package jwt_test import ( "fmt" - "time" - "github.com/dgrijalva/jwt-go" ) -func ExampleParse(myToken string, myLookupKey func(interface{}) (interface{}, error)) { - token, err := jwt.Parse(myToken, func(token *jwt.Token) (interface{}, error) { - return myLookupKey(token.Header["kid"]) - }) - - if err == nil && token.Valid { - fmt.Println("Your token is valid. I like your style.") - } else { - fmt.Println("This token is terrible! I cannot accept this.") - } -} - -func ExampleNew() { - // Create the token - token := jwt.New(jwt.SigningMethodRS256) - - // Set some claims - 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 -} - -func ExampleNewWithClaims() { +// Example (atypical) using the StandardClaims type by itself to parse a token. +// The StandardClaims type is designed to be embedded into your custom types +// to provide standard validation features. You can use it alone, but there's +// no way to retrieve other fields after parsing. +// See the CustomClaimsType example for intended usage. +func ExampleNewWithClaims_standardClaims() { mySigningKey := []byte("AllYourBase") // Create the Claims - claims := jwt.StandardClaims{ + claims := &jwt.StandardClaims{ ExpiresAt: 15000, Issuer: "test", } @@ -47,7 +25,9 @@ func ExampleNewWithClaims() { //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.QsODzZu3lUZMVdhbO76u3Jv02iYCvEHcYVUI1kOWEU0 } -func ExampleNewWithClaims_customType() { +// Example parsing a token using a custom claims type. The StandardClaim is embedded +// in the custom type to allow for easy encoding, parsing and validation of standard claims. +func ExampleNewWithClaims_customClaimsType() { mySigningKey := []byte("AllYourBase") type MyCustomClaims struct { @@ -70,9 +50,13 @@ func ExampleNewWithClaims_customType() { //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c } -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"]) +// An example of parsing the error types using bitfield checks +func ExampleParse_errorChecking() { + // Token from another example. This token is expired + var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c" + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte("AllYourBase"), nil }) if token.Valid { @@ -90,4 +74,5 @@ func ExampleParse_errorChecking(myToken string, myLookupKey func(interface{}) (i fmt.Println("Couldn't handle this token:", err) } + // Output: Timing is everything } diff --git a/hmac_example_test.go b/hmac_example_test.go new file mode 100644 index 0000000..f8d2fc4 --- /dev/null +++ b/hmac_example_test.go @@ -0,0 +1,64 @@ +package jwt_test + +import ( + "fmt" + "github.com/dgrijalva/jwt-go" + "io/ioutil" + "time" +) + +// For HMAC signing method, the key can be any []byte. It is recommended to generate +// a key using crypto/rand or something equivalent. You need the same key for signing +// and validating. +var hmacSampleSecret []byte + +func init() { + // Load sample key data + if keyData, e := ioutil.ReadFile("test/hmacTestKey"); e == nil { + hmacSampleSecret = keyData + } else { + panic(e) + } +} + +// Example creating, signing, and encoding a JWT token using the HMAC signing method +func ExampleNew_hmac() { + // Create a new token object, specifying signing method and the claims + // you would like it to contain. + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "foo": "bar", + "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(), + }) + + // Sign and get the complete encoded token as a string using the secret + tokenString, err := token.SignedString(hmacSampleSecret) + + fmt.Println(tokenString, err) + // Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9.u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU +} + +// Example parsing and validating a token using the HMAC signing method +func ExampleParse_hmac() { + // sample token string taken from the New example + tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9.u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU" + + // Parse takes the token string and a function for looking up the key. The latter is especially + // useful if you use multiple keys for your application. The standard is to use 'kid' in the + // head of the token to identify which key to use, but the parsed token (head and claims) is provided + // to the callback, providing flexibility. + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + return hmacSampleSecret, nil + }) + + if claims, ok := token.Claims.(*jwt.MapClaims); ok && token.Valid { + fmt.Println((*claims)["foo"], (*claims)["nbf"]) + } else { + fmt.Println(err) + } + + // Output: bar 1.4444784e+09 +} From 0c245a4f7ecfa253fcd9272dd5ad51d47be7c3b6 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 12 Apr 2016 13:33:20 -0700 Subject: [PATCH 31/51] added example of parsing using custom type --- example_test.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/example_test.go b/example_test.go index dff2855..c1e399c 100644 --- a/example_test.go +++ b/example_test.go @@ -3,6 +3,7 @@ package jwt_test import ( "fmt" "github.com/dgrijalva/jwt-go" + "time" ) // Example (atypical) using the StandardClaims type by itself to parse a token. @@ -25,7 +26,7 @@ func ExampleNewWithClaims_standardClaims() { //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.QsODzZu3lUZMVdhbO76u3Jv02iYCvEHcYVUI1kOWEU0 } -// Example parsing a token using a custom claims type. The StandardClaim is embedded +// Example creating a token using a custom claims type. The StandardClaim is embedded // in the custom type to allow for easy encoding, parsing and validation of standard claims. func ExampleNewWithClaims_customClaimsType() { mySigningKey := []byte("AllYourBase") @@ -50,6 +51,41 @@ func ExampleNewWithClaims_customClaimsType() { //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c } +// Example creating a token using a custom claims type. The StandardClaim is embedded +// in the custom type to allow for easy encoding, parsing and validation of standard claims. +func ExampleParseWithClaims_customClaimsType() { + tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c" + + type MyCustomClaims struct { + Foo string `json:"foo"` + jwt.StandardClaims + } + + // sample token is expired. override time so it parses as valid + at(time.Unix(0, 0), func() { + token, err := jwt.ParseWithClaims(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte("AllYourBase"), nil + }, &MyCustomClaims{}) + + if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid { + fmt.Printf("%v %v", claims.Foo, claims.StandardClaims.ExpiresAt) + } else { + fmt.Println(err) + } + }) + + // Output: bar 15000 +} + +// Override time value for tests. Restore default value after. +func at(t time.Time, f func()) { + jwt.TimeFunc = func() time.Time { + return t + } + f() + jwt.TimeFunc = time.Now +} + // An example of parsing the error types using bitfield checks func ExampleParse_errorChecking() { // Token from another example. This token is expired From 6f2e6588a2922d9225ebcfaee9b18a573153f7a8 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 12 Apr 2016 13:34:11 -0700 Subject: [PATCH 32/51] replace inline examples with links to examples in the test suite. these examples are evaluated as part of the test pass, so they should remain correct --- README.md | 34 +++++----------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 5966b88..e4829e5 100644 --- a/README.md +++ b/README.md @@ -17,37 +17,13 @@ The part in the middle is the interesting bit. It's called the Claims and conta 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 +## Examples -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. +See [the project documentation](https://godoc.org/github.com/dgrijalva/jwt-go) for examples of usage: -```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"]), nil - }) - - if err == nil && token.Valid { - deliverGoodness("!") - } else { - deliverUtterRejection(":(") - } -``` - -## Create a token - -```go - // Create the token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "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) -``` +* [Simple example of parsing and validating a token](https://godoc.org/github.com/dgrijalva/jwt-go#example_Parse_hmac) +* [Simple example of building and signing a token](https://godoc.org/github.com/dgrijalva/jwt-go#example_New_hmac) +* [Directory of Examples](https://godoc.org/github.com/dgrijalva/jwt-go#pkg-examples) ## Extensions From 0145da90d469d9ec7f5a5cc72a4a2c28d276d093 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 12 Apr 2016 13:39:14 -0700 Subject: [PATCH 33/51] added link to jwt.io --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e4829e5..7802b96 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ A [go](http://www.golang.org) (or 'golang' for search engine friendliness) imple ## What the heck is a JWT? +JWT.io has [a great introduction](https://jwt.io/introduction) to JSON Web Tokens. + 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. From 9d0dcba491dbcb031a8b0a5722e8b02fcfff3a59 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 12 Apr 2016 14:31:41 -0700 Subject: [PATCH 34/51] fix for weird map pointer crap --- hmac_example_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hmac_example_test.go b/hmac_example_test.go index f8d2fc4..8fb5678 100644 --- a/hmac_example_test.go +++ b/hmac_example_test.go @@ -54,8 +54,8 @@ func ExampleParse_hmac() { return hmacSampleSecret, nil }) - if claims, ok := token.Claims.(*jwt.MapClaims); ok && token.Valid { - fmt.Println((*claims)["foo"], (*claims)["nbf"]) + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + fmt.Println(claims["foo"], claims["nbf"]) } else { fmt.Println(err) } From fb4ca74c9f326415a43ecfbb8ff504b06858a02a Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 12 Apr 2016 14:32:24 -0700 Subject: [PATCH 35/51] added special case behavior for MapClaims so they aren't all weird --- parser.go | 14 ++++++++++---- parser_test.go | 45 +++++++++++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/parser.go b/parser.go index cea5591..884fea9 100644 --- a/parser.go +++ b/parser.go @@ -16,7 +16,7 @@ type Parser struct { // keyFunc will receive the parsed token and should return the key for validating. // If everything is kosher, err will be nil func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { - return p.ParseWithClaims(tokenString, keyFunc, &MapClaims{}) + return p.ParseWithClaims(tokenString, keyFunc, MapClaims{}) } func (p *Parser) ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token, error) { @@ -42,6 +42,7 @@ func (p *Parser) ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Cla // parse Claims var claimBytes []byte + token.Claims = claims if claimBytes, err = DecodeSegment(parts[1]); err != nil { return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed} @@ -50,12 +51,17 @@ func (p *Parser) ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Cla if p.UseJSONNumber { dec.UseNumber() } - if err = dec.Decode(&claims); err != nil { + // JSON Decode. Special case for map type to avoid weird pointer behavior + if c, ok := token.Claims.(MapClaims); ok { + err = dec.Decode(&c) + } else { + err = dec.Decode(&claims) + } + // Handle decode error + if err != nil { return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed} } - token.Claims = claims - // Lookup signature method if method, ok := token.Header["alg"].(string); ok { if token.Method = GetSigningMethod(method); token.Method == nil { diff --git a/parser_test.go b/parser_test.go index 0c09204..a4eb83f 100644 --- a/parser_test.go +++ b/parser_test.go @@ -25,7 +25,7 @@ var jwtTestData = []struct { name string tokenString string keyfunc jwt.Keyfunc - claims jwt.MapClaims + claims jwt.Claims valid bool errors uint32 parser *jwt.Parser @@ -106,7 +106,7 @@ var jwtTestData = []struct { "invalid signing method", "", defaultKeyFunc, - map[string]interface{}{"foo": "bar"}, + jwt.MapClaims{"foo": "bar"}, false, jwt.ValidationErrorSignatureInvalid, &jwt.Parser{ValidMethods: []string{"HS256"}}, @@ -115,7 +115,7 @@ var jwtTestData = []struct { "valid signing method", "", defaultKeyFunc, - map[string]interface{}{"foo": "bar"}, + jwt.MapClaims{"foo": "bar"}, true, 0, &jwt.Parser{ValidMethods: []string{"RS256", "HS256"}}, @@ -124,7 +124,18 @@ var jwtTestData = []struct { "JSON Number", "", defaultKeyFunc, - map[string]interface{}{"foo": json.Number("123.4")}, + jwt.MapClaims{"foo": json.Number("123.4")}, + true, + 0, + &jwt.Parser{UseJSONNumber: true}, + }, + { + "Standard Claims", + "", + defaultKeyFunc, + &jwt.StandardClaims{ + ExpiresAt: time.Now().Add(time.Second * 10).Unix(), + }, true, 0, &jwt.Parser{UseJSONNumber: true}, @@ -141,7 +152,7 @@ func init() { } } -func makeSample(c jwt.MapClaims) string { +func makeSample(c jwt.Claims) string { keyData, e := ioutil.ReadFile("test/sample_key") if e != nil { panic(e.Error()) @@ -162,20 +173,30 @@ func makeSample(c jwt.MapClaims) string { } func TestParser_Parse(t *testing.T) { + // Iterate over test data set and run tests for _, data := range jwtTestData { + // If the token string is blank, use helper function to generate string if data.tokenString == "" { data.tokenString = makeSample(data.claims) } + // Parse the token var token *jwt.Token var err error - if data.parser != nil { - token, err = data.parser.Parse(data.tokenString, data.keyfunc) - } else { - token, err = jwt.Parse(data.tokenString, data.keyfunc) + var parser = data.parser + if parser == nil { + parser = new(jwt.Parser) + } + // Figure out correct claims type + switch data.claims.(type) { + case jwt.MapClaims: + token, err = parser.ParseWithClaims(data.tokenString, data.keyfunc, jwt.MapClaims{}) + case *jwt.StandardClaims: + token, err = parser.ParseWithClaims(data.tokenString, data.keyfunc, &jwt.StandardClaims{}) } - if !reflect.DeepEqual(&data.claims, token.Claims) { + // Verify result matches expectation + if !reflect.DeepEqual(data.claims, token.Claims) { t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims) } @@ -218,13 +239,13 @@ 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.MapClaims{}) + token, err := jwt.ParseFromRequestWithClaims(r, data.keyfunc, jwt.MapClaims{}) 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 c8d4fff6cc42e4e5dd48ffcc95e63446432180e8 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 12 Apr 2016 14:52:39 -0700 Subject: [PATCH 36/51] verify that an error is always returned if token.Valid is false (and the reverse) --- parser_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/parser_test.go b/parser_test.go index 622a423..8448986 100644 --- a/parser_test.go +++ b/parser_test.go @@ -183,6 +183,10 @@ func TestParser_Parse(t *testing.T) { t.Errorf("[%v] Invalid token passed validation", data.name) } + if (err == nil && !token.Valid) || (err != nil && token.Valid) { + t.Errorf("[%v] Inconsistent behavior between returned error and token.Valid") + } + if data.errors != 0 { if err == nil { t.Errorf("[%v] Expecting error. Didn't get one.", data.name) From 70eefe1649b54320f71c44ee107fc6b1c809be4b Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 12 Apr 2016 16:22:14 -0700 Subject: [PATCH 37/51] moved map claims to its own file --- claims.go | 87 ++---------------------------------------------- map_claims.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 85 deletions(-) create mode 100644 map_claims.go diff --git a/claims.go b/claims.go index ef4e57b..46b1a42 100644 --- a/claims.go +++ b/claims.go @@ -2,7 +2,6 @@ package jwt import ( "crypto/subtle" - "encoding/json" ) // For a type to be a Claims object, it must just have a Valid method that determines @@ -13,6 +12,7 @@ type Claims interface { // Structured version of Claims Section, as referenced at // https://tools.ietf.org/html/rfc7519#section-4.1 +// See examples for how to use this with your own claim types type StandardClaims struct { Audience string `json:"aud,omitempty"` ExpiresAt int64 `json:"exp,omitempty"` @@ -85,90 +85,7 @@ func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { return verifyNbf(c.NotBefore, cmp, req) } -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 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 MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { - switch exp := m["exp"].(type) { - case float64: - return verifyExp(int64(exp), cmp, req) - case json.Number: - v, _ := exp.Int64() - return verifyExp(v, cmp, req) - } - return req == false -} - -// Compares the iat claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { - switch iat := m["iat"].(type) { - case float64: - return verifyIat(int64(iat), cmp, req) - case json.Number: - v, _ := iat.Int64() - return verifyIat(v, cmp, req) - } - return req == false -} - -// Compares the iss claim against cmp. -// If required is false, this method will return true if the value matches or is unset -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 MapClaims) VerifyNotBefore(cmp int64, req bool) bool { - switch nbf := m["nbf"].(type) { - case float64: - return verifyNbf(int64(nbf), cmp, req) - case json.Number: - v, _ := nbf.Int64() - return verifyNbf(v, cmp, req) - } - return req == false -} - -// 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 MapClaims) 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 -} +// ----- helpers func verifyAud(aud string, cmp string, required bool) bool { if aud == "" { diff --git a/map_claims.go b/map_claims.go new file mode 100644 index 0000000..1e9d59f --- /dev/null +++ b/map_claims.go @@ -0,0 +1,92 @@ +package jwt + +import ( + "encoding/json" +) + +// Claims type that uses the map[string]interface{} for JSON decoding +// This is the default claims type if you don't supply one +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 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 MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { + switch exp := m["exp"].(type) { + case float64: + return verifyExp(int64(exp), cmp, req) + case json.Number: + v, _ := exp.Int64() + return verifyExp(v, cmp, req) + } + return req == false +} + +// Compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { + switch iat := m["iat"].(type) { + case float64: + return verifyIat(int64(iat), cmp, req) + case json.Number: + v, _ := iat.Int64() + return verifyIat(v, cmp, req) + } + return req == false +} + +// Compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +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 MapClaims) VerifyNotBefore(cmp int64, req bool) bool { + switch nbf := m["nbf"].(type) { + case float64: + return verifyNbf(int64(nbf), cmp, req) + case json.Number: + v, _ := nbf.Int64() + return verifyNbf(v, cmp, req) + } + return req == false +} + +// 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 MapClaims) 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 +} From 5e270fa6cdfaa49531d766eab8061de542376780 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 12 Apr 2016 16:25:25 -0700 Subject: [PATCH 38/51] changed argument order to put claims type before keyfunc. this is easier to read when keyfunc is an inline closure --- example_test.go | 4 ++-- parser.go | 4 ++-- parser_test.go | 4 ++-- request/request.go | 8 ++++---- request/request_test.go | 2 +- token.go | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/example_test.go b/example_test.go index c1e399c..ae8b788 100644 --- a/example_test.go +++ b/example_test.go @@ -63,9 +63,9 @@ func ExampleParseWithClaims_customClaimsType() { // sample token is expired. override time so it parses as valid at(time.Unix(0, 0), func() { - token, err := jwt.ParseWithClaims(tokenString, func(token *jwt.Token) (interface{}, error) { + token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte("AllYourBase"), nil - }, &MyCustomClaims{}) + }) if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid { fmt.Printf("%v %v", claims.Foo, claims.StandardClaims.ExpiresAt) diff --git a/parser.go b/parser.go index 884fea9..ab3b234 100644 --- a/parser.go +++ b/parser.go @@ -16,10 +16,10 @@ type Parser struct { // keyFunc will receive the parsed token and should return the key for validating. // If everything is kosher, err will be nil func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { - return p.ParseWithClaims(tokenString, keyFunc, MapClaims{}) + return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc) } -func (p *Parser) ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token, error) { +func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { parts := strings.Split(tokenString, ".") if len(parts) != 3 { return nil, &ValidationError{err: "token contains an invalid number of segments", Errors: ValidationErrorMalformed} diff --git a/parser_test.go b/parser_test.go index 3736b09..03e3b2c 100644 --- a/parser_test.go +++ b/parser_test.go @@ -192,9 +192,9 @@ func TestParser_Parse(t *testing.T) { // Figure out correct claims type switch data.claims.(type) { case jwt.MapClaims: - token, err = parser.ParseWithClaims(data.tokenString, data.keyfunc, jwt.MapClaims{}) + token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc) case *jwt.StandardClaims: - token, err = parser.ParseWithClaims(data.tokenString, data.keyfunc, &jwt.StandardClaims{}) + token, err = parser.ParseWithClaims(data.tokenString, &jwt.StandardClaims{}, data.keyfunc) } // Verify result matches expectation diff --git a/request/request.go b/request/request.go index c812488..98e1be2 100644 --- a/request/request.go +++ b/request/request.go @@ -17,22 +17,22 @@ var ( // 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 jwt.Keyfunc) (token *jwt.Token, err error) { - return ParseFromRequestWithClaims(req, keyFunc, &jwt.MapClaims{}) + return ParseFromRequestWithClaims(req, jwt.MapClaims{}, keyFunc) } -func ParseFromRequestWithClaims(req *http.Request, keyFunc jwt.Keyfunc, claims jwt.Claims) (token *jwt.Token, err error) { +func ParseFromRequestWithClaims(req *http.Request, claims jwt.Claims, keyFunc jwt.Keyfunc) (token *jwt.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:7]) == "BEARER " { - return jwt.ParseWithClaims(ah[7:], keyFunc, claims) + return jwt.ParseWithClaims(ah[7:], claims, keyFunc) } } // Look for "access_token" parameter req.ParseMultipartForm(10e6) if tokStr := req.Form.Get("access_token"); tokStr != "" { - return jwt.ParseWithClaims(tokStr, keyFunc, claims) + return jwt.ParseWithClaims(tokStr, claims, keyFunc) } return nil, ErrNoTokenInRequest diff --git a/request/request_test.go b/request/request_test.go index 0f2fb9b..6b09521 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -65,7 +65,7 @@ func TestParseRequest(t *testing.T) { r.Header.Set(k, tokenString) } } - token, err := ParseFromRequestWithClaims(r, keyfunc, jwt.MapClaims{}) + token, err := ParseFromRequestWithClaims(r, jwt.MapClaims{}, keyfunc) if token == nil { t.Errorf("[%v] Token was not found: %v", data.name, err) diff --git a/token.go b/token.go index 7ac24fa..5eb365c 100644 --- a/token.go +++ b/token.go @@ -89,8 +89,8 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { return new(Parser).Parse(tokenString, keyFunc) } -func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token, error) { - return new(Parser).ParseWithClaims(tokenString, keyFunc, claims) +func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { + return new(Parser).ParseWithClaims(tokenString, claims, keyFunc) } // Encode JWT specific base64url encoding with padding stripped From b9283128ba203e9e9a8c238d51394c465e73f584 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 12 Apr 2016 16:39:41 -0700 Subject: [PATCH 39/51] updated README with new argument order for WithClaims methods --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7802b96..db16671 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ On the other end of usage all of the `jwt.Parse` and friends got a `WithClaims` New method usage: ```go - token, err := jwt.ParseWithClaims(token, keyFunc, &jwt.StandardClaims{}) + token, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, keyFunc) claims := token.Claims.(jwt.StandardClaims) fmt.Println(claims.IssuedAt) ``` From b6d201ffa0bb0c68d8c360c4447bb8212fe7eec0 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Tue, 12 Apr 2016 17:06:56 -0700 Subject: [PATCH 40/51] mutex around signing method registration. shouldn't matter, but couldn't hurt --- signing_method.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/signing_method.go b/signing_method.go index 12cf0f3..ed1f212 100644 --- a/signing_method.go +++ b/signing_method.go @@ -1,6 +1,11 @@ package jwt +import ( + "sync" +) + var signingMethods = map[string]func() SigningMethod{} +var signingMethodLock = new(sync.RWMutex) // Implement SigningMethod to add new methods for signing or verifying tokens. type SigningMethod interface { @@ -12,11 +17,17 @@ type SigningMethod interface { // Register the "alg" name and a factory function for signing method. // This is typically done during init() in the method's implementation func RegisterSigningMethod(alg string, f func() SigningMethod) { + signingMethodLock.Lock() + defer signingMethodLock.Unlock() + signingMethods[alg] = f } // Get a signing method from an "alg" string func GetSigningMethod(alg string) (method SigningMethod) { + signingMethodLock.RLock() + defer signingMethodLock.RUnlock() + if methodF, ok := signingMethods[alg]; ok { method = methodF() } From bb45bfcdecac35b6afa09673bd17a780b258a541 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Mon, 6 Jun 2016 15:27:44 -0700 Subject: [PATCH 41/51] add new interface Extractor for making token extraction pluggable --- request/extractor.go | 75 +++++++++++++++++++++++++++++++++++++++++ request/oauth2.go | 25 ++++++++++++++ request/request.go | 31 +++++------------ request/request_test.go | 2 +- 4 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 request/extractor.go create mode 100644 request/oauth2.go diff --git a/request/extractor.go b/request/extractor.go new file mode 100644 index 0000000..57ea2dd --- /dev/null +++ b/request/extractor.go @@ -0,0 +1,75 @@ +package request + +import ( + "errors" + "net/http" +) + +// Errors +var ( + ErrNoTokenInRequest = errors.New("no token present in request") +) + +// Interface for extracting a token from an HTTP request +type Extractor interface { + ExtractToken(*http.Request) (string, error) +} + +// Extract token from headers +type HeaderExtractor []string + +func (e HeaderExtractor) ExtractToken(req *http.Request) (string, error) { + // loop over header names and return the first one that contains data + for _, header := range e { + if ah := req.Header.Get(header); ah != "" { + return ah, nil + } + } + return "", ErrNoTokenInRequest +} + +// Extract token from request args +type ArgumentExtractor []string + +func (e ArgumentExtractor) ExtractToken(req *http.Request) (string, error) { + // Make sure form is parsed + req.ParseMultipartForm(10e6) + + // loop over arg names and return the first one that contains data + for _, arg := range e { + if ah := req.Form.Get(arg); ah != "" { + return ah, nil + } + } + + return "", ErrNoTokenInRequest +} + +// Tries extractors in order until one works or an error occurs +type MultiExtractor []Extractor + +func (e MultiExtractor) ExtractToken(req *http.Request) (string, error) { + // loop over header names and return the first one that contains data + for _, extractor := range e { + if tok, err := extractor.ExtractToken(req); tok != "" { + return tok, nil + } else if err != ErrNoTokenInRequest { + return "", err + } + } + return "", ErrNoTokenInRequest +} + +// Wrap an Extractor in this to post-process the value before it's handed off +type PostExtractionFilter struct { + Extractor + Filter func(string) (string, error) +} + +func (e *PostExtractionFilter) ExtractToken(req *http.Request) (string, error) { + if tok, err := e.Extractor.ExtractToken(req); tok != "" { + return e.Filter(tok) + } else { + return "", err + } +} diff --git a/request/oauth2.go b/request/oauth2.go new file mode 100644 index 0000000..bbb6ac2 --- /dev/null +++ b/request/oauth2.go @@ -0,0 +1,25 @@ +package request + +import ( + "strings" +) + +// Extract Authorization header and strip 'Bearer ' from it +var AuthorizationHeaderExtractor = &PostExtractionFilter{ + HeaderExtractor{"Authorization"}, + func(tok string) (string, error) { + // Should be a bearer token + if len(tok) > 6 && strings.ToUpper(tok[0:7]) == "BEARER " { + return tok[7:], nil + } + return tok, nil + }, +} + +// Extractor for OAuth2 access tokens +var OAuth2Extractor = &MultiExtractor{ + // Look for authorization token first + AuthorizationHeaderExtractor, + // Extract access_token from form or GET argument + &ArgumentExtractor{"access_token"}, +} diff --git a/request/request.go b/request/request.go index 98e1be2..9daaa48 100644 --- a/request/request.go +++ b/request/request.go @@ -1,39 +1,24 @@ package request import ( - "errors" "github.com/dgrijalva/jwt-go" "net/http" - "strings" -) - -// Errors -var ( - ErrNoTokenInRequest = errors.New("no token present in request") ) // Try to find the token in an http.Request. // This method will call ParseMultipartForm if there's no token in the header. // 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 jwt.Keyfunc) (token *jwt.Token, err error) { - return ParseFromRequestWithClaims(req, jwt.MapClaims{}, keyFunc) +func ParseFromRequest(req *http.Request, extractor Extractor, keyFunc jwt.Keyfunc) (token *jwt.Token, err error) { + return ParseFromRequestWithClaims(req, extractor, jwt.MapClaims{}, keyFunc) } -func ParseFromRequestWithClaims(req *http.Request, claims jwt.Claims, keyFunc jwt.Keyfunc) (token *jwt.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:7]) == "BEARER " { - return jwt.ParseWithClaims(ah[7:], claims, keyFunc) - } +func ParseFromRequestWithClaims(req *http.Request, extractor Extractor, claims jwt.Claims, keyFunc jwt.Keyfunc) (token *jwt.Token, err error) { + // Extract token from request + tokStr, err := extractor.ExtractToken(req) + if err != nil { + return nil, err } - // Look for "access_token" parameter - req.ParseMultipartForm(10e6) - if tokStr := req.Form.Get("access_token"); tokStr != "" { - return jwt.ParseWithClaims(tokStr, claims, keyFunc) - } - - return nil, ErrNoTokenInRequest + return jwt.ParseWithClaims(tokStr, claims, keyFunc) } diff --git a/request/request_test.go b/request/request_test.go index 6b09521..504e1bc 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -65,7 +65,7 @@ func TestParseRequest(t *testing.T) { r.Header.Set(k, tokenString) } } - token, err := ParseFromRequestWithClaims(r, jwt.MapClaims{}, keyfunc) + token, err := ParseFromRequestWithClaims(r, OAuth2Extractor, jwt.MapClaims{}, keyfunc) if token == nil { t.Errorf("[%v] Token was not found: %v", data.name, err) From de0a819d8dadfba7a8f666d3c80b72c7a9a2bd45 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Mon, 6 Jun 2016 16:55:41 -0700 Subject: [PATCH 42/51] added documentation --- request/doc.go | 7 +++++++ request/extractor.go | 15 ++++++++++----- request/oauth2.go | 6 ++++-- request/request.go | 16 ++++++++-------- 4 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 request/doc.go diff --git a/request/doc.go b/request/doc.go new file mode 100644 index 0000000..c01069c --- /dev/null +++ b/request/doc.go @@ -0,0 +1,7 @@ +// Utility package for extracting JWT tokens from +// HTTP requests. +// +// The main function is ParseFromRequest and it's WithClaims variant. +// See examples for how to use the various Extractor implementations +// or roll your own. +package request diff --git a/request/extractor.go b/request/extractor.go index 57ea2dd..61c330a 100644 --- a/request/extractor.go +++ b/request/extractor.go @@ -10,12 +10,15 @@ var ( ErrNoTokenInRequest = errors.New("no token present in request") ) -// Interface for extracting a token from an HTTP request +// Interface for extracting a token from an HTTP request. +// The ExtractToken method should return a token string or an error. +// If no token is present, you must return ErrNoTokenInRequest. type Extractor interface { ExtractToken(*http.Request) (string, error) } -// Extract token from headers +// Extractor for finding a token in a header. Looks at each specified +// header in order until there's a match type HeaderExtractor []string func (e HeaderExtractor) ExtractToken(req *http.Request) (string, error) { @@ -28,7 +31,8 @@ func (e HeaderExtractor) ExtractToken(req *http.Request) (string, error) { return "", ErrNoTokenInRequest } -// Extract token from request args +// Extract token from request arguments. This includes a POSTed form or +// GET URL arguments. Argument names are tried in order until there's a match. type ArgumentExtractor []string func (e ArgumentExtractor) ExtractToken(req *http.Request) (string, error) { @@ -45,7 +49,7 @@ func (e ArgumentExtractor) ExtractToken(req *http.Request) (string, error) { return "", ErrNoTokenInRequest } -// Tries extractors in order until one works or an error occurs +// Tries Extractors in order until one returns a token string or an error occurs type MultiExtractor []Extractor func (e MultiExtractor) ExtractToken(req *http.Request) (string, error) { @@ -60,7 +64,8 @@ func (e MultiExtractor) ExtractToken(req *http.Request) (string, error) { return "", ErrNoTokenInRequest } -// Wrap an Extractor in this to post-process the value before it's handed off +// Wrap an Extractor in this to post-process the value before it's handed off. +// See AuthorizationHeaderExtractor for an example type PostExtractionFilter struct { Extractor Filter func(string) (string, error) diff --git a/request/oauth2.go b/request/oauth2.go index bbb6ac2..0f7fa02 100644 --- a/request/oauth2.go +++ b/request/oauth2.go @@ -4,7 +4,8 @@ import ( "strings" ) -// Extract Authorization header and strip 'Bearer ' from it +// Extract bearer token from Authorization header +// Uses PostExtractionFilter to strip "Bearer " prefix from header var AuthorizationHeaderExtractor = &PostExtractionFilter{ HeaderExtractor{"Authorization"}, func(tok string) (string, error) { @@ -16,7 +17,8 @@ var AuthorizationHeaderExtractor = &PostExtractionFilter{ }, } -// Extractor for OAuth2 access tokens +// Extractor for OAuth2 access tokens. Looks in 'Authorization' +// header then 'access_token' argument for a token. var OAuth2Extractor = &MultiExtractor{ // Look for authorization token first AuthorizationHeaderExtractor, diff --git a/request/request.go b/request/request.go index 9daaa48..1807b39 100644 --- a/request/request.go +++ b/request/request.go @@ -5,20 +5,20 @@ import ( "net/http" ) -// Try to find the token in an http.Request. -// This method will call ParseMultipartForm if there's no token in the header. -// Currently, it looks in the Authorization header as well as -// looking for an 'access_token' request parameter in req.Form. +// Extract and parse a JWT token from an HTTP request. +// This behaves the same as Parse, but accepts a request and an extractor +// instead of a token string. The Extractor interface allows you to define +// the logic for extracting a token. Several useful implementations are provided. func ParseFromRequest(req *http.Request, extractor Extractor, keyFunc jwt.Keyfunc) (token *jwt.Token, err error) { return ParseFromRequestWithClaims(req, extractor, jwt.MapClaims{}, keyFunc) } +// ParseFromRequest but with custom Claims type func ParseFromRequestWithClaims(req *http.Request, extractor Extractor, claims jwt.Claims, keyFunc jwt.Keyfunc) (token *jwt.Token, err error) { // Extract token from request - tokStr, err := extractor.ExtractToken(req) - if err != nil { + if tokStr, err := extractor.ExtractToken(req); err == nil { + return jwt.ParseWithClaims(tokStr, claims, keyFunc) + } else { return nil, err } - - return jwt.ParseWithClaims(tokStr, claims, keyFunc) } From f93fcfd3f91f046d0bb0d2f537083864f59725ac Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Mon, 6 Jun 2016 17:18:24 -0700 Subject: [PATCH 43/51] unit tests for extractor --- request/extractor_test.go | 86 +++++++++++++++++++++++++++++++++++++++ request/oauth2.go | 2 +- request/request_test.go | 31 +++++++++++--- 3 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 request/extractor_test.go diff --git a/request/extractor_test.go b/request/extractor_test.go new file mode 100644 index 0000000..ecfd122 --- /dev/null +++ b/request/extractor_test.go @@ -0,0 +1,86 @@ +package request + +import ( + "fmt" + "net/http" + "net/url" + "testing" +) + +var extractorTestTokenA = "A" +var extractorTestTokenB = "B" + +var extractorTestData = []struct { + name string + extractor Extractor + headers map[string]string + query url.Values + token string + err error +}{ + { + name: "simple header", + extractor: HeaderExtractor{"Foo"}, + headers: map[string]string{"Foo": extractorTestTokenA}, + query: nil, + token: extractorTestTokenA, + err: nil, + }, + { + name: "simple argument", + extractor: ArgumentExtractor{"token"}, + headers: map[string]string{}, + query: url.Values{"token": {extractorTestTokenA}}, + token: extractorTestTokenA, + err: nil, + }, + { + name: "multiple extractors", + extractor: MultiExtractor{ + HeaderExtractor{"Foo"}, + ArgumentExtractor{"token"}, + }, + headers: map[string]string{"Foo": extractorTestTokenA}, + query: url.Values{"token": {extractorTestTokenB}}, + token: extractorTestTokenA, + err: nil, + }, + { + name: "simple miss", + extractor: HeaderExtractor{"This-Header-Is-Not-Set"}, + headers: map[string]string{"Foo": extractorTestTokenA}, + query: nil, + token: "", + err: ErrNoTokenInRequest, + }, + { + name: "filter", + extractor: AuthorizationHeaderExtractor, + headers: map[string]string{"Authorization": "Bearer " + extractorTestTokenA}, + query: nil, + token: extractorTestTokenA, + err: nil, + }, +} + +func TestExtractor(t *testing.T) { + // Bearer token request + for _, data := range extractorTestData { + // Make request from test struct + r, _ := http.NewRequest("GET", fmt.Sprintf("/?%v", data.query.Encode()), nil) + for k, v := range data.headers { + r.Header.Set(k, v) + } + + // Test extractor + token, err := data.extractor.ExtractToken(r) + if token != data.token { + t.Errorf("[%v] Expected token '%v'. Got '%v'", data.name, data.token, token) + continue + } + if err != data.err { + t.Errorf("[%v] Expected error '%v'. Got '%v'", data.name, data.err, err) + continue + } + } +} diff --git a/request/oauth2.go b/request/oauth2.go index 0f7fa02..c84cbd7 100644 --- a/request/oauth2.go +++ b/request/oauth2.go @@ -23,5 +23,5 @@ var OAuth2Extractor = &MultiExtractor{ // Look for authorization token first AuthorizationHeaderExtractor, // Extract access_token from form or GET argument - &ArgumentExtractor{"access_token"}, + ArgumentExtractor{"access_token"}, } diff --git a/request/request_test.go b/request/request_test.go index 504e1bc..b4365cd 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -12,15 +12,25 @@ import ( ) var requestTestData = []struct { - name string - claims jwt.MapClaims - headers map[string]string - query url.Values - valid bool + name string + claims jwt.MapClaims + extractor Extractor + headers map[string]string + query url.Values + valid bool }{ + { + "authorization bearer token", + jwt.MapClaims{"foo": "bar"}, + AuthorizationHeaderExtractor, + map[string]string{"Authorization": "Bearer %v"}, + url.Values{}, + true, + }, { "oauth bearer token - header", jwt.MapClaims{"foo": "bar"}, + OAuth2Extractor, map[string]string{"Authorization": "Bearer %v"}, url.Values{}, true, @@ -28,10 +38,19 @@ var requestTestData = []struct { { "oauth bearer token - url", jwt.MapClaims{"foo": "bar"}, + OAuth2Extractor, map[string]string{}, url.Values{"access_token": {"%v"}}, true, }, + { + "url token", + jwt.MapClaims{"foo": "bar"}, + ArgumentExtractor{"token"}, + map[string]string{}, + url.Values{"token": {"%v"}}, + true, + }, } func TestParseRequest(t *testing.T) { @@ -65,7 +84,7 @@ func TestParseRequest(t *testing.T) { r.Header.Set(k, tokenString) } } - token, err := ParseFromRequestWithClaims(r, OAuth2Extractor, jwt.MapClaims{}, keyfunc) + token, err := ParseFromRequestWithClaims(r, data.extractor, jwt.MapClaims{}, keyfunc) if token == nil { t.Errorf("[%v] Token was not found: %v", data.name, err) From 5eef21b7edec11f3819794484eee852323a7e204 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Mon, 6 Jun 2016 17:45:30 -0700 Subject: [PATCH 44/51] a few examples and some documentation cleanup --- request/extractor_example_test.go | 32 +++++++++++++++++++++++++++++++ request/extractor_test.go | 13 +++++++++---- request/oauth2.go | 19 +++++++++--------- 3 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 request/extractor_example_test.go diff --git a/request/extractor_example_test.go b/request/extractor_example_test.go new file mode 100644 index 0000000..a994ffe --- /dev/null +++ b/request/extractor_example_test.go @@ -0,0 +1,32 @@ +package request + +import ( + "fmt" + "net/url" +) + +const ( + exampleTokenA = "A" +) + +func ExampleHeaderExtractor() { + req := makeExampleRequest("GET", "/", map[string]string{"Token": exampleTokenA}, nil) + tokenString, err := HeaderExtractor{"Token"}.ExtractToken(req) + if err == nil { + fmt.Println(tokenString) + } else { + fmt.Println(err) + } + //Output: A +} + +func ExampleArgumentExtractor() { + req := makeExampleRequest("GET", "/", nil, url.Values{"token": {extractorTestTokenA}}) + tokenString, err := ArgumentExtractor{"token"}.ExtractToken(req) + if err == nil { + fmt.Println(tokenString) + } else { + fmt.Println(err) + } + //Output: A +} diff --git a/request/extractor_test.go b/request/extractor_test.go index ecfd122..e3bbb0a 100644 --- a/request/extractor_test.go +++ b/request/extractor_test.go @@ -67,10 +67,7 @@ func TestExtractor(t *testing.T) { // Bearer token request for _, data := range extractorTestData { // Make request from test struct - r, _ := http.NewRequest("GET", fmt.Sprintf("/?%v", data.query.Encode()), nil) - for k, v := range data.headers { - r.Header.Set(k, v) - } + r := makeExampleRequest("GET", "/", data.headers, data.query) // Test extractor token, err := data.extractor.ExtractToken(r) @@ -84,3 +81,11 @@ func TestExtractor(t *testing.T) { } } } + +func makeExampleRequest(method, path string, headers map[string]string, urlArgs url.Values) *http.Request { + r, _ := http.NewRequest(method, fmt.Sprintf("%v?%v", path, urlArgs.Encode()), nil) + for k, v := range headers { + r.Header.Set(k, v) + } + return r +} diff --git a/request/oauth2.go b/request/oauth2.go index c84cbd7..5948694 100644 --- a/request/oauth2.go +++ b/request/oauth2.go @@ -4,24 +4,25 @@ import ( "strings" ) +// Strips 'Bearer ' prefix from bearer token string +func stripBearerPrefixFromTokenString(tok string) (string, error) { + // Should be a bearer token + if len(tok) > 6 && strings.ToUpper(tok[0:7]) == "BEARER " { + return tok[7:], nil + } + return tok, nil +} + // Extract bearer token from Authorization header // Uses PostExtractionFilter to strip "Bearer " prefix from header var AuthorizationHeaderExtractor = &PostExtractionFilter{ HeaderExtractor{"Authorization"}, - func(tok string) (string, error) { - // Should be a bearer token - if len(tok) > 6 && strings.ToUpper(tok[0:7]) == "BEARER " { - return tok[7:], nil - } - return tok, nil - }, + stripBearerPrefixFromTokenString, } // Extractor for OAuth2 access tokens. Looks in 'Authorization' // header then 'access_token' argument for a token. var OAuth2Extractor = &MultiExtractor{ - // Look for authorization token first AuthorizationHeaderExtractor, - // Extract access_token from form or GET argument ArgumentExtractor{"access_token"}, } From fa2cc688957528cb9c605503d8ac0a3bc348e21e Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Mon, 6 Jun 2016 18:08:17 -0700 Subject: [PATCH 45/51] version history --- VERSION_HISTORY.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/VERSION_HISTORY.md b/VERSION_HISTORY.md index 318bf39..174a641 100644 --- a/VERSION_HISTORY.md +++ b/VERSION_HISTORY.md @@ -2,10 +2,20 @@ #### 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. +* **Compatibility Breaking Changes** + * 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. + * Signature of `Keyfunc` is now `func(*Token) (interface{}, error)` + * `ParseFromRequest` has been moved to `request` subpackage and usage has changed + * 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. +* Other Additions and Changes + * Added `Claims` interface type to allow users to decode the claims into a custom type + * 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. + * Dramatically improved the functionality and flexibility of `ParseFromRequest`, which is now in the `request` subpackage + * Added `ParseFromRequestWithClaims` which is the `FromRequest` equivalent of `ParseWithClaims` + * Added new interface type `Extractor`, which is used for extracting JWT strings from http requests. Used with `ParseFromRequest` and `ParseFromRequestWithClaims`. + * Added several new, more specific, validation errors to error type bitmask + * Moved examples from README to executable example files + * Signing method registry is now thread safe #### 2.5.0 From c5c24ad06a447d06ce235322deaeb7a8721d453b Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Mon, 6 Jun 2016 18:22:25 -0700 Subject: [PATCH 46/51] updated version history --- VERSION_HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/VERSION_HISTORY.md b/VERSION_HISTORY.md index 2a39b24..b526ce3 100644 --- a/VERSION_HISTORY.md +++ b/VERSION_HISTORY.md @@ -16,6 +16,7 @@ * Added several new, more specific, validation errors to error type bitmask * Moved examples from README to executable example files * Signing method registry is now thread safe + * Added new property to `ValidationError`, which contains the raw error returned by calls made by parse/verify (such as those returned by keyfunc or json parser) #### 2.6.0 From f1a773028f6a70c72890f5b8d7df91c39a2bf7b2 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Wed, 8 Jun 2016 11:36:38 -0700 Subject: [PATCH 47/51] added a simple http example --- http_example_test.go | 215 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 http_example_test.go diff --git a/http_example_test.go b/http_example_test.go new file mode 100644 index 0000000..715c5d7 --- /dev/null +++ b/http_example_test.go @@ -0,0 +1,215 @@ +package jwt_test + +// using asymmetric crypto/RSA keys + +import ( + "bytes" + "crypto/rsa" + "fmt" + "github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go/request" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// location of the files used for signing and verification +const ( + privKeyPath = "test/sample_key" // openssl genrsa -out app.rsa keysize + pubKeyPath = "test/sample_key.pub" // openssl rsa -in app.rsa -pubout > app.rsa.pub +) + +var ( + verifyKey *rsa.PublicKey + signKey *rsa.PrivateKey + serverPort int + // storing sample username/password pairs + // don't do this on a real server + users = map[string]string{ + "test": "known", + } +) + +// read the key files before starting http handlers +func init() { + signBytes, err := ioutil.ReadFile(privKeyPath) + fatal(err) + + signKey, err = jwt.ParseRSAPrivateKeyFromPEM(signBytes) + fatal(err) + + verifyBytes, err := ioutil.ReadFile(pubKeyPath) + fatal(err) + + verifyKey, err = jwt.ParseRSAPublicKeyFromPEM(verifyBytes) + fatal(err) + + http.HandleFunc("/authenticate", authHandler) + http.HandleFunc("/restricted", restrictedHandler) + + // Setup listener + listener, err := net.ListenTCP("tcp", &net.TCPAddr{}) + serverPort = listener.Addr().(*net.TCPAddr).Port + + log.Println("Listening...") + go func() { + fatal(http.Serve(listener, nil)) + }() +} + +var start func() + +func fatal(err error) { + if err != nil { + log.Fatal(err) + } +} + +// Define some custom types were going to use within our tokens +type CustomerInfo struct { + Name string + Kind string +} + +type CustomClaimsExample struct { + *jwt.StandardClaims + TokenType string + CustomerInfo +} + +func Example_getTokenViaHTTP() { + // See func authHandler for an example auth handler that produces a token + res, err := http.PostForm(fmt.Sprintf("http://localhost:%v/authenticate", serverPort), url.Values{ + "user": {"test"}, + "pass": {"known"}, + }) + if err != nil { + fatal(err) + } + + if res.StatusCode != 200 { + fmt.Println("Unexpected status code", res.StatusCode) + } + + // Read the token out of the response body + buf := new(bytes.Buffer) + io.Copy(buf, res.Body) + res.Body.Close() + tokenString := strings.TrimSpace(buf.String()) + + // Parse the token + token, err := jwt.ParseWithClaims(tokenString, &CustomClaimsExample{}, func(token *jwt.Token) (interface{}, error) { + // since we only use the one private key to sign the tokens, + // we also only use its public counter part to verify + return verifyKey, nil + }) + fatal(err) + + claims := token.Claims.(*CustomClaimsExample) + fmt.Println(claims.CustomerInfo.Name) + + //Output: test +} + +func Example_useTokenViaHTTP() { + + // Make a sample token + // In a real world situation, this token will have been acquired from + // some other API call (see Example_getTokenViaHTTP) + token, err := createToken("foo") + fatal(err) + + // Make request. See func restrictedHandler for example request processor + req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%v/restricted", serverPort), nil) + fatal(err) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token)) + res, err := http.DefaultClient.Do(req) + fatal(err) + + // Read the response body + buf := new(bytes.Buffer) + io.Copy(buf, res.Body) + res.Body.Close() + fmt.Println(buf.String()) + + // Output: Welcome, foo +} + +func createToken(user string) (string, error) { + // create a signer for rsa 256 + t := jwt.New(jwt.GetSigningMethod("RS256")) + + // set our claims + t.Claims = &CustomClaimsExample{ + &jwt.StandardClaims{ + // set the expire time + // see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-20#section-4.1.4 + ExpiresAt: time.Now().Add(time.Minute * 1).Unix(), + }, + "level1", + CustomerInfo{user, "human"}, + } + + // Creat token string + return t.SignedString(signKey) +} + +// reads the form values, checks them and creates the token +func authHandler(w http.ResponseWriter, r *http.Request) { + // make sure its post + if r.Method != "POST" { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintln(w, "No POST", r.Method) + return + } + + user := r.FormValue("user") + pass := r.FormValue("pass") + + log.Printf("Authenticate: user[%s] pass[%s]\n", user, pass) + + // check values + if user != "test" || pass != "known" { + w.WriteHeader(http.StatusForbidden) + fmt.Fprintln(w, "Wrong info") + return + } + + tokenString, err := createToken(user) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, "Sorry, error while Signing Token!") + log.Printf("Token Signing error: %v\n", err) + return + } + + w.Header().Set("Content-Type", "application/jwt") + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, tokenString) +} + +// only accessible with a valid token +func restrictedHandler(w http.ResponseWriter, r *http.Request) { + // Get token from request + token, err := request.ParseFromRequestWithClaims(r, request.OAuth2Extractor, &CustomClaimsExample{}, func(token *jwt.Token) (interface{}, error) { + // since we only use the one private key to sign the tokens, + // we also only use its public counter part to verify + return verifyKey, nil + }) + + // If the token is missing or invalid, return error + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintln(w, "Invalid token:", err) + return + } + + // Token is valid + fmt.Fprintln(w, "Welcome,", token.Claims.(*CustomClaimsExample).Name) + return +} From 0fbf86c98bea9e5c5c2fa06dea5159593bf404bd Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Wed, 8 Jun 2016 11:38:48 -0700 Subject: [PATCH 48/51] documentation update --- README.md | 2 +- http_example_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d84a9c5..93203f9 100644 --- a/README.md +++ b/README.md @@ -133,4 +133,4 @@ Without going too far down the rabbit hole, here's a description of the interact Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go). -The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. For a more http centric example, see [this gist](https://gist.github.com/cryptix/45c33ecf0ae54828e63b). +The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in to documentation. diff --git a/http_example_test.go b/http_example_test.go index 715c5d7..82e9c50 100644 --- a/http_example_test.go +++ b/http_example_test.go @@ -1,6 +1,7 @@ package jwt_test -// using asymmetric crypto/RSA keys +// Example HTTP auth using asymmetric crypto/RSA keys +// This is based on a (now outdated) example at https://gist.github.com/cryptix/45c33ecf0ae54828e63b import ( "bytes" From 2ed748cd9c410d8078fac6591189322264d2c184 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Wed, 15 Jun 2016 10:49:54 -0700 Subject: [PATCH 49/51] updated documentation. moved migration guide to separate doc --- MIGRATION_GUIDE.md | 96 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 59 +-------------------------- VERSION_HISTORY.md | 1 - request/extractor.go | 1 + 4 files changed, 98 insertions(+), 59 deletions(-) create mode 100644 MIGRATION_GUIDE.md diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..fd62e94 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,96 @@ +## Migration Guide from v2 -> v3 + +Version 3 adds several new, frequently requested features. To do so, it introduces a few breaking changes. We've worked to keep these as minimal as possible. This guide explains the breaking changes and how you can quickly update your code. + +### `Token.Claims` is now an interface type + +The most requested feature from the 2.0 verison of this library was the ability to provide a custom type to the JSON parser for claims. This was implemented by introducing a new interface, `Claims`, to replace `map[string]interface{}`. We also included two concrete implementations of `Claims`: `MapClaims` and `StandardClaims`. + +`MapClaims` is an alias for `map[string]interface{}` with built in validation behavior. It is the default claims type when using `Parse`. The usage is unchanged except you must type cast the claims property. + +The old example for parsing a token looked like this.. + +```go + if token, err := jwt.Parse(tokenString, keyLookupFunc); err == nil { + fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"]) + } +``` + +is now directly mapped to... + +```go + if token, err := jwt.Parse(tokenString, keyLookupFunc); err == nil { + claims := token.Claims.(jwt.MapClaims) + fmt.Printf("Token for user %v expires %v", claims["user"], claims["exp"]) + } +``` + +`StandardClaims` is designed to be embedded in your custom type. You can supply a custom claims type with the new `ParseWithClaims` function. Here's an example of using a custom claims type. + +```go + type MyCustomClaims struct { + User string + *StandardClaims + } + + if token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, keyLookupFunc); err == nil { + claims := token.Claims.(*MyCustomClaims) + fmt.Printf("Token for user %v expires %v", claims.User, claims.StandardClaims.ExpiresAt) + } +``` + +### `ParseFromRequest` has been moved + +To keep this library focused on the tokens without becoming overburdened with complex request processing logic, `ParseFromRequest` and its new companion `ParseFromRequestWithClaims` have been moved to a subpackage, `request`. The method signatues have also been augmented to receive a new argument: `Extractor`. + +`Extractors` do the work of picking the token string out of a request. The interface is simple and composable. + +This simple parsing example: + +```go + if token, err := jwt.ParseFromRequest(tokenString, req, keyLookupFunc); err == nil { + fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"]) + } +``` + +is directly mapped to: + +```go + if token, err := request.ParseFromRequest(tokenString, request.OAuth2Extractor, req, keyLookupFunc); err == nil { + fmt.Printf("Token for user %v expires %v", token.Claims["user"], token.Claims["exp"]) + } +``` + +There are several concrete `Extractor` types provided for your convenience: + +* `HeaderExtractor` will search a list of headers until one contains content. +* `ArgumentExtractor` will search a list of keys in request query and form arguments until one contains content. +* `MultiExtractor` will try a list of `Extractors` in order until one returns content. +* `AuthorizationHeaderExtractor` will look in the `Authorization` header for a `Bearer` token. +* `OAuth2Extractor` searches the places an OAuth2 token would be specified (per the spec): `Authorization` header and `access_token` argument +* `PostExtractionFilter` wraps an `Extractor`, allowing you to process the content before it's parsed. A simple example is stripping the `Bearer ` text from a header + + +### RSA signing methods no longer accept `[]byte` keys + +Due to a [critical vulnerability](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/), we've decided the convenience of accepting `[]byte` instead of `rsa.PublicKey` or `rsa.PrivateKey` isn't worth the risk of misuse. + +To replace this behavior, we've added two helper methods: `ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error)` and `ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error)`. These are just simple helpers for unpacking PEM encoded PKCS1 and PKCS8 keys. If your keys are encoded any other way, all you need to do is convert them to the `crypto/rsa` package's types. + +```go + func keyLookupFunc(*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"]) + } + + // Look up key + key, err := lookupPublicKey(token.Header["kid"]) + if err != nil { + return nil, err + } + + // Unpack key from PEM encoded PKCS8 + return jwt.ParseRSAPublicKeyFromPEM(key) + } +``` diff --git a/README.md b/README.md index 93203f9..10fc29d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A [go](http://www.golang.org) (or 'golang' for search engine friendliness) imple [![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go) -**BREAKING CHANGES COMING:*** Version 3.0.0 is almost complete. It will include _a lot_ of changes including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes will be available before 3.0.0 lands. If you would like to have any input befor 3.0.0 is locked, now's the time to review and provide feedback. +**BREAKING CHANGES:*** Version 3.0.0 is here. It includes _a lot_ of changes including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code. **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. @@ -43,63 +43,6 @@ 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. -## 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.MapClaims`. - -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.MapClaims) - 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. - -```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, &jwt.StandardClaims{}, keyFunc) - claims := token.Claims.(jwt.StandardClaims) - fmt.Println(claims.IssuedAt) -``` - ## Usage Tips ### Signing vs Encryption diff --git a/VERSION_HISTORY.md b/VERSION_HISTORY.md index b526ce3..993eac9 100644 --- a/VERSION_HISTORY.md +++ b/VERSION_HISTORY.md @@ -4,7 +4,6 @@ * **Compatibility Breaking Changes** * 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. - * Signature of `Keyfunc` is now `func(*Token) (interface{}, error)` * `ParseFromRequest` has been moved to `request` subpackage and usage has changed * 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. * Other Additions and Changes diff --git a/request/extractor.go b/request/extractor.go index 61c330a..14414fe 100644 --- a/request/extractor.go +++ b/request/extractor.go @@ -33,6 +33,7 @@ func (e HeaderExtractor) ExtractToken(req *http.Request) (string, error) { // Extract token from request arguments. This includes a POSTed form or // GET URL arguments. Argument names are tried in order until there's a match. +// This extractor calls `ParseMultipartForm` on the request type ArgumentExtractor []string func (e ArgumentExtractor) ExtractToken(req *http.Request) (string, error) { From 5fbf45924d057121148b8b962a213360beb022ec Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Wed, 15 Jun 2016 16:42:50 -0700 Subject: [PATCH 50/51] errors only have an exposed Inner property if the error was generated by another library --- errors.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/errors.go b/errors.go index d4b183c..662df19 100644 --- a/errors.go +++ b/errors.go @@ -30,7 +30,7 @@ const ( // Helper for constructing a ValidationError with a string error message func NewValidationError(errorText string, errorFlags uint32) *ValidationError { return &ValidationError{ - Inner: errors.New(errorText), + text: errorText, Errors: errorFlags, } } @@ -39,11 +39,16 @@ func NewValidationError(errorText string, errorFlags uint32) *ValidationError { type ValidationError struct { Inner error // stores the error returned by external dependencies, i.e.: KeyFunc Errors uint32 // bitfield. see ValidationError... constants + text string // errors that do not have a valid error just have text } // Validation error is an error type func (e ValidationError) Error() string { - if e.Inner == nil { + if e.Inner != nil { + return e.Inner.Error() + } else if e.text != "" { + return e.text + } else { return "token is invalid" } return e.Inner.Error() From 4638392845e7c0b347923a9ffc13799c80b72ce4 Mon Sep 17 00:00:00 2001 From: Dave Grijalva Date: Wed, 15 Jun 2016 16:56:01 -0700 Subject: [PATCH 51/51] updated documentation to reflect the supported draft version --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 10fc29d..00f6136 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-jones-json-web-token.html) +A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html) [![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go) @@ -35,6 +35,12 @@ This library publishes all the necessary components for adding your own signing Here's an example of an extension that integrates with the Google App Engine signing tools: https://github.com/someone1/gcp-jwt-go +## Compliance + +This library was last reviewed to comply with [RTF 7519](http://www.rfc-editor.org/info/rfc7519) dated May 2015 with a few notable differences: + +* In order to protect against accidental use of [Unsecured JWTs](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#UnsecuredJWT), tokens using `alg=none` will only be accepted if the constant `jwt.UnsafeAllowNoneSignatureType` is provided as the key. + ## 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).