From 3de6fae0b265b3a0bac42cfdbb0a8e4eb906b678 Mon Sep 17 00:00:00 2001 From: "Banse, Christian" Date: Fri, 28 May 2021 11:55:37 +0200 Subject: [PATCH 01/21] Starting development of RFC7519Claims --- claims.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++-- claims_test.go | 24 +++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 claims_test.go diff --git a/claims.go b/claims.go index 1133fd45..b4ce4adc 100644 --- a/claims.go +++ b/claims.go @@ -3,6 +3,8 @@ package jwt import ( "crypto/subtle" "fmt" + "math" + "strconv" "time" ) @@ -12,9 +14,56 @@ type Claims interface { Valid() error } -// Structured version of Claims Section, as referenced at -// https://tools.ietf.org/html/rfc7519#section-4.1 +// NumericDate represents a JSON numeric value, as referenced at +// https://datatracker.ietf.org/doc/html/rfc7519#section-2. +type NumericDate struct { + time.Time +} + +func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { + var ( + f float64 + seconds float64 + frac float64 + ) + + // since this can be a non-integer, we parse it as float and construct a time.Time object out if + + if f, err = strconv.ParseFloat(string(b), 64); err != nil { + // TODO(oxisto): This makes use of the new errors API introduced in 1.13, might need to remove it agian + return fmt.Errorf("could not parse NumericData: %w", err) + } + + seconds, frac = math.Modf(f) + + (*date).Time = time.Unix(int64(seconds), int64(frac*1e9)) + + return nil +} + +// RFC7519Claims are a 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 RFC7519Claims struct { + Audience []string `json:"aud,omitempty"` + ExpiresAt NumericDate `json:"exp,omitempty"` + Id string `json:"jti,omitempty"` + IssuedAt NumericDate `json:"iat,omitempty"` + Issuer string `json:"iss,omitempty"` + NotBefore NumericDate `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` +} + +// StandardClaims are a structured version of Claims Section, as referenced at +// https://tools.ietf.org/html/rfc7519#section-4.1. They do not follow the +// specification exactly, since they were based on an earlier draft of the +// specification and not updated. The main difference is that they only +// support integer-based date fields and singular audiances. +// // See examples for how to use this with your own claim types +// +// Deprecated: Use RFC7519Claims instead. type StandardClaims struct { Audience string `json:"aud,omitempty"` ExpiresAt int64 `json:"exp,omitempty"` diff --git a/claims_test.go b/claims_test.go new file mode 100644 index 00000000..a27f3f30 --- /dev/null +++ b/claims_test.go @@ -0,0 +1,24 @@ +package jwt_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/dgrijalva/jwt-go" +) + +func TestNumericDate(t *testing.T) { + var s struct { + Iat jwt.NumericDate `json:"iat"` + Exp jwt.NumericDate `json:"exp"` + } + + err := json.Unmarshal([]byte(`{"iat": 1516239022, "exp": 1516239022.1234567}`), &s) + + if err != nil { + t.Errorf("Unexpected error: %s", err) + } + + fmt.Printf("%+v", s) +} From 56ebc296ae7311a70297d5f383c94b0d9a2e250c Mon Sep 17 00:00:00 2001 From: "Banse, Christian" Date: Fri, 28 May 2021 12:24:59 +0200 Subject: [PATCH 02/21] Migrating internal verify functions to time.Time --- claims.go | 143 ++++++++++++++++--------- map_claims.go | 55 +++++++--- numeric_date.go | 43 ++++++++ claims_test.go => numeric_date_test.go | 0 4 files changed, 177 insertions(+), 64 deletions(-) create mode 100644 numeric_date.go rename claims_test.go => numeric_date_test.go (100%) diff --git a/claims.go b/claims.go index b4ce4adc..b2f5dbd0 100644 --- a/claims.go +++ b/claims.go @@ -3,8 +3,6 @@ package jwt import ( "crypto/subtle" "fmt" - "math" - "strconv" "time" ) @@ -14,45 +12,71 @@ type Claims interface { Valid() error } -// NumericDate represents a JSON numeric value, as referenced at -// https://datatracker.ietf.org/doc/html/rfc7519#section-2. -type NumericDate struct { - time.Time +// RFC7519Claims are a 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 RFC7519Claims struct { + Audience []string `json:"aud,omitempty"` + ExpiresAt *NumericDate `json:"exp,omitempty"` + Id string `json:"jti,omitempty"` + IssuedAt *NumericDate `json:"iat,omitempty"` + Issuer string `json:"iss,omitempty"` + NotBefore *NumericDate `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` } -func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { - var ( - f float64 - seconds float64 - frac float64 - ) +// Valid 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 RFC7519Claims) Valid() error { + vErr := new(ValidationError) + now := TimeFunc() - // since this can be a non-integer, we parse it as float and construct a time.Time object out if + // 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) { + delta := now.Sub(c.ExpiresAt.Time) + vErr.Inner = fmt.Errorf("token is expired by %v", delta) + vErr.Errors |= ValidationErrorExpired + } - if f, err = strconv.ParseFloat(string(b), 64); err != nil { - // TODO(oxisto): This makes use of the new errors API introduced in 1.13, might need to remove it agian - return fmt.Errorf("could not parse NumericData: %w", err) + /*if !c.VerifyIssuedAt(now, false) { + vErr.Inner = fmt.Errorf("Token used before issued") + vErr.Errors |= ValidationErrorIssuedAt } - seconds, frac = math.Modf(f) + if !c.VerifyNotBefore(now, false) { + vErr.Inner = fmt.Errorf("token is not valid yet") + vErr.Errors |= ValidationErrorNotValidYet + } - (*date).Time = time.Unix(int64(seconds), int64(frac*1e9)) + if vErr.valid() { + return nil + }*/ - return nil + return vErr } -// RFC7519Claims are a 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 RFC7519Claims struct { - Audience []string `json:"aud,omitempty"` - ExpiresAt NumericDate `json:"exp,omitempty"` - Id string `json:"jti,omitempty"` - IssuedAt NumericDate `json:"iat,omitempty"` - Issuer string `json:"iss,omitempty"` - NotBefore NumericDate `json:"nbf,omitempty"` - Subject string `json:"sub,omitempty"` +// VerifyExpiresAt compares the exp claim against cmp. If required is false, this method +// will return true if the value matches or is unset +func (c *RFC7519Claims) VerifyExpiresAt(cmp time.Time, req bool) bool { + if c.ExpiresAt == nil { + verifyExp(nil, cmp, req) + } + + return verifyExp(&c.ExpiresAt.Time, cmp, req) +} + +// VerifyIssuedAt compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *RFC7519Claims) VerifyIssuedAt(cmp time.Time, req bool) bool { + if c.IssuedAt == nil { + return verifyIat(nil, cmp, req) + } + + return verifyIat(&c.IssuedAt.Time, cmp, req) } // StandardClaims are a structured version of Claims Section, as referenced at @@ -116,25 +140,40 @@ func (c *StandardClaims) VerifyAudience(cmp string, req bool) 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) + if c.ExpiresAt == 0 { + return verifyExp(nil, time.Unix(cmp, 0), req) + } + + t := time.Unix(c.ExpiresAt, 0) + return verifyExp(&t, time.Unix(cmp, 0), 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) -} + if c.IssuedAt == 0 { + return verifyIat(nil, time.Unix(cmp, 0), 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) + t := time.Unix(c.IssuedAt, 0) + return verifyIat(&t, time.Unix(cmp, 0), 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) + if c.NotBefore == 0 { + return verifyNbf(nil, time.Unix(cmp, 0), req) + } + + t := time.Unix(c.NotBefore, 0) + return verifyNbf(&t, time.Unix(cmp, 0), 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) } // ----- helpers @@ -150,18 +189,25 @@ func verifyAud(aud string, cmp string, required bool) bool { } } -func verifyExp(exp int64, now int64, required bool) bool { - if exp == 0 { +func verifyExp(exp *time.Time, now time.Time, required bool) bool { + if exp == nil { return !required } - return now <= exp + return now.Before(*exp) || now.Equal(*exp) } -func verifyIat(iat int64, now int64, required bool) bool { - if iat == 0 { +func verifyIat(iat *time.Time, now time.Time, required bool) bool { + if iat == nil { return !required } - return now >= iat + return now.After(*iat) || now.Equal(*iat) +} + +func verifyNbf(nbf *time.Time, now time.Time, required bool) bool { + if nbf == nil { + return !required + } + return now.After(*nbf) || now.Equal(*nbf) } func verifyIss(iss string, cmp string, required bool) bool { @@ -174,10 +220,3 @@ func verifyIss(iss string, cmp string, required bool) bool { return false } } - -func verifyNbf(nbf int64, now int64, required bool) bool { - if nbf == 0 { - return !required - } - return now >= nbf -} diff --git a/map_claims.go b/map_claims.go index e1d6f3fa..a40e446f 100644 --- a/map_claims.go +++ b/map_claims.go @@ -3,6 +3,7 @@ package jwt import ( "encoding/json" "errors" + "time" // "fmt" ) @@ -20,26 +21,46 @@ func (m MapClaims) VerifyAudience(cmp string, req bool) bool { // 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 { + cmpTime := time.Unix(cmp, 0) + switch exp := m["exp"].(type) { case float64: - return verifyExp(int64(exp), cmp, req) + if exp == 0 { + return verifyExp(nil, cmpTime, req) + } + + t := timeFromFloat(exp) + return verifyExp(&t, cmpTime, req) case json.Number: - v, _ := exp.Int64() - return verifyExp(v, cmp, req) + v, _ := exp.Float64() + + t := timeFromFloat(v) + return verifyExp(&t, cmpTime, req) } + return !req } -// Compares the iat claim against cmp. +// VerifyIssuedAt 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) { + cmpTime := time.Unix(cmp, 0) + + switch exp := m["iat"].(type) { case float64: - return verifyIat(int64(iat), cmp, req) + if exp == 0 { + return verifyIat(nil, cmpTime, req) + } + + t := timeFromFloat(exp) + return verifyIat(&t, cmpTime, req) case json.Number: - v, _ := iat.Int64() - return verifyIat(v, cmp, req) + v, _ := exp.Float64() + + t := timeFromFloat(v) + return verifyIat(&t, cmpTime, req) } + return !req } @@ -53,13 +74,23 @@ func (m MapClaims) VerifyIssuer(cmp string, req bool) bool { // 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) { + cmpTime := time.Unix(cmp, 0) + + switch exp := m["nbf"].(type) { case float64: - return verifyNbf(int64(nbf), cmp, req) + if exp == 0 { + return verifyNbf(nil, cmpTime, req) + } + + t := timeFromFloat(exp) + return verifyNbf(&t, cmpTime, req) case json.Number: - v, _ := nbf.Int64() - return verifyNbf(v, cmp, req) + v, _ := exp.Float64() + + t := timeFromFloat(v) + return verifyNbf(&t, cmpTime, req) } + return !req } diff --git a/numeric_date.go b/numeric_date.go new file mode 100644 index 00000000..205b8a72 --- /dev/null +++ b/numeric_date.go @@ -0,0 +1,43 @@ +package jwt + +import ( + "fmt" + "math" + "strconv" + "time" +) + +// NumericDate represents a JSON numeric value, as referenced at +// https://datatracker.ietf.org/doc/html/rfc7519#section-2. +type NumericDate struct { + time.Time +} + +func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { + var ( + f float64 + ) + + // since this can be a non-integer, we parse it as float and construct a time.Time object out if + + // TODO(oxisto): Another approach would be to use json.Unmarshal into a json.Number + if f, err = strconv.ParseFloat(string(b), 64); err != nil { + // TODO(oxisto): This makes use of the new errors API introduced in 1.13, might need to remove it again + return fmt.Errorf("could not parse NumericData: %w", err) + } + + (*date).Time = timeFromFloat(f) + + return nil +} + +func timeFromFloat(f float64) time.Time { + var ( + seconds float64 + frac float64 + ) + + seconds, frac = math.Modf(f) + + return time.Unix(int64(seconds), int64(frac*1e9)) +} diff --git a/claims_test.go b/numeric_date_test.go similarity index 100% rename from claims_test.go rename to numeric_date_test.go From 9bd4bcb696a0fd4b0ca80f4592ccfbd3ec9d97a5 Mon Sep 17 00:00:00 2001 From: "Banse, Christian" Date: Fri, 28 May 2021 12:52:12 +0200 Subject: [PATCH 03/21] Implementing marshalling back. Still some problem with fractions --- claims.go | 14 ++++++++++++-- numeric_date.go | 14 ++++++++++++++ numeric_date_test.go | 15 +++++++++++++-- parser_test.go | 15 +++++++++++++++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/claims.go b/claims.go index b2f5dbd0..74bbf927 100644 --- a/claims.go +++ b/claims.go @@ -42,7 +42,7 @@ func (c RFC7519Claims) Valid() error { vErr.Errors |= ValidationErrorExpired } - /*if !c.VerifyIssuedAt(now, false) { + if !c.VerifyIssuedAt(now, false) { vErr.Inner = fmt.Errorf("Token used before issued") vErr.Errors |= ValidationErrorIssuedAt } @@ -54,7 +54,7 @@ func (c RFC7519Claims) Valid() error { if vErr.valid() { return nil - }*/ + } return vErr } @@ -79,6 +79,16 @@ func (c *RFC7519Claims) VerifyIssuedAt(cmp time.Time, req bool) bool { return verifyIat(&c.IssuedAt.Time, cmp, req) } +// VerifyNotBefore compares the nbf claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *RFC7519Claims) VerifyNotBefore(cmp time.Time, req bool) bool { + if c.NotBefore == nil { + return verifyNbf(nil, cmp, req) + } + + return verifyNbf(&c.NotBefore.Time, cmp, req) +} + // StandardClaims are a structured version of Claims Section, as referenced at // https://tools.ietf.org/html/rfc7519#section-4.1. They do not follow the // specification exactly, since they were based on an earlier draft of the diff --git a/numeric_date.go b/numeric_date.go index 205b8a72..1b7f7f22 100644 --- a/numeric_date.go +++ b/numeric_date.go @@ -13,6 +13,20 @@ type NumericDate struct { time.Time } +func (date NumericDate) MarshalJSON() (b []byte, err error) { + + // only serialize as float, if we actually have nanoseconds + if date.Nanosecond() != 0 { + f := float64(date.UnixNano()) / 1e9 + + b = []byte(strconv.FormatFloat(f, 'f', 6, 64)) + } else { + b = []byte(strconv.FormatInt(date.Unix(), 10)) + } + + return +} + func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { var ( f float64 diff --git a/numeric_date_test.go b/numeric_date_test.go index a27f3f30..fb19904b 100644 --- a/numeric_date_test.go +++ b/numeric_date_test.go @@ -14,11 +14,22 @@ func TestNumericDate(t *testing.T) { Exp jwt.NumericDate `json:"exp"` } - err := json.Unmarshal([]byte(`{"iat": 1516239022, "exp": 1516239022.1234567}`), &s) + raw := `{"iat":1516239022,"exp":1516239022.123456}` + + err := json.Unmarshal([]byte(raw), &s) if err != nil { t.Errorf("Unexpected error: %s", err) } - fmt.Printf("%+v", s) + fmt.Printf("%+v\n", s) + + b, _ := json.Marshal(s) + + fmt.Printf("%s\n", string(raw)) + fmt.Printf("%s\n", string(b)) + + if raw != string(b) { + t.Errorf("Serialized format of numeric date mismatch. Expecting: %s Got: %s", string(raw), string(b)) + } } diff --git a/parser_test.go b/parser_test.go index 39077978..a062c6b9 100644 --- a/parser_test.go +++ b/parser_test.go @@ -181,6 +181,17 @@ var jwtTestData = []struct { 0, &jwt.Parser{UseJSONNumber: true, SkipClaimsValidation: true}, }, + { + "RFC7519 Claims", + "", + defaultKeyFunc, + &jwt.RFC7519Claims{ + ExpiresAt: &jwt.NumericDate{time.Now().Add(time.Second * 10)}, + }, + true, + 0, + &jwt.Parser{UseJSONNumber: true}, + }, } func TestParser_Parse(t *testing.T) { @@ -206,6 +217,8 @@ func TestParser_Parse(t *testing.T) { token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc) case *jwt.StandardClaims: token, err = parser.ParseWithClaims(data.tokenString, &jwt.StandardClaims{}, data.keyfunc) + case *jwt.RFC7519Claims: + token, err = parser.ParseWithClaims(data.tokenString, &jwt.RFC7519Claims{}, data.keyfunc) } // Verify result matches expectation @@ -270,6 +283,8 @@ func TestParser_ParseUnverified(t *testing.T) { token, _, err = parser.ParseUnverified(data.tokenString, jwt.MapClaims{}) case *jwt.StandardClaims: token, _, err = parser.ParseUnverified(data.tokenString, &jwt.StandardClaims{}) + case *jwt.RFC7519Claims: + token, _, err = parser.ParseUnverified(data.tokenString, &jwt.RFC7519Claims{}) } if err != nil { From ee78d89caa0a0d57d887b7ef14b5255a54569062 Mon Sep 17 00:00:00 2001 From: "Banse, Christian" Date: Fri, 28 May 2021 14:36:13 +0200 Subject: [PATCH 04/21] Added time precision. Times are equal now, but test still fails --- numeric_date.go | 42 ++++++++++++++++++++++++------------------ numeric_date_test.go | 2 +- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/numeric_date.go b/numeric_date.go index 1b7f7f22..821a8cfe 100644 --- a/numeric_date.go +++ b/numeric_date.go @@ -1,46 +1,50 @@ package jwt import ( + "encoding/json" "fmt" "math" - "strconv" "time" ) +// TimePrecision sets the precision of times and dates within this library. +// This has an influence on the precision of times when comparing expiry or +// other related time fields. Furthermore, it is also the precision of times +// when serializing. +var TimePrecision = time.Microsecond + // NumericDate represents a JSON numeric value, as referenced at // https://datatracker.ietf.org/doc/html/rfc7519#section-2. type NumericDate struct { time.Time } -func (date NumericDate) MarshalJSON() (b []byte, err error) { +func FromTime(t time.Time) *NumericDate { + return &NumericDate{t.Truncate(TimePrecision)} +} - // only serialize as float, if we actually have nanoseconds - if date.Nanosecond() != 0 { - f := float64(date.UnixNano()) / 1e9 +func NewNumericDate(f float64) *NumericDate { + return FromTime(time.Unix(0, int64(f*float64(time.Second)))) +} - b = []byte(strconv.FormatFloat(f, 'f', 6, 64)) - } else { - b = []byte(strconv.FormatInt(date.Unix(), 10)) - } +func (date NumericDate) MarshalJSON() (b []byte, err error) { + f := float64(date.Truncate(TimePrecision).UnixNano()) / float64(time.Second) - return + return json.Marshal(f) } func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { - var ( - f float64 - ) + var number json.Number // since this can be a non-integer, we parse it as float and construct a time.Time object out if - - // TODO(oxisto): Another approach would be to use json.Unmarshal into a json.Number - if f, err = strconv.ParseFloat(string(b), 64); err != nil { + if err = json.Unmarshal(b, &number); err != nil { // TODO(oxisto): This makes use of the new errors API introduced in 1.13, might need to remove it again return fmt.Errorf("could not parse NumericData: %w", err) } - (*date).Time = timeFromFloat(f) + f, _ := number.Float64() + n := NewNumericDate(f) + *date = *n return nil } @@ -53,5 +57,7 @@ func timeFromFloat(f float64) time.Time { seconds, frac = math.Modf(f) - return time.Unix(int64(seconds), int64(frac*1e9)) + fmt.Printf("f: %f, sec: %f, frac: %f, nsec: %d, converted: %d\n", f, seconds, frac, int64(frac*float64(1e9)), int64(frac)) + + return time.Unix(int64(seconds), int64(frac*float64(1e9))) } diff --git a/numeric_date_test.go b/numeric_date_test.go index fb19904b..f9e6268c 100644 --- a/numeric_date_test.go +++ b/numeric_date_test.go @@ -14,7 +14,7 @@ func TestNumericDate(t *testing.T) { Exp jwt.NumericDate `json:"exp"` } - raw := `{"iat":1516239022,"exp":1516239022.123456}` + raw := `{"iat":1516239022,"exp":1516239022.12345}` err := json.Unmarshal([]byte(raw), &s) From 16b1f6701dcb5318217fe7fa0157f4a05d6829e8 Mon Sep 17 00:00:00 2001 From: "Banse, Christian" Date: Fri, 28 May 2021 14:43:48 +0200 Subject: [PATCH 05/21] Tests finally run through --- map_claims.go | 44 +++++++++++++++++++------------------------- numeric_date.go | 16 ++-------------- parser_test.go | 2 +- 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/map_claims.go b/map_claims.go index a40e446f..6d187fe0 100644 --- a/map_claims.go +++ b/map_claims.go @@ -29,13 +29,11 @@ func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { return verifyExp(nil, cmpTime, req) } - t := timeFromFloat(exp) - return verifyExp(&t, cmpTime, req) + return verifyExp(&NewNumericDate(exp).Time, cmpTime, req) case json.Number: v, _ := exp.Float64() - t := timeFromFloat(v) - return verifyExp(&t, cmpTime, req) + return verifyExp(&NewNumericDate(v).Time, cmpTime, req) } return !req @@ -46,54 +44,50 @@ func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { cmpTime := time.Unix(cmp, 0) - switch exp := m["iat"].(type) { + switch iat := m["iat"].(type) { case float64: - if exp == 0 { + if iat == 0 { return verifyIat(nil, cmpTime, req) } - t := timeFromFloat(exp) - return verifyIat(&t, cmpTime, req) + return verifyIat(&NewNumericDate(iat).Time, cmpTime, req) case json.Number: - v, _ := exp.Float64() + v, _ := iat.Float64() - t := timeFromFloat(v) - return verifyIat(&t, cmpTime, req) + return verifyIat(&NewNumericDate(v).Time, cmpTime, req) } return !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 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 { cmpTime := time.Unix(cmp, 0) - switch exp := m["nbf"].(type) { + switch nbf := m["nbf"].(type) { case float64: - if exp == 0 { + if nbf == 0 { return verifyNbf(nil, cmpTime, req) } - t := timeFromFloat(exp) - return verifyNbf(&t, cmpTime, req) + return verifyNbf(&NewNumericDate(nbf).Time, cmpTime, req) case json.Number: - v, _ := exp.Float64() + v, _ := nbf.Float64() - t := timeFromFloat(v) - return verifyNbf(&t, cmpTime, req) + return verifyNbf(&NewNumericDate(v).Time, cmpTime, req) } return !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 MapClaims) VerifyIssuer(cmp string, req bool) bool { + iss, _ := m["iss"].(string) + return verifyIss(iss, 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 diff --git a/numeric_date.go b/numeric_date.go index 821a8cfe..7e36c887 100644 --- a/numeric_date.go +++ b/numeric_date.go @@ -3,7 +3,6 @@ package jwt import ( "encoding/json" "fmt" - "math" "time" ) @@ -11,6 +10,8 @@ import ( // This has an influence on the precision of times when comparing expiry or // other related time fields. Furthermore, it is also the precision of times // when serializing. + +// TODO(oxisto): What would be a sensible default? Seconds? to be more backwards-compatible? var TimePrecision = time.Microsecond // NumericDate represents a JSON numeric value, as referenced at @@ -48,16 +49,3 @@ func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { return nil } - -func timeFromFloat(f float64) time.Time { - var ( - seconds float64 - frac float64 - ) - - seconds, frac = math.Modf(f) - - fmt.Printf("f: %f, sec: %f, frac: %f, nsec: %d, converted: %d\n", f, seconds, frac, int64(frac*float64(1e9)), int64(frac)) - - return time.Unix(int64(seconds), int64(frac*float64(1e9))) -} diff --git a/parser_test.go b/parser_test.go index a062c6b9..f3260ae9 100644 --- a/parser_test.go +++ b/parser_test.go @@ -186,7 +186,7 @@ var jwtTestData = []struct { "", defaultKeyFunc, &jwt.RFC7519Claims{ - ExpiresAt: &jwt.NumericDate{time.Now().Add(time.Second * 10)}, + ExpiresAt: jwt.FromTime(time.Now().Add(time.Second * 10)), }, true, 0, From f901bc5f9e51fdebf5d4b32819bea4521ee7191d Mon Sep 17 00:00:00 2001 From: "Banse, Christian" Date: Fri, 28 May 2021 14:50:48 +0200 Subject: [PATCH 06/21] Removed %w for older Go versions --- numeric_date.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/numeric_date.go b/numeric_date.go index 7e36c887..252e477b 100644 --- a/numeric_date.go +++ b/numeric_date.go @@ -14,7 +14,7 @@ import ( // TODO(oxisto): What would be a sensible default? Seconds? to be more backwards-compatible? var TimePrecision = time.Microsecond -// NumericDate represents a JSON numeric value, as referenced at +// NumericDate represents a JSON numeric date value, as referenced at // https://datatracker.ietf.org/doc/html/rfc7519#section-2. type NumericDate struct { time.Time @@ -37,10 +37,9 @@ func (date NumericDate) MarshalJSON() (b []byte, err error) { func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { var number json.Number - // since this can be a non-integer, we parse it as float and construct a time.Time object out if if err = json.Unmarshal(b, &number); err != nil { - // TODO(oxisto): This makes use of the new errors API introduced in 1.13, might need to remove it again - return fmt.Errorf("could not parse NumericData: %w", err) + // TODO(oxisto): Once we are on Go 1.13+, we should use %w here + return fmt.Errorf("could not parse NumericData: %s", err) } f, _ := number.Float64() From c9877f39ee6139fd228250f221563989d05da257 Mon Sep 17 00:00:00 2001 From: "Banse, Christian" Date: Fri, 28 May 2021 15:25:49 +0200 Subject: [PATCH 07/21] Setting default precision to seconds --- numeric_date.go | 4 ++-- numeric_date_test.go | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/numeric_date.go b/numeric_date.go index 252e477b..559cb598 100644 --- a/numeric_date.go +++ b/numeric_date.go @@ -11,8 +11,8 @@ import ( // other related time fields. Furthermore, it is also the precision of times // when serializing. -// TODO(oxisto): What would be a sensible default? Seconds? to be more backwards-compatible? -var TimePrecision = time.Microsecond +// TODO(oxisto): the tests seem to fail sometimes, if the precision is microseconds because the difference is literally 1 microsecond +var TimePrecision = time.Second // NumericDate represents a JSON numeric date value, as referenced at // https://datatracker.ietf.org/doc/html/rfc7519#section-2. diff --git a/numeric_date_test.go b/numeric_date_test.go index f9e6268c..01a1b4da 100644 --- a/numeric_date_test.go +++ b/numeric_date_test.go @@ -2,8 +2,8 @@ package jwt_test import ( "encoding/json" - "fmt" "testing" + "time" "github.com/dgrijalva/jwt-go" ) @@ -14,6 +14,10 @@ func TestNumericDate(t *testing.T) { Exp jwt.NumericDate `json:"exp"` } + oldPrecision := jwt.TimePrecision + + jwt.TimePrecision = time.Microsecond + raw := `{"iat":1516239022,"exp":1516239022.12345}` err := json.Unmarshal([]byte(raw), &s) @@ -22,14 +26,11 @@ func TestNumericDate(t *testing.T) { t.Errorf("Unexpected error: %s", err) } - fmt.Printf("%+v\n", s) - b, _ := json.Marshal(s) - fmt.Printf("%s\n", string(raw)) - fmt.Printf("%s\n", string(b)) - if raw != string(b) { t.Errorf("Serialized format of numeric date mismatch. Expecting: %s Got: %s", string(raw), string(b)) } + + jwt.TimePrecision = oldPrecision } From 900b0cb68069da6397d2a56c6e154a2a4beaa5b6 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 29 May 2021 11:11:44 +0200 Subject: [PATCH 08/21] fixed import path --- numeric_date_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numeric_date_test.go b/numeric_date_test.go index 01a1b4da..3526f4bd 100644 --- a/numeric_date_test.go +++ b/numeric_date_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/dgrijalva/jwt-go" + "github.com/golang-jwt/jwt" ) func TestNumericDate(t *testing.T) { From 02021af26833810df7b825e28edb91de9810490e Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 29 May 2021 11:47:31 +0200 Subject: [PATCH 09/21] Stabilizing the public API Added documentation --- claims.go | 41 ++++++++++++++++++++++++++++++----------- map_claims.go | 12 ++++++------ numeric_date.go | 38 +++++++++++++++++++++++++++++--------- parser_test.go | 2 +- 4 files changed, 66 insertions(+), 27 deletions(-) diff --git a/claims.go b/claims.go index 4aacf593..e8610528 100644 --- a/claims.go +++ b/claims.go @@ -12,18 +12,31 @@ type Claims interface { Valid() error } -// RFC7519Claims are a structured version of Claims Section, as referenced at -// https://tools.ietf.org/html/rfc7519#section-4.1. +// RFC7519Claims are a structured version of the JWT Claims Set, as referenced at +// https://datatracker.ietf.org/doc/html/rfc7519#section-4 // // See examples for how to use this with your own claim types type RFC7519Claims struct { - Audience []string `json:"aud,omitempty"` + // the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 + Issuer string `json:"iss,omitempty"` + + // the `sub (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2 + Subject string `json:"sub,omitempty"` + + // the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3 + Audience []string `json:"aud,omitempty"` + + // the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 ExpiresAt *NumericDate `json:"exp,omitempty"` - Id string `json:"jti,omitempty"` - IssuedAt *NumericDate `json:"iat,omitempty"` - Issuer string `json:"iss,omitempty"` + + // the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5 NotBefore *NumericDate `json:"nbf,omitempty"` - Subject string `json:"sub,omitempty"` + + // the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6 + IssuedAt *NumericDate `json:"iat,omitempty"` + + // the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7 + ID string `json:"jti,omitempty"` } // Valid validates time based claims "exp, iat, nbf". @@ -59,8 +72,14 @@ func (c RFC7519Claims) Valid() error { return vErr } -// VerifyExpiresAt compares the exp claim against cmp. If required is false, this method -// will return true if the value matches or is unset +// VerifyAudience compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *RFC7519Claims) VerifyAudience(cmp string, req bool) bool { + return verifyAud(c.Audience, cmp, req) +} + +// VerifyExpiresAt compares the exp claim against cmp. +// If required is false, this method will return true if the value matches or is unset func (c *RFC7519Claims) VerifyExpiresAt(cmp time.Time, req bool) bool { if c.ExpiresAt == nil { verifyExp(nil, cmp, req) @@ -89,8 +108,8 @@ func (c *RFC7519Claims) VerifyNotBefore(cmp time.Time, req bool) bool { return verifyNbf(&c.NotBefore.Time, cmp, req) } -// StandardClaims are a structured version of Claims Section, as referenced at -// https://tools.ietf.org/html/rfc7519#section-4.1. They do not follow the +// StandardClaims are a structured version of the JWT Claims Set, as referenced at +// https://datatracker.ietf.org/doc/html/rfc7519#section-4. They do not follow the // specification exactly, since they were based on an earlier draft of the // specification and not updated. The main difference is that they only // support integer-based date fields and singular audiances. diff --git a/map_claims.go b/map_claims.go index 2aaac6f1..2ab02954 100644 --- a/map_claims.go +++ b/map_claims.go @@ -43,11 +43,11 @@ func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { return verifyExp(nil, cmpTime, req) } - return verifyExp(&NewNumericDate(exp).Time, cmpTime, req) + return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req) case json.Number: v, _ := exp.Float64() - return verifyExp(&NewNumericDate(v).Time, cmpTime, req) + return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req) } return !req @@ -64,11 +64,11 @@ func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { return verifyIat(nil, cmpTime, req) } - return verifyIat(&NewNumericDate(iat).Time, cmpTime, req) + return verifyIat(&newNumericDateFromSeconds(iat).Time, cmpTime, req) case json.Number: v, _ := iat.Float64() - return verifyIat(&NewNumericDate(v).Time, cmpTime, req) + return verifyIat(&newNumericDateFromSeconds(v).Time, cmpTime, req) } return !req @@ -85,11 +85,11 @@ func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool { return verifyNbf(nil, cmpTime, req) } - return verifyNbf(&NewNumericDate(nbf).Time, cmpTime, req) + return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req) case json.Number: v, _ := nbf.Float64() - return verifyNbf(&NewNumericDate(v).Time, cmpTime, req) + return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req) } return !req diff --git a/numeric_date.go b/numeric_date.go index 559cb598..ffabea1e 100644 --- a/numeric_date.go +++ b/numeric_date.go @@ -3,6 +3,7 @@ package jwt import ( "encoding/json" "fmt" + "strconv" "time" ) @@ -10,7 +11,10 @@ import ( // This has an influence on the precision of times when comparing expiry or // other related time fields. Furthermore, it is also the precision of times // when serializing. - +// +// For backwards compatibility the default precision is set to seconds, so that +// no fractional timestamps are generated. +// // TODO(oxisto): the tests seem to fail sometimes, if the precision is microseconds because the difference is literally 1 microsecond var TimePrecision = time.Second @@ -20,30 +24,46 @@ type NumericDate struct { time.Time } -func FromTime(t time.Time) *NumericDate { +// NewNumericDate constructs a new *NumericDate from a standard library time.Time struct. +// It will truncate the timestamp according to the precision specified in TimePrecision. +func NewNumericDate(t time.Time) *NumericDate { return &NumericDate{t.Truncate(TimePrecision)} } -func NewNumericDate(f float64) *NumericDate { - return FromTime(time.Unix(0, int64(f*float64(time.Second)))) +// newNumericDateFromSeconds creates a new *NumericDate out of a float64 representing a +// UNIX epoch with the float fraction representing non-integer seconds. +func newNumericDateFromSeconds(f float64) *NumericDate { + return NewNumericDate(time.Unix(0, int64(f*float64(time.Second)))) } +// MarshalJSON is an implementation of the json.RawMessage interface and serializes the UNIX epoch +// represented in NumericDate to a byte array, using the precision specified in TimePrecision. func (date NumericDate) MarshalJSON() (b []byte, err error) { f := float64(date.Truncate(TimePrecision).UnixNano()) / float64(time.Second) - return json.Marshal(f) + return []byte(strconv.FormatFloat(f, 'f', -1, 64)), nil } +// UnmarshalJSON is an implementation of the json.RawMessage interface and deserializses a +// NumericDate from a JSON representation, i.e. a json.Number. This number represents an UNIX epoch +// with either integer or non-integer seconds. func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { - var number json.Number + var ( + number json.Number + f float64 + ) if err = json.Unmarshal(b, &number); err != nil { - // TODO(oxisto): Once we are on Go 1.13+, we should use %w here + // TODO(oxisto): Once we are on Go 1.13+, we should use %w instead of %s here return fmt.Errorf("could not parse NumericData: %s", err) } - f, _ := number.Float64() - n := NewNumericDate(f) + if f, err = number.Float64(); err != nil { + // TODO(oxisto): Once we are on Go 1.13+, we should use %w instead of %s here + return fmt.Errorf("could not convert json number value to float: %s", err) + } + + n := newNumericDateFromSeconds(f) *date = *n return nil diff --git a/parser_test.go b/parser_test.go index 0ed7df33..714c2133 100644 --- a/parser_test.go +++ b/parser_test.go @@ -186,7 +186,7 @@ var jwtTestData = []struct { "", defaultKeyFunc, &jwt.RFC7519Claims{ - ExpiresAt: jwt.FromTime(time.Now().Add(time.Second * 10)), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 10)), }, true, 0, From 29000da7e1d56385f9e1c1fc42ab5944cfff2361 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 29 May 2021 12:44:29 +0200 Subject: [PATCH 10/21] Renamed RFC7519Claims to RegisteredClaims --- claims.go | 25 +++++++++++++++---------- parser_test.go | 10 +++++----- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/claims.go b/claims.go index e8610528..39d78796 100644 --- a/claims.go +++ b/claims.go @@ -12,11 +12,16 @@ type Claims interface { Valid() error } -// RFC7519Claims are a structured version of the JWT Claims Set, as referenced at -// https://datatracker.ietf.org/doc/html/rfc7519#section-4 +// RegisteredClaims are a structured version of the JWT Claims Set, +// restricted to Registered Claim Names, as referenced at +// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 // -// See examples for how to use this with your own claim types -type RFC7519Claims struct { +// This type can be used on its own, but then additional private and +// public claims embedded in the JWT will not be parsed. The typical usecase +// therefore is to embedded this in a user-defined claim type. +// +// See examples for how to use this with your own claim types. +type RegisteredClaims struct { // the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 Issuer string `json:"iss,omitempty"` @@ -43,7 +48,7 @@ type RFC7519Claims struct { // 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 RFC7519Claims) Valid() error { +func (c RegisteredClaims) Valid() error { vErr := new(ValidationError) now := TimeFunc() @@ -74,13 +79,13 @@ func (c RFC7519Claims) Valid() error { // VerifyAudience compares the aud claim against cmp. // If required is false, this method will return true if the value matches or is unset -func (c *RFC7519Claims) VerifyAudience(cmp string, req bool) bool { +func (c *RegisteredClaims) VerifyAudience(cmp string, req bool) bool { return verifyAud(c.Audience, cmp, req) } // VerifyExpiresAt compares the exp claim against cmp. // If required is false, this method will return true if the value matches or is unset -func (c *RFC7519Claims) VerifyExpiresAt(cmp time.Time, req bool) bool { +func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool { if c.ExpiresAt == nil { verifyExp(nil, cmp, req) } @@ -90,7 +95,7 @@ func (c *RFC7519Claims) VerifyExpiresAt(cmp time.Time, req bool) bool { // VerifyIssuedAt compares the iat claim against cmp. // If required is false, this method will return true if the value matches or is unset -func (c *RFC7519Claims) VerifyIssuedAt(cmp time.Time, req bool) bool { +func (c *RegisteredClaims) VerifyIssuedAt(cmp time.Time, req bool) bool { if c.IssuedAt == nil { return verifyIat(nil, cmp, req) } @@ -100,7 +105,7 @@ func (c *RFC7519Claims) VerifyIssuedAt(cmp time.Time, req bool) bool { // VerifyNotBefore compares the nbf claim against cmp. // If required is false, this method will return true if the value matches or is unset -func (c *RFC7519Claims) VerifyNotBefore(cmp time.Time, req bool) bool { +func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool { if c.NotBefore == nil { return verifyNbf(nil, cmp, req) } @@ -116,7 +121,7 @@ func (c *RFC7519Claims) VerifyNotBefore(cmp time.Time, req bool) bool { // // See examples for how to use this with your own claim types // -// Deprecated: Use RFC7519Claims instead. +// Deprecated: Use RegisteredClaims instead for a forward-compatible way to access claims in a struct. type StandardClaims struct { Audience string `json:"aud,omitempty"` ExpiresAt int64 `json:"exp,omitempty"` diff --git a/parser_test.go b/parser_test.go index 714c2133..a6722855 100644 --- a/parser_test.go +++ b/parser_test.go @@ -185,7 +185,7 @@ var jwtTestData = []struct { "RFC7519 Claims", "", defaultKeyFunc, - &jwt.RFC7519Claims{ + &jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 10)), }, true, @@ -217,8 +217,8 @@ func TestParser_Parse(t *testing.T) { token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc) case *jwt.StandardClaims: token, err = parser.ParseWithClaims(data.tokenString, &jwt.StandardClaims{}, data.keyfunc) - case *jwt.RFC7519Claims: - token, err = parser.ParseWithClaims(data.tokenString, &jwt.RFC7519Claims{}, data.keyfunc) + case *jwt.RegisteredClaims: + token, err = parser.ParseWithClaims(data.tokenString, &jwt.RegisteredClaims{}, data.keyfunc) } // Verify result matches expectation @@ -283,8 +283,8 @@ func TestParser_ParseUnverified(t *testing.T) { token, _, err = parser.ParseUnverified(data.tokenString, jwt.MapClaims{}) case *jwt.StandardClaims: token, _, err = parser.ParseUnverified(data.tokenString, &jwt.StandardClaims{}) - case *jwt.RFC7519Claims: - token, _, err = parser.ParseUnverified(data.tokenString, &jwt.RFC7519Claims{}) + case *jwt.RegisteredClaims: + token, _, err = parser.ParseUnverified(data.tokenString, &jwt.RegisteredClaims{}) } if err != nil { From 514934291e7a023417cb70444e5fbef849e5ff59 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 29 May 2021 17:07:49 +0200 Subject: [PATCH 11/21] Adjusted the examples --- claims.go | 10 +++--- example_test.go | 75 +++++++++++++++++++++++--------------------- http_example_test.go | 8 ++--- rsa_pss_test.go | 4 +-- 4 files changed, 51 insertions(+), 46 deletions(-) diff --git a/claims.go b/claims.go index 39d78796..a28977ae 100644 --- a/claims.go +++ b/claims.go @@ -87,7 +87,7 @@ func (c *RegisteredClaims) VerifyAudience(cmp string, req bool) bool { // If required is false, this method will return true if the value matches or is unset func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool { if c.ExpiresAt == nil { - verifyExp(nil, cmp, req) + return verifyExp(nil, cmp, req) } return verifyExp(&c.ExpiresAt.Time, cmp, req) @@ -117,11 +117,11 @@ func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool { // https://datatracker.ietf.org/doc/html/rfc7519#section-4. They do not follow the // specification exactly, since they were based on an earlier draft of the // specification and not updated. The main difference is that they only -// support integer-based date fields and singular audiances. +// support integer-based date fields and singular audiances. This might lead to +// incompatibilities with other JWT implementations. The use of this is discouraged, instead +// the newer RegisteredClaims struct should be used. // -// See examples for how to use this with your own claim types -// -// Deprecated: Use RegisteredClaims instead for a forward-compatible way to access claims in a struct. +// Deprecated: Use RegisteredClaims instead for a forward-compatible way to access registered claims in a struct. type StandardClaims struct { Audience string `json:"aud,omitempty"` ExpiresAt int64 `json:"exp,omitempty"` diff --git a/example_test.go b/example_test.go index 5be4a296..dd844734 100644 --- a/example_test.go +++ b/example_test.go @@ -7,41 +7,57 @@ import ( "github.com/golang-jwt/jwt" ) -// Example (atypical) using the StandardClaims type by itself to parse a token. -// The StandardClaims type is designed to be embedded into your custom types +// Example (atypical) using the RegisteredClaims type by itself to parse a token. +// The RegisteredClaims 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() { +func ExampleNewWithClaims_registeredClaims() { mySigningKey := []byte("AllYourBase") // Create the Claims - claims := &jwt.StandardClaims{ - ExpiresAt: 15000, + claims := &jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)), Issuer: "test", } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) ss, err := token.SignedString(mySigningKey) fmt.Printf("%v %v", ss, err) - //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.QsODzZu3lUZMVdhbO76u3Jv02iYCvEHcYVUI1kOWEU0 + //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8 } -// 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. +// Example creating a token using a custom claims type. The RegisteredClaims is embedded +// in the custom type to allow for easy encoding, parsing and validation of registered claims. func ExampleNewWithClaims_customClaimsType() { mySigningKey := []byte("AllYourBase") type MyCustomClaims struct { Foo string `json:"foo"` - jwt.StandardClaims + jwt.RegisteredClaims } - // Create the Claims + // Create the claims claims := MyCustomClaims{ "bar", - jwt.StandardClaims{ - ExpiresAt: 15000, + jwt.RegisteredClaims{ + // A usual scenario is to set the expiration time relative to the current time + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "test", + Subject: "somebody", + ID: "1", + Audience: []string{"somebody_else"}, + }, + } + + // Create claims while leaving out some of the optional fields + claims = MyCustomClaims{ + "bar", + jwt.RegisteredClaims{ + // Also fixed dates can be used for the NumericDate + ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)), Issuer: "test", }, } @@ -49,42 +65,31 @@ func ExampleNewWithClaims_customClaimsType() { token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) ss, err := token.SignedString(mySigningKey) fmt.Printf("%v %v", ss, err) - //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c + + //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.xVuY2FZ_MRXMIEgVQ7J-TFtaucVFRXUzHm9LmV41goM } // 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" + tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0In0.Wvm9G-ihWYR90OoEav3TpxmRQ3oTk0Diqhsgd8hZPQ4" type MyCustomClaims struct { Foo string `json:"foo"` - jwt.StandardClaims + jwt.RegisteredClaims } - // sample token is expired. override time so it parses as valid - at(time.Unix(0, 0), func() { - token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte("AllYourBase"), nil - }) - - if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid { - fmt.Printf("%v %v", claims.Foo, claims.StandardClaims.ExpiresAt) - } else { - fmt.Println(err) - } + token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte("AllYourBase"), nil }) - // 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 + if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid { + fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer) + } else { + fmt.Println(err) } - f() - jwt.TimeFunc = time.Now + + // Output: bar test } // An example of parsing the error types using bitfield checks diff --git a/http_example_test.go b/http_example_test.go index 3b6330b5..7c2ce3ee 100644 --- a/http_example_test.go +++ b/http_example_test.go @@ -80,7 +80,7 @@ type CustomerInfo struct { } type CustomClaimsExample struct { - *jwt.StandardClaims + *jwt.RegisteredClaims TokenType string CustomerInfo } @@ -149,10 +149,10 @@ func createToken(user string) (string, error) { // set our claims t.Claims = &CustomClaimsExample{ - &jwt.StandardClaims{ + &jwt.RegisteredClaims{ // 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(), + // see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 1)), }, "level1", CustomerInfo{user, "human"}, diff --git a/rsa_pss_test.go b/rsa_pss_test.go index d1cafe71..9774da91 100644 --- a/rsa_pss_test.go +++ b/rsa_pss_test.go @@ -131,9 +131,9 @@ func TestRSAPSSSaltLengthCompatibility(t *testing.T) { } func makeToken(method jwt.SigningMethod) string { - token := jwt.NewWithClaims(method, jwt.StandardClaims{ + token := jwt.NewWithClaims(method, jwt.RegisteredClaims{ Issuer: "example", - IssuedAt: time.Now().Unix(), + IssuedAt: jwt.NewNumericDate(time.Now()), }) privateKey := test.LoadRSAPrivateKeyFromDisk("test/sample_key") signed, err := token.SignedString(privateKey) From 1cf9d8d961e19a8fe937381df028d56367d0dc56 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 29 May 2021 19:39:43 +0200 Subject: [PATCH 12/21] Fixed another typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 85684492..ffb91dbe 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Here's an example of an extension that integrates with multiple Google Cloud Pla ## Compliance -This library was last reviewed to comply with [RTF 7519](https://datatracker.ietf.org/doc/html/rfc7519) dated May 2015 with a few notable differences: +This library was last reviewed to comply with [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519) dated May 2015 with a few notable differences: * In order to protect against accidental use of [Unsecured JWTs](https://datatracker.ietf.org/doc/html/rfc7519#section-6), tokens using `alg=none` will only be accepted if the constant `jwt.UnsafeAllowNoneSignatureType` is provided as the key. From 4fb0bc571242e3ea32f09311bea412a7a72f6b31 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 23 Jun 2021 22:05:45 +0200 Subject: [PATCH 13/21] nit fix --- claims.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claims.go b/claims.go index a28977ae..f9a836aa 100644 --- a/claims.go +++ b/claims.go @@ -25,7 +25,7 @@ type RegisteredClaims struct { // the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 Issuer string `json:"iss,omitempty"` - // the `sub (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2 + // the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2 Subject string `json:"sub,omitempty"` // the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3 From 7301e3799853c5fdd0bac90a6bd22964d8b35e1c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 25 Jun 2021 22:22:16 +0200 Subject: [PATCH 14/21] Added a custom type for a string (array) --- claims.go | 2 +- example_test.go | 2 +- numeric_date.go => types.go | 31 +++++++++++++++++++++++---- numeric_date_test.go => types_test.go | 0 4 files changed, 29 insertions(+), 6 deletions(-) rename numeric_date.go => types.go (77%) rename numeric_date_test.go => types_test.go (100%) diff --git a/claims.go b/claims.go index f9a836aa..b1ef4bc8 100644 --- a/claims.go +++ b/claims.go @@ -29,7 +29,7 @@ type RegisteredClaims struct { Subject string `json:"sub,omitempty"` // the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3 - Audience []string `json:"aud,omitempty"` + Audience StringArray `json:"aud,omitempty"` // the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 ExpiresAt *NumericDate `json:"exp,omitempty"` diff --git a/example_test.go b/example_test.go index dd844734..ec68d6a6 100644 --- a/example_test.go +++ b/example_test.go @@ -72,7 +72,7 @@ func ExampleNewWithClaims_customClaimsType() { // 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.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0In0.Wvm9G-ihWYR90OoEav3TpxmRQ3oTk0Diqhsgd8hZPQ4" + tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA" type MyCustomClaims struct { Foo string `json:"foo"` diff --git a/numeric_date.go b/types.go similarity index 77% rename from numeric_date.go rename to types.go index ffabea1e..9400f1f4 100644 --- a/numeric_date.go +++ b/types.go @@ -3,6 +3,7 @@ package jwt import ( "encoding/json" "fmt" + "reflect" "strconv" "time" ) @@ -54,13 +55,11 @@ func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { ) if err = json.Unmarshal(b, &number); err != nil { - // TODO(oxisto): Once we are on Go 1.13+, we should use %w instead of %s here - return fmt.Errorf("could not parse NumericData: %s", err) + return fmt.Errorf("could not parse NumericData: %w", err) } if f, err = number.Float64(); err != nil { - // TODO(oxisto): Once we are on Go 1.13+, we should use %w instead of %s here - return fmt.Errorf("could not convert json number value to float: %s", err) + return fmt.Errorf("could not convert json number value to float: %w", err) } n := newNumericDateFromSeconds(f) @@ -68,3 +67,27 @@ func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { return nil } + +// StringArray is basically just a slice of strings, but it can be either serialized from a string array or just a string. +// This type is necessary, since the "aud" claim can either be a single string or an array. +type StringArray []string + +func (s *StringArray) UnmarshalJSON(data []byte) (err error) { + var value interface{} + + if err = json.Unmarshal(data, &value); err != nil { + return err + } + + switch v := value.(type) { + case string: + *s = StringArray{v} + return + case []string: + *s = StringArray(v) + default: + err = &json.UnsupportedTypeError{Type: reflect.TypeOf(v)} + } + + return +} diff --git a/numeric_date_test.go b/types_test.go similarity index 100% rename from numeric_date_test.go rename to types_test.go From 15ed3b71763fb4ad04b022c827544a0d63203047 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 30 Jul 2021 09:22:26 +0200 Subject: [PATCH 15/21] Added option how to serialize the StringArray type --- parser_test.go | 38 ++++++++++++++++++++++++++++++++++++++ types.go | 35 ++++++++++++++++++++++++++++++++--- types_test.go | 31 +++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/parser_test.go b/parser_test.go index 66775eb9..835a6440 100644 --- a/parser_test.go +++ b/parser_test.go @@ -192,6 +192,39 @@ var jwtTestData = []struct { 0, &jwt.Parser{UseJSONNumber: true}, }, + { + "RFC7519 Claims - single aud", + "", + defaultKeyFunc, + &jwt.RegisteredClaims{ + Audience: jwt.StringArray{"test"}, + }, + true, + 0, + &jwt.Parser{UseJSONNumber: true}, + }, + { + "RFC7519 Claims - multiple aud", + "", + defaultKeyFunc, + &jwt.RegisteredClaims{ + Audience: jwt.StringArray{"test", "test"}, + }, + true, + 0, + &jwt.Parser{UseJSONNumber: true}, + }, + { + "RFC7519 Claims - multiple aud with wrong types", + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdCIsMV19.htEBUf7BVbfSmVoTFjXf3y6DLmDUuLy1vTJ14_EX7Ws", // { "aud": ["test", 1] } + defaultKeyFunc, + &jwt.RegisteredClaims{ + Audience: nil, // because of the unmarshal error, this will be empty + }, + false, + jwt.ValidationErrorMalformed, + &jwt.Parser{UseJSONNumber: true}, + }, } func TestParser_Parse(t *testing.T) { @@ -265,6 +298,11 @@ func TestParser_ParseUnverified(t *testing.T) { // Iterate over test data set and run tests for _, data := range jwtTestData { + // Skip test data, that intentionally contains malformed tokens, as they would lead to an error + if data.errors&jwt.ValidationErrorMalformed != 0 { + continue + } + // If the token string is blank, use helper function to generate string if data.tokenString == "" { data.tokenString = test.MakeSampleToken(data.claims, privateKey) diff --git a/types.go b/types.go index 9400f1f4..951e9b49 100644 --- a/types.go +++ b/types.go @@ -19,6 +19,15 @@ import ( // TODO(oxisto): the tests seem to fail sometimes, if the precision is microseconds because the difference is literally 1 microsecond var TimePrecision = time.Second +// MarshalSingleStringAsArray modifies the behaviour of the StringArray type, especially +// its MarshalJSON function. +// +// If it is set to true (the default), it will always serialize the type as an +// array of strings, even if it just contains one element, defaulting to the behaviour +// of the underlying []string. If it is set to false, it will serialize to a single +// string, if it contains one element. Otherwise, it will serialize to an array of strings. +var MarshalSingleStringAsArray = true + // NumericDate represents a JSON numeric date value, as referenced at // https://datatracker.ietf.org/doc/html/rfc7519#section-2. type NumericDate struct { @@ -79,15 +88,35 @@ func (s *StringArray) UnmarshalJSON(data []byte) (err error) { return err } + var aud []string + switch v := value.(type) { case string: - *s = StringArray{v} - return + aud = append(aud, v) case []string: - *s = StringArray(v) + aud = StringArray(v) + case []interface{}: + for _, a := range v { + vs, ok := a.(string) + if !ok { + return &json.UnsupportedTypeError{Type: reflect.TypeOf(a)} + } + aud = append(aud, vs) + } default: err = &json.UnsupportedTypeError{Type: reflect.TypeOf(v)} + return } + *s = aud + return } + +func (s StringArray) MarshalJSON() (b []byte, err error) { + if len(s) == 1 && !MarshalSingleStringAsArray { + return json.Marshal(s[0]) + } + + return json.Marshal([]string(s)) +} diff --git a/types_test.go b/types_test.go index 3526f4bd..dd3b77d1 100644 --- a/types_test.go +++ b/types_test.go @@ -34,3 +34,34 @@ func TestNumericDate(t *testing.T) { jwt.TimePrecision = oldPrecision } + +func TestSingleArrayMarshal(t *testing.T) { + jwt.MarshalSingleStringAsArray = false + + s := jwt.StringArray{"test"} + expected := `"test"` + + b, err := json.Marshal(s) + + if err != nil { + t.Errorf("Unexpected error: %s", err) + } + + if expected != string(b) { + t.Errorf("Serialized format of string array mismatch. Expecting: %s Got: %s", string(expected), string(b)) + } + + jwt.MarshalSingleStringAsArray = true + + expected = `["test"]` + + b, err = json.Marshal(s) + + if err != nil { + t.Errorf("Unexpected error: %s", err) + } + + if expected != string(b) { + t.Errorf("Serialized format of string array mismatch. Expecting: %s Got: %s", string(expected), string(b)) + } +} From f8936f01c1b601c27e9fb55c4e0861a6fede1eb9 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 30 Jul 2021 09:33:01 +0200 Subject: [PATCH 16/21] Added one more test and some more explanation --- parser_test.go | 11 +++++++++++ types.go | 7 +++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/parser_test.go b/parser_test.go index 835a6440..22b08042 100644 --- a/parser_test.go +++ b/parser_test.go @@ -214,6 +214,17 @@ var jwtTestData = []struct { 0, &jwt.Parser{UseJSONNumber: true}, }, + { + "RFC7519 Claims - single aud with wrong type", + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOjF9.8mAIDUfZNQT3TGm1QFIQp91OCpJpQpbB1-m9pA2mkHc", // { "aud": 1 } + defaultKeyFunc, + &jwt.RegisteredClaims{ + Audience: nil, // because of the unmarshal error, this will be empty + }, + false, + jwt.ValidationErrorMalformed, + &jwt.Parser{UseJSONNumber: true}, + }, { "RFC7519 Claims - multiple aud with wrong types", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdCIsMV19.htEBUf7BVbfSmVoTFjXf3y6DLmDUuLy1vTJ14_EX7Ws", // { "aud": ["test", 1] } diff --git a/types.go b/types.go index 951e9b49..1664e6b7 100644 --- a/types.go +++ b/types.go @@ -104,8 +104,7 @@ func (s *StringArray) UnmarshalJSON(data []byte) (err error) { aud = append(aud, vs) } default: - err = &json.UnsupportedTypeError{Type: reflect.TypeOf(v)} - return + return &json.UnsupportedTypeError{Type: reflect.TypeOf(v)} } *s = aud @@ -114,6 +113,10 @@ func (s *StringArray) UnmarshalJSON(data []byte) (err error) { } func (s StringArray) MarshalJSON() (b []byte, err error) { + // This handles a special case in the JWT RFC. If the string array, e.g. used by the "aud" field, + // only contains one element, it MAY be serialized as a single string. This may or may not be + // desired based on the ecosystem of other JWT library used, so we make it configurable by the + // variable MarshalSingleStringAsArray. if len(s) == 1 && !MarshalSingleStringAsArray { return json.Marshal(s[0]) } From a528dfea3c7196dc3dfcd232b1d23f8180f34ce1 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 30 Jul 2021 09:45:52 +0200 Subject: [PATCH 17/21] Using t.Run for better testing --- parser_test.go | 160 +++++++++++++++++++++++++------------------------ 1 file changed, 82 insertions(+), 78 deletions(-) diff --git a/parser_test.go b/parser_test.go index 22b08042..cbf79769 100644 --- a/parser_test.go +++ b/parser_test.go @@ -243,64 +243,66 @@ 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 = test.MakeSampleToken(data.claims, privateKey) - } + t.Run(data.name, func(t *testing.T) { + // If the token string is blank, use helper function to generate string + if data.tokenString == "" { + data.tokenString = test.MakeSampleToken(data.claims, privateKey) + } - // Parse the token - var token *jwt.Token - var err error - 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, jwt.MapClaims{}, data.keyfunc) - case *jwt.StandardClaims: - token, err = parser.ParseWithClaims(data.tokenString, &jwt.StandardClaims{}, data.keyfunc) - case *jwt.RegisteredClaims: - token, err = parser.ParseWithClaims(data.tokenString, &jwt.RegisteredClaims{}, data.keyfunc) - } + // Parse the token + var token *jwt.Token + var err error + 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, jwt.MapClaims{}, data.keyfunc) + case *jwt.StandardClaims: + token, err = parser.ParseWithClaims(data.tokenString, &jwt.StandardClaims{}, data.keyfunc) + case *jwt.RegisteredClaims: + token, err = parser.ParseWithClaims(data.tokenString, &jwt.RegisteredClaims{}, data.keyfunc) + } - // 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) - } + // 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) + } - 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] 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.valid && err == nil { + 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", data.name) - } + if (err == nil && !token.Valid) || (err != nil && token.Valid) { + t.Errorf("[%v] Inconsistent behavior between returned error and token.Valid", data.name) + } - if data.errors != 0 { - if err == nil { - t.Errorf("[%v] Expecting error. Didn't get one.", data.name) - } else { + if data.errors != 0 { + if err == nil { + t.Errorf("[%v] Expecting error. Didn't get one.", data.name) + } else { - ve := err.(*jwt.ValidationError) - // compare the bitfield part of the error - if e := ve.Errors; e != data.errors { - t.Errorf("[%v] Errors don't match expectation. %v != %v", data.name, e, data.errors) - } + ve := err.(*jwt.ValidationError) + // compare the bitfield part of the error + if e := ve.Errors; e != data.errors { + t.Errorf("[%v] Errors don't match expectation. %v != %v", data.name, e, data.errors) + } - if err.Error() == keyFuncError.Error() && ve.Inner != keyFuncError { - t.Errorf("[%v] Inner error does not match expectation. %v != %v", data.name, ve.Inner, keyFuncError) + if err.Error() == keyFuncError.Error() && ve.Inner != keyFuncError { + t.Errorf("[%v] Inner error does not match expectation. %v != %v", data.name, ve.Inner, keyFuncError) + } } } - } - if data.valid && token.Signature == "" { - t.Errorf("[%v] Signature is left unpopulated after parsing", data.name) - } + if data.valid && token.Signature == "" { + t.Errorf("[%v] Signature is left unpopulated after parsing", data.name) + } + }) } } @@ -314,40 +316,42 @@ func TestParser_ParseUnverified(t *testing.T) { continue } - // If the token string is blank, use helper function to generate string - if data.tokenString == "" { - data.tokenString = test.MakeSampleToken(data.claims, privateKey) - } + t.Run(data.name, func(t *testing.T) { + // If the token string is blank, use helper function to generate string + if data.tokenString == "" { + data.tokenString = test.MakeSampleToken(data.claims, privateKey) + } - // Parse the token - var token *jwt.Token - var err error - 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.ParseUnverified(data.tokenString, jwt.MapClaims{}) - case *jwt.StandardClaims: - token, _, err = parser.ParseUnverified(data.tokenString, &jwt.StandardClaims{}) - case *jwt.RegisteredClaims: - token, _, err = parser.ParseUnverified(data.tokenString, &jwt.RegisteredClaims{}) - } + // Parse the token + var token *jwt.Token + var err error + 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.ParseUnverified(data.tokenString, jwt.MapClaims{}) + case *jwt.StandardClaims: + token, _, err = parser.ParseUnverified(data.tokenString, &jwt.StandardClaims{}) + case *jwt.RegisteredClaims: + token, _, err = parser.ParseUnverified(data.tokenString, &jwt.RegisteredClaims{}) + } - if err != nil { - t.Errorf("[%v] Invalid token", data.name) - } + if err != nil { + t.Errorf("[%v] Invalid token", data.name) + } - // 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) - } + // 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) + } - 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] Error while verifying token: %T:%v", data.name, err, err) + } + }) } } From 4fa46b809254d97dc5d702a5a2816a83f16bbe4d Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 31 Jul 2021 14:26:44 +0200 Subject: [PATCH 18/21] Using the type ClaimStrings instead of StringArray --- claims.go | 2 +- parser_test.go | 4 ++-- types.go | 20 +++++++++++--------- types_test.go | 2 +- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/claims.go b/claims.go index b1ef4bc8..11acc517 100644 --- a/claims.go +++ b/claims.go @@ -29,7 +29,7 @@ type RegisteredClaims struct { Subject string `json:"sub,omitempty"` // the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3 - Audience StringArray `json:"aud,omitempty"` + Audience ClaimStrings `json:"aud,omitempty"` // the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 ExpiresAt *NumericDate `json:"exp,omitempty"` diff --git a/parser_test.go b/parser_test.go index cbf79769..bb3a5451 100644 --- a/parser_test.go +++ b/parser_test.go @@ -197,7 +197,7 @@ var jwtTestData = []struct { "", defaultKeyFunc, &jwt.RegisteredClaims{ - Audience: jwt.StringArray{"test"}, + Audience: jwt.ClaimStrings{"test"}, }, true, 0, @@ -208,7 +208,7 @@ var jwtTestData = []struct { "", defaultKeyFunc, &jwt.RegisteredClaims{ - Audience: jwt.StringArray{"test", "test"}, + Audience: jwt.ClaimStrings{"test", "test"}, }, true, 0, diff --git a/types.go b/types.go index 1664e6b7..1ec6f15b 100644 --- a/types.go +++ b/types.go @@ -19,7 +19,7 @@ import ( // TODO(oxisto): the tests seem to fail sometimes, if the precision is microseconds because the difference is literally 1 microsecond var TimePrecision = time.Second -// MarshalSingleStringAsArray modifies the behaviour of the StringArray type, especially +// MarshalSingleStringAsArray modifies the behaviour of the ClaimStrings type, especially // its MarshalJSON function. // // If it is set to true (the default), it will always serialize the type as an @@ -77,11 +77,11 @@ func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { return nil } -// StringArray is basically just a slice of strings, but it can be either serialized from a string array or just a string. +// ClaimStrings is basically just a slice of strings, but it can be either serialized from a string array or just a string. // This type is necessary, since the "aud" claim can either be a single string or an array. -type StringArray []string +type ClaimStrings []string -func (s *StringArray) UnmarshalJSON(data []byte) (err error) { +func (s *ClaimStrings) UnmarshalJSON(data []byte) (err error) { var value interface{} if err = json.Unmarshal(data, &value); err != nil { @@ -94,15 +94,17 @@ func (s *StringArray) UnmarshalJSON(data []byte) (err error) { case string: aud = append(aud, v) case []string: - aud = StringArray(v) + aud = ClaimStrings(v) case []interface{}: - for _, a := range v { - vs, ok := a.(string) + for _, vv := range v { + vs, ok := vv.(string) if !ok { - return &json.UnsupportedTypeError{Type: reflect.TypeOf(a)} + return &json.UnsupportedTypeError{Type: reflect.TypeOf(vv)} } aud = append(aud, vs) } + case nil: + return nil default: return &json.UnsupportedTypeError{Type: reflect.TypeOf(v)} } @@ -112,7 +114,7 @@ func (s *StringArray) UnmarshalJSON(data []byte) (err error) { return } -func (s StringArray) MarshalJSON() (b []byte, err error) { +func (s ClaimStrings) MarshalJSON() (b []byte, err error) { // This handles a special case in the JWT RFC. If the string array, e.g. used by the "aud" field, // only contains one element, it MAY be serialized as a single string. This may or may not be // desired based on the ecosystem of other JWT library used, so we make it configurable by the diff --git a/types_test.go b/types_test.go index dd3b77d1..50b4c943 100644 --- a/types_test.go +++ b/types_test.go @@ -38,7 +38,7 @@ func TestNumericDate(t *testing.T) { func TestSingleArrayMarshal(t *testing.T) { jwt.MarshalSingleStringAsArray = false - s := jwt.StringArray{"test"} + s := jwt.ClaimStrings{"test"} expected := `"test"` b, err := json.Marshal(s) From 297bbc3ddfe585f94d32a159f15f906694ab6675 Mon Sep 17 00:00:00 2001 From: "Banse, Christian" Date: Tue, 3 Aug 2021 19:26:15 +0200 Subject: [PATCH 19/21] Fixed import path --- types_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types_test.go b/types_test.go index 50b4c943..675f9539 100644 --- a/types_test.go +++ b/types_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" ) func TestNumericDate(t *testing.T) { From 254c2e0deaf225de32be3aef77afcbf25e7477b4 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 21 Aug 2021 22:45:30 +0200 Subject: [PATCH 20/21] Removed TODO comment --- types.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/types.go b/types.go index 1ec6f15b..15c39a30 100644 --- a/types.go +++ b/types.go @@ -15,8 +15,6 @@ import ( // // For backwards compatibility the default precision is set to seconds, so that // no fractional timestamps are generated. -// -// TODO(oxisto): the tests seem to fail sometimes, if the precision is microseconds because the difference is literally 1 microsecond var TimePrecision = time.Second // MarshalSingleStringAsArray modifies the behaviour of the ClaimStrings type, especially From f964c98fbad6e215d4ddd6e2ce481ce1fa82758b Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 21 Aug 2021 22:48:35 +0200 Subject: [PATCH 21/21] Changed documentation of verify methods similar to #83 --- claims.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/claims.go b/claims.go index 4254a923..b9f05af6 100644 --- a/claims.go +++ b/claims.go @@ -170,8 +170,8 @@ func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { return verifyAud([]string{c.Audience}, cmp, req) } -// VerifyExpiresAt compares the exp claim against cmp. -// If required is false, this method will return true if the value matches or is unset +// VerifyExpiresAt compares the exp claim against cmp (cmp <= exp). +// If req is false, it will return true, if exp is unset. func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { if c.ExpiresAt == 0 { return verifyExp(nil, time.Unix(cmp, 0), req) @@ -181,8 +181,8 @@ func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { return verifyExp(&t, time.Unix(cmp, 0), req) } -// VerifyIssuedAt compares the iat claim against cmp. -// If required is false, this method will return true if the value matches or is unset +// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat). +// If req is false, it will return true, if iat is unset. func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { if c.IssuedAt == 0 { return verifyIat(nil, time.Unix(cmp, 0), req) @@ -192,8 +192,8 @@ func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { return verifyIat(&t, time.Unix(cmp, 0), req) } -// VerifyNotBefore compares the nbf claim against cmp. -// If required is false, this method will return true if the value matches or is unset +// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). +// If req is false, it will return true, if nbf is unset. func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { if c.NotBefore == 0 { return verifyNbf(nil, time.Unix(cmp, 0), req)