diff --git a/parser_option.go b/parser_option.go index 43157355..d845865d 100644 --- a/parser_option.go +++ b/parser_option.go @@ -66,6 +66,14 @@ func WithExpirationRequired() ParserOption { } } +// WithNotBeforeRequired returns the ParserOption to make nbf claim required. +// By default nbf claim is optional. +func WithNotBeforeRequired() ParserOption { + return func(p *Parser) { + p.validator.requireNbf = true + } +} + // WithAudience configures the validator to require any of the specified // audiences in the `aud` claim. Validation will fail if the audience is not // listed in the token or the `aud` claim is missing. diff --git a/validator.go b/validator.go index 92b5c057..c82dfcae 100644 --- a/validator.go +++ b/validator.go @@ -44,6 +44,9 @@ type Validator struct { // requireExp specifies whether the exp claim is required requireExp bool + // requireNbf specifies whether the nbf claim is required + requireNbf bool + // verifyIat specifies whether the iat (Issued At) claim will be verified. // According to https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6 this // only specifies the age of the token, but no validation check is @@ -111,8 +114,9 @@ func (v *Validator) Validate(claims Claims) error { } // We always need to check not-before, but usage of the claim itself is - // OPTIONAL. - if err = v.verifyNotBefore(claims, now, false); err != nil { + // OPTIONAL by default. requireNbf overrides this behavior and makes + // the nbf claim mandatory. + if err = v.verifyNotBefore(claims, now, v.requireNbf); err != nil { errs = append(errs, err) } diff --git a/validator_test.go b/validator_test.go index 9fdaafab..1790be1f 100644 --- a/validator_test.go +++ b/validator_test.go @@ -262,6 +262,73 @@ func Test_Validator_verifyIssuedAt(t *testing.T) { } } +func Test_Validator_requireNotBefore(t *testing.T) { + type fields struct { + leeway time.Duration + timeFunc func() time.Time + requireNbf bool + } + type args struct { + claims Claims + cmp time.Time + required bool + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "good claim without nbf", + fields: fields{requireNbf: false}, + args: args{claims: MapClaims{}, required: false}, + wantErr: nil, + }, + { + name: "good claim with nbf", + fields: fields{requireNbf: true}, + args: args{ + claims: RegisteredClaims{NotBefore: NewNumericDate(time.Now().Add(time.Minute * -10))}, + cmp: time.Now().Add(10 * time.Minute), + required: true, + }, + wantErr: nil, + }, + { + name: "token nbf time is in future", + fields: fields{requireNbf: true, timeFunc: time.Now}, + args: args{ + claims: RegisteredClaims{NotBefore: NewNumericDate(time.Now().Add(time.Minute * +10))}, + cmp: time.Now().Add(10 * time.Minute), + required: true, + }, + wantErr: ErrTokenNotValidYet, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := []ParserOption{ + WithLeeway(tt.fields.leeway), + } + + if tt.fields.requireNbf { + opts = append(opts, WithNotBeforeRequired()) + } + + if tt.fields.timeFunc != nil { + opts = append(opts, WithTimeFunc(tt.fields.timeFunc)) + } + + v := NewValidator(opts...) + + if err := v.verifyNotBefore(tt.args.claims, tt.args.cmp, tt.args.required); (err != nil) && !errors.Is(err, tt.wantErr) { + t.Errorf("validator.requireNotBefore() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + func Test_Validator_verifyAudience(t *testing.T) { type fields struct { expectedAud []string