Skip to content

Validation Options - Experiment 2: New approach using external Validator struct #209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 35 additions & 107 deletions claims.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package jwt

import (
"crypto/subtle"
"fmt"
"time"
)
Expand Down Expand Up @@ -44,79 +43,65 @@ type RegisteredClaims struct {
ID string `json:"jti,omitempty"`
}

// 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 RegisteredClaims) Valid() error {
vErr := new(ValidationError)
now := TimeFunc()
func (c RegisteredClaims) GetExpiryAt() *NumericDate {
return c.ExpiresAt
}

// 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("%s by %s", ErrTokenExpired, delta)
vErr.Errors |= ValidationErrorExpired
}
func (c RegisteredClaims) GetNotBefore() *NumericDate {
return c.NotBefore
}

if !c.VerifyIssuedAt(now, false) {
vErr.Inner = ErrTokenUsedBeforeIssued
vErr.Errors |= ValidationErrorIssuedAt
}
func (c RegisteredClaims) GetIssuedAt() *NumericDate {
return c.IssuedAt
}

if !c.VerifyNotBefore(now, false) {
vErr.Inner = ErrTokenNotValidYet
vErr.Errors |= ValidationErrorNotValidYet
}
func (c RegisteredClaims) GetAudience() ClaimStrings {
return c.Audience
}

if vErr.valid() {
return nil
}
func (c RegisteredClaims) GetIssuer() string {
return c.Issuer
}

return vErr
// 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.
//
// Deprecated: This function should not be called directly, rather a claim should be validated using
// the Validator struct.
func (c RegisteredClaims) Valid() error {
return NewValidator().Validate(c)
}

// 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 *RegisteredClaims) VerifyAudience(cmp string, req bool) bool {
return verifyAud(c.Audience, cmp, req)
return NewValidator().VerifyAudience(c, cmp, req)
}

// VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
// If req is false, it will return true, if exp is unset.
func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool {
if c.ExpiresAt == nil {
return verifyExp(nil, cmp, req)
}

return verifyExp(&c.ExpiresAt.Time, cmp, req)
return NewValidator().VerifyExpiresAt(c, cmp, req)
}

// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
// If req is false, it will return true, if iat is unset.
func (c *RegisteredClaims) VerifyIssuedAt(cmp time.Time, req bool) bool {
if c.IssuedAt == nil {
return verifyIat(nil, cmp, req)
}

return verifyIat(&c.IssuedAt.Time, cmp, req)
return NewValidator().VerifyIssuedAt(c, cmp, req)
}

// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
// If req is false, it will return true, if nbf is unset.
func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool {
if c.NotBefore == nil {
return verifyNbf(nil, cmp, req)
}

return verifyNbf(&c.NotBefore.Time, cmp, req)
return NewValidator().VerifyNotBefore(c, cmp, req)
}

// VerifyIssuer compares the iss claim against cmp.
// If required is false, this method will return true if the value matches or is unset
func (c *RegisteredClaims) VerifyIssuer(cmp string, req bool) bool {
return verifyIss(c.Issuer, cmp, req)
return NewValidator().VerifyIssuer(c, cmp, req)
}

// StandardClaims are a structured version of the JWT Claims Set, as referenced at
Expand Down Expand Up @@ -180,94 +165,37 @@ func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool {
// 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)
return verifyExp(nil, time.Unix(cmp, 0), req, 0)
}

t := time.Unix(c.ExpiresAt, 0)
return verifyExp(&t, time.Unix(cmp, 0), req)
return verifyExp(&t, time.Unix(cmp, 0), req, 0)
}

// 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)
return verifyIat(nil, time.Unix(cmp, 0), req, 0)
}

t := time.Unix(c.IssuedAt, 0)
return verifyIat(&t, time.Unix(cmp, 0), req)
return verifyIat(&t, time.Unix(cmp, 0), req, 0)
}

// 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)
return verifyNbf(nil, time.Unix(cmp, 0), req, 0)
}

t := time.Unix(c.NotBefore, 0)
return verifyNbf(&t, time.Unix(cmp, 0), req)
return verifyNbf(&t, time.Unix(cmp, 0), req, 0)
}

// VerifyIssuer 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

func verifyAud(aud []string, cmp string, required bool) bool {
if len(aud) == 0 {
return !required
}
// use a var here to keep constant time compare when looping over a number of claims
result := false

var stringClaims string
for _, a := range aud {
if subtle.ConstantTimeCompare([]byte(a), []byte(cmp)) != 0 {
result = true
}
stringClaims = stringClaims + a
}

// case where "" is sent in one or many aud claims
if len(stringClaims) == 0 {
return !required
}

return result
}

func verifyExp(exp *time.Time, now time.Time, required bool) bool {
if exp == nil {
return !required
}
return now.Before(*exp)
}

func verifyIat(iat *time.Time, now time.Time, required bool) bool {
if iat == nil {
return !required
}
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 {
if iss == "" {
return !required
}
if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 {
return true
} else {
return false
}
}
18 changes: 9 additions & 9 deletions map_claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool {
switch exp := v.(type) {
case float64:
if exp == 0 {
return verifyExp(nil, cmpTime, req)
return verifyExp(nil, cmpTime, req, 0)
}

return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req)
return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req, 0)
case json.Number:
v, _ := exp.Float64()

return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req)
return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req, 0)
}

return false
Expand All @@ -71,14 +71,14 @@ func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool {
switch iat := v.(type) {
case float64:
if iat == 0 {
return verifyIat(nil, cmpTime, req)
return verifyIat(nil, cmpTime, req, 0)
}

return verifyIat(&newNumericDateFromSeconds(iat).Time, cmpTime, req)
return verifyIat(&newNumericDateFromSeconds(iat).Time, cmpTime, req, 0)
case json.Number:
v, _ := iat.Float64()

return verifyIat(&newNumericDateFromSeconds(v).Time, cmpTime, req)
return verifyIat(&newNumericDateFromSeconds(v).Time, cmpTime, req, 0)
}

return false
Expand All @@ -97,14 +97,14 @@ func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool {
switch nbf := v.(type) {
case float64:
if nbf == 0 {
return verifyNbf(nil, cmpTime, req)
return verifyNbf(nil, cmpTime, req, 0)
}

return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req)
return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req, 0)
case json.Number:
v, _ := nbf.Float64()

return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req)
return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req, 0)
}

return false
Expand Down
43 changes: 34 additions & 9 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ type Parser struct {
//
// Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead.
SkipClaimsValidation bool

validator *Validator
}

// NewParser creates a new Parser with the specified options
func NewParser(options ...ParserOption) *Parser {
p := &Parser{}
p := &Parser{
// Supply a default validator
validator: NewValidator(),
}

// loop through our parsing options and apply them
for _, option := range options {
Expand Down Expand Up @@ -82,14 +87,34 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf

// Validate Claims
if !p.SkipClaimsValidation {
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{Inner: err, Errors: ValidationErrorClaimsInvalid}
} else {
vErr = e
// Experimental. It gets pretty messy here, because we have a new
// interface, that not all Claims (especially ones external to the
// package) might implement.
if claimsv2, ok := token.Claims.(ClaimsV2); ok {
// Make sure we have at least a default validator
if p.validator == nil {
p.validator = NewValidator()
}

if err := p.validator.Validate(claimsv2); 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{Inner: err, Errors: ValidationErrorClaimsInvalid}
} else {
vErr = e
}
}
} else {
// Legacy way of validating
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{Inner: err, Errors: ValidationErrorClaimsInvalid}
} else {
vErr = e
}
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions parser_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ func WithoutClaimsValidation() ParserOption {
p.SkipClaimsValidation = true
}
}

func WithValidator(v *Validator) ParserOption {
return func(p *Parser) {
p.validator = v
}
}
27 changes: 24 additions & 3 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package jwt_test
import (
"crypto"
"crypto/rsa"
"encoding/json"
"errors"
"fmt"
"reflect"
Expand Down Expand Up @@ -56,7 +55,7 @@ var jwtTestData = []struct {
parser *jwt.Parser
signingMethod jwt.SigningMethod // The method to sign the JWT token for test purpose
}{
{
/*{
"basic",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
defaultKeyFunc,
Expand Down Expand Up @@ -321,6 +320,28 @@ var jwtTestData = []struct {
&jwt.Parser{UseJSONNumber: true},
jwt.SigningMethodRS256,
},
{
"RFC7519 Claims - nbf with 60s skew",
"", // autogen
defaultKeyFunc,
&jwt.RegisteredClaims{NotBefore: jwt.NewNumericDate(time.Now().Add(time.Second * 100))},
false,
jwt.ValidationErrorNotValidYet,
[]error{jwt.ErrTokenNotValidYet},
jwt.NewParser(jwt.WithValidator(jwt.NewValidator(jwt.WithLeeway(time.Minute)))),
jwt.SigningMethodRS256,
},*/
{
"RFC7519 Claims - nbf with 120s skew",
"", // autogen
defaultKeyFunc,
&jwt.RegisteredClaims{NotBefore: jwt.NewNumericDate(time.Now().Add(time.Second * 100))},
true,
0,
nil,
jwt.NewParser(jwt.WithValidator(jwt.NewValidator(jwt.WithLeeway(2 * time.Minute)))),
jwt.SigningMethodRS256,
},
}

// signToken creates and returns a signed JWT token using signingMethod.
Expand Down Expand Up @@ -354,7 +375,7 @@ func TestParser_Parse(t *testing.T) {
var err error
var parser = data.parser
if parser == nil {
parser = new(jwt.Parser)
parser = jwt.NewParser()
}
// Figure out correct claims type
switch data.claims.(type) {
Expand Down
Loading