Skip to content
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

Make issued at (iat) claim validation optional #175

Closed
wants to merge 14 commits into from
89 changes: 63 additions & 26 deletions claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (c RegisteredClaims) Valid(opts ...validationOption) error {
vErr.Errors |= ValidationErrorExpired
}

if !c.VerifyIssuedAt(now, false) {
if !c.VerifyIssuedAt(now, false, opts...) {
vErr.Inner = ErrTokenUsedBeforeIssued
vErr.Errors |= ValidationErrorIssuedAt
}
Expand All @@ -73,6 +73,11 @@ func (c RegisteredClaims) Valid(opts ...validationOption) error {
vErr.Errors |= ValidationErrorNotValidYet
}

if !c.validateAudience(false, opts...) {
vErr.Inner = ErrTokenInvalidAudience
vErr.Errors |= ValidationErrorAudience
}

if vErr.valid() {
return nil
}
Expand All @@ -89,10 +94,7 @@ func (c *RegisteredClaims) VerifyAudience(cmp string, req bool) bool {
// 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, opts ...validationOption) bool {
validator := validator{}
for _, o := range opts {
o(&validator)
}
validator := getValidator(opts...)
if c.ExpiresAt == nil {
return verifyExp(nil, cmp, req, validator.leeway)
}
Expand All @@ -102,21 +104,23 @@ func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool, opts ...vali

// 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 {
func (c *RegisteredClaims) VerifyIssuedAt(cmp time.Time, req bool, opts ...validationOption) bool {
validator := getValidator(opts...)
if validator.skipIssuedAt {
return true
}

if c.IssuedAt == nil {
return verifyIat(nil, cmp, req)
return verifyIat(nil, cmp, req, validator.leeway)
}

return verifyIat(&c.IssuedAt.Time, cmp, req)
return verifyIat(&c.IssuedAt.Time, cmp, req, validator.leeway)
}

// 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, opts ...validationOption) bool {
validator := validator{}
for _, o := range opts {
o(&validator)
}
validator := getValidator(opts...)
if c.NotBefore == nil {
return verifyNbf(nil, cmp, req, validator.leeway)
}
Expand All @@ -130,6 +134,20 @@ func (c *RegisteredClaims) VerifyIssuer(cmp string, req bool) bool {
return verifyIss(c.Issuer, cmp, req)
}

func (c *RegisteredClaims) validateAudience(req bool, opts ...validationOption) bool {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it somehow possible to move this function to our validation struct? Otherwise we are duplicating a lot of code. This would also make sense to move the other validate/verify functions there in the long run.

Copy link
Contributor Author

@ksegun ksegun Mar 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, it cannot be moved as each VerifyAudience call is a receiver method on the respective struct. I refactored most of the code into a validator function to reduce duplication.

v := getValidator(opts...)
aud, skip := v.getAudienceValidationOpts(len(c.Audience) != 0)

// Based on my reading of https://datatracker.ietf.org/doc/html/rfc7519/#section-4.1.3
// this should technically fail. This is left as a decision for the maintainers to alter
// the behavior as it would be a breaking change.
if !skip && aud != nil {
return c.VerifyAudience(*aud, req)
}

return !req
}

// 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
Expand Down Expand Up @@ -164,7 +182,7 @@ func (c StandardClaims) Valid(opts ...validationOption) error {
vErr.Errors |= ValidationErrorExpired
}

if !c.VerifyIssuedAt(now, false) {
if !c.VerifyIssuedAt(now, false, opts...) {
vErr.Inner = ErrTokenUsedBeforeIssued
vErr.Errors |= ValidationErrorIssuedAt
}
Expand All @@ -174,6 +192,11 @@ func (c StandardClaims) Valid(opts ...validationOption) error {
vErr.Errors |= ValidationErrorNotValidYet
}

if !c.validateAudience(false, opts...) {
vErr.Inner = ErrTokenInvalidAudience
vErr.Errors |= ValidationErrorAudience
}

if vErr.valid() {
return nil
}
Expand All @@ -190,10 +213,7 @@ func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool {
// 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, opts ...validationOption) bool {
validator := validator{}
for _, o := range opts {
o(&validator)
}
validator := getValidator(opts...)
if c.ExpiresAt == 0 {
return verifyExp(nil, time.Unix(cmp, 0), req, validator.leeway)
}
Expand All @@ -204,22 +224,24 @@ func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool, opts ...validation

// 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 {
func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool, opts ...validationOption) bool {
validator := getValidator(opts...)
if validator.skipIssuedAt {
return true
}

if c.IssuedAt == 0 {
return verifyIat(nil, time.Unix(cmp, 0), req)
return verifyIat(nil, time.Unix(cmp, 0), req, validator.leeway)
}

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

// 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, opts ...validationOption) bool {
validator := validator{}
for _, o := range opts {
o(&validator)
}
validator := getValidator(opts...)
if c.NotBefore == 0 {
return verifyNbf(nil, time.Unix(cmp, 0), req, validator.leeway)
}
Expand All @@ -234,6 +256,20 @@ func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool {
return verifyIss(c.Issuer, cmp, req)
}

func (c *StandardClaims) validateAudience(req bool, opts ...validationOption) bool {
v := getValidator(opts...)
aud, skip := v.getAudienceValidationOpts(c.Audience != "")

// Based on my reading of https://datatracker.ietf.org/doc/html/rfc7519/#section-4.1.3
// this should technically fail. This is left as a decision for the maintainers to alter
// the behavior as it would be a breaking change.
if !skip && aud != nil {
return c.VerifyAudience(*aud, req)
}

return !req
}

// ----- helpers

func verifyAud(aud []string, cmp string, required bool) bool {
Expand Down Expand Up @@ -266,11 +302,12 @@ func verifyExp(exp *time.Time, now time.Time, required bool, skew time.Duration)
return now.Before((*exp).Add(+skew))
}

func verifyIat(iat *time.Time, now time.Time, required bool) bool {
func verifyIat(iat *time.Time, now time.Time, required bool, skew time.Duration) bool {
if iat == nil {
return !required
}
return now.After(*iat) || now.Equal(*iat)
t := (*iat).Add(-skew)
return now.After(t) || now.Equal(*iat)
}

func verifyNbf(nbf *time.Time, now time.Time, required bool, skew time.Duration) bool {
Expand Down
62 changes: 42 additions & 20 deletions map_claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package jwt

import (
"encoding/json"
"errors"
"time"
// "fmt"
)
Expand Down Expand Up @@ -42,10 +41,7 @@ func (m MapClaims) VerifyExpiresAt(cmp int64, req bool, opts ...validationOption
return !req
}

validator := validator{}
for _, o := range opts {
o(&validator)
}
validator := getValidator(opts...)

switch exp := v.(type) {
case float64:
Expand All @@ -65,25 +61,37 @@ func (m MapClaims) VerifyExpiresAt(cmp int64, req bool, opts ...validationOption

// VerifyIssuedAt compares the exp claim against cmp (cmp >= iat).
// If req is false, it will return true, if iat is unset.
func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool {
func (m MapClaims) VerifyIssuedAt(cmp int64, req bool, opts ...validationOption) bool {
cmpTime := time.Unix(cmp, 0)

v, ok := m["iat"]
if !ok {
return !req
}

validator := getValidator(opts...)

// validate the type
switch v.(type) {
case float64, json.Number:
if validator.skipIssuedAt {
return true
}
default:
return false
}

switch iat := v.(type) {
case float64:
if iat == 0 {
return verifyIat(nil, cmpTime, req)
return verifyIat(nil, cmpTime, req, validator.leeway)
}

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

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

return false
Expand All @@ -99,10 +107,7 @@ func (m MapClaims) VerifyNotBefore(cmp int64, req bool, opts ...validationOption
return !req
}

validator := validator{}
for _, o := range opts {
o(&validator)
}
validator := getValidator(opts...)

switch nbf := v.(type) {
case float64:
Expand All @@ -127,6 +132,21 @@ func (m MapClaims) VerifyIssuer(cmp string, req bool) bool {
return verifyIss(iss, cmp, req)
}

func (m MapClaims) validateAudience(req bool, opts ...validationOption) bool {
_, ok := m["aud"]
v := getValidator(opts...)
aud, skip := v.getAudienceValidationOpts(ok)

// Based on my reading of https://datatracker.ietf.org/doc/html/rfc7519/#section-4.1.3
// this should technically fail. This is left as a decision for the maintainers to alter
// the behavior as it would be a breaking change.
if !skip && aud != nil {
return m.VerifyAudience(*aud, req)
}

return !req
}

// 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
Expand All @@ -136,23 +156,25 @@ func (m MapClaims) Valid(opts ...validationOption) error {
now := TimeFunc().Unix()

if !m.VerifyExpiresAt(now, false, opts...) {
// TODO(oxisto): this should be replaced with ErrTokenExpired
vErr.Inner = errors.New("Token is expired")
vErr.Inner = ErrTokenExpired
vErr.Errors |= ValidationErrorExpired
}

if !m.VerifyIssuedAt(now, false) {
// TODO(oxisto): this should be replaced with ErrTokenUsedBeforeIssued
vErr.Inner = errors.New("Token used before issued")
if !m.VerifyIssuedAt(now, false, opts...) {
vErr.Inner = ErrTokenUsedBeforeIssued
vErr.Errors |= ValidationErrorIssuedAt
}

if !m.VerifyNotBefore(now, false, opts...) {
// TODO(oxisto): this should be replaced with ErrTokenNotValidYet
vErr.Inner = errors.New("Token is not valid yet")
vErr.Inner = ErrTokenNotValidYet
vErr.Errors |= ValidationErrorNotValidYet
}

if !m.validateAudience(false, opts...) {
vErr.Inner = ErrTokenInvalidAudience
vErr.Errors |= ValidationErrorAudience
}

if vErr.valid() {
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions map_claims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,13 @@ func TestMapClaimsVerifyExpiresAtExpire(t *testing.T) {
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
}

got = mapClaims.VerifyExpiresAt(exp + 1, true)
got = mapClaims.VerifyExpiresAt(exp+1, true)
if want != got {
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
}

want = true
got = mapClaims.VerifyExpiresAt(exp - 1, true)
got = mapClaims.VerifyExpiresAt(exp-1, true)
if want != got {
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
}
Expand Down
23 changes: 23 additions & 0 deletions parser_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,26 @@ func WithLeeway(d time.Duration) ParserOption {
p.validationOptions = append(p.validationOptions, withLeeway(d))
}
}

// WithoutIssuedAtValidation is an option to disable the validation of the issued at (iat) claim.
// The current `iat` time based validation is planned to be deprecated in v5

func WithoutIssuedAtValidation() ParserOption {
return func(p *Parser) {
p.validationOptions = append(p.validationOptions, withoutIssuedAtValidation())
}
}

// WithAudience returns the ParserOption for specifying an expected aud member value
func WithAudience(aud string) ParserOption {
return func(p *Parser) {
p.validationOptions = append(p.validationOptions, withAudience(aud))
}
}

// WithoutAudienceValidation returns the ParserOption that specifies audience check should be skipped
func WithoutAudienceValidation() ParserOption {
return func(p *Parser) {
p.validationOptions = append(p.validationOptions, withoutAudienceValidation())
}
}
Loading