Added timeFunc, made iat optional

This commit is contained in:
Christian Banse 2022-08-27 13:36:45 +02:00
parent 0e79f91215
commit 4990d2cdf3
5 changed files with 108 additions and 27 deletions

View File

@ -95,7 +95,7 @@ func ExampleParseWithClaims_customClaimsType() {
// Example creating a token using a custom claims type and validation options. The RegisteredClaims is embedded // Example creating a token using a custom claims type and validation options. The RegisteredClaims is embedded
// in the custom type to allow for easy encoding, parsing and validation of standard claims. // in the custom type to allow for easy encoding, parsing and validation of standard claims.
func ExampleParseWithClaims_customValidator() { func ExampleParseWithClaims_validationOptions() {
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA" tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
type MyCustomClaims struct { type MyCustomClaims struct {
@ -117,7 +117,41 @@ func ExampleParseWithClaims_customValidator() {
// Output: bar test // Output: bar test
} }
// An example of parsing the error types using bitfield checks type MyCustomClaims struct {
Foo string `json:"foo"`
jwt.RegisteredClaims
}
func (m MyCustomClaims) CustomValidation() error {
if m.Foo != "bar" {
return errors.New("must be foobar")
}
return nil
}
// Example creating a token using a custom claims type and validation options.
// The RegisteredClaims is embedded in the custom type to allow for easy
// encoding, parsing and validation of standard claims and the function
// CustomValidation is implemented.
func ExampleParseWithClaims_customValidation() {
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
validator := jwt.NewValidator(jwt.WithLeeway(5 * time.Second))
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte("AllYourBase"), nil
}, jwt.WithValidator(validator))
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer)
} else {
fmt.Println(err)
}
// Output: bar test
}
// An example of parsing the error types using errors.Is.
func ExampleParse_errorChecking() { func ExampleParse_errorChecking() {
// Token from another example. This token is expired // Token from another example. This token is expired
var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c" var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"

View File

@ -7,6 +7,9 @@ import (
"strings" "strings"
) )
// DefaultValidator is the default validator that is used, if no custom validator is supplied in a Parser.
var DefaultValidator = NewValidator()
type Parser struct { type Parser struct {
// If populated, only these methods will be considered valid. // If populated, only these methods will be considered valid.
// //
@ -28,12 +31,9 @@ type Parser struct {
// NewParser creates a new Parser with the specified options // NewParser creates a new Parser with the specified options
func NewParser(options ...ParserOption) *Parser { func NewParser(options ...ParserOption) *Parser {
p := &Parser{ p := &Parser{}
// Supply a default validator
validator: NewValidator(),
}
// loop through our parsing options and apply them // Loop through our parsing options and apply them
for _, option := range options { for _, option := range options {
option(p) option(p)
} }
@ -89,7 +89,7 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
if !p.SkipClaimsValidation { if !p.SkipClaimsValidation {
// Make sure we have at least a default validator // Make sure we have at least a default validator
if p.validator == nil { if p.validator == nil {
p.validator = NewValidator() p.validator = DefaultValidator
} }
if err := p.validator.Validate(claims); err != nil { if err := p.validator.Validate(claims); err != nil {

View File

@ -4,7 +4,6 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"strings" "strings"
"time"
) )
// DecodePaddingAllowed will switch the codec used for decoding JWTs respectively. Note that the JWS RFC7515 // DecodePaddingAllowed will switch the codec used for decoding JWTs respectively. Note that the JWS RFC7515
@ -14,11 +13,6 @@ import (
// To use the non-recommended decoding, set this boolean to `true` prior to using this package. // To use the non-recommended decoding, set this boolean to `true` prior to using this package.
var DecodePaddingAllowed bool var DecodePaddingAllowed bool
// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time).
// You can override it to use another time value. This is useful for testing or if your
// server uses a different time zone than your tokens.
var TimeFunc = time.Now
// Keyfunc will be used by the Parse methods as a callback function to supply // Keyfunc will be used by the Parse methods as a callback function to supply
// the key for verification. The function receives the parsed, // the key for verification. The function receives the parsed,
// but unverified Token. This allows you to use properties in the // but unverified Token. This allows you to use properties in the

View File

@ -6,13 +6,48 @@ import (
"time" "time"
) )
// Validator is the core of the new Validation API. It is
type Validator struct { type Validator struct {
// leeway is an optional leeway that can be provided to account for clock skew.
leeway time.Duration leeway time.Duration
// timeFunc is used to supply the current time that is needed for
// validation. If unspecified, this defaults to time.Now.
timeFunc func() time.Time
// 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
// necessary. However, if wanted, it can be checked if the iat is
// unrealistic, i.e., in the future.
verifyIat bool
}
type customValidationType interface {
CustomValidation() error
}
func NewValidator(opts ...ValidatorOption) *Validator {
v := &Validator{}
// Apply the validator options
for _, o := range opts {
o(v)
}
return v
} }
func (v *Validator) Validate(claims Claims) error { func (v *Validator) Validate(claims Claims) error {
var now time.Time
vErr := new(ValidationError) vErr := new(ValidationError)
now := TimeFunc()
// Check, if we have a time func
if v.timeFunc != nil {
now = v.timeFunc()
} else {
now = time.Now()
}
if !v.VerifyExpiresAt(claims, now, false) { if !v.VerifyExpiresAt(claims, now, false) {
exp := claims.GetExpirationTime() exp := claims.GetExpirationTime()
@ -21,7 +56,8 @@ func (v *Validator) Validate(claims Claims) error {
vErr.Errors |= ValidationErrorExpired vErr.Errors |= ValidationErrorExpired
} }
if !v.VerifyIssuedAt(claims, now, false) { // Check iat if the option is enabled
if v.verifyIat && !v.VerifyIssuedAt(claims, now, false) {
vErr.Inner = ErrTokenUsedBeforeIssued vErr.Inner = ErrTokenUsedBeforeIssued
vErr.Errors |= ValidationErrorIssuedAt vErr.Errors |= ValidationErrorIssuedAt
} }
@ -31,6 +67,16 @@ func (v *Validator) Validate(claims Claims) error {
vErr.Errors |= ValidationErrorNotValidYet vErr.Errors |= ValidationErrorNotValidYet
} }
// Finally, we want to give the claim itself some possibility to do some
// additional custom validation based on their custom claims
cvt, ok := claims.(customValidationType)
if ok {
if err := cvt.CustomValidation(); err != nil {
vErr.Inner = err
vErr.Errors |= ValidationErrorClaimsInvalid
}
}
if vErr.valid() { if vErr.valid() {
return nil return nil
} }
@ -83,16 +129,6 @@ func (v *Validator) VerifyIssuer(claims Claims, cmp string, req bool) bool {
return verifyIss(claims.GetIssuer(), cmp, req) return verifyIss(claims.GetIssuer(), cmp, req)
} }
func NewValidator(opts ...ValidatorOption) *Validator {
v := &Validator{}
for _, o := range opts {
o(v)
}
return v
}
// ----- helpers // ----- helpers
func verifyAud(aud []string, cmp string, required bool) bool { func verifyAud(aud []string, cmp string, required bool) bool {

View File

@ -9,9 +9,26 @@ import "time"
// accordingly. // accordingly.
type ValidatorOption func(*Validator) type ValidatorOption func(*Validator)
// WithLeeway returns the ParserOption for specifying the leeway window. // WithLeeway returns the ValidatorOption for specifying the leeway window.
func WithLeeway(leeway time.Duration) ValidatorOption { func WithLeeway(leeway time.Duration) ValidatorOption {
return func(v *Validator) { return func(v *Validator) {
v.leeway = leeway v.leeway = leeway
} }
} }
// WithTimeFunc returns the ValidatorOption for specifying the time func. The
// primary use-case for this is testing. If you are looking for a way to account
// for clock-skew, WithLeeway should be used instead.
func WithTimeFunc(f func() time.Time) ValidatorOption {
return func(v *Validator) {
v.timeFunc = f
}
}
// WithIssuedAtVerification returns the ValidatorOption to enable verification
// of issued-at.
func WithIssuedAtVerification() ValidatorOption {
return func(v *Validator) {
v.verifyIat = true
}
}