forked from mirror/jwt
210 lines
5.7 KiB
Go
210 lines
5.7 KiB
Go
package jwt
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Validator is the core of the new Validation API. It can either be used to
|
|
// modify the validation used during parsing with the [WithValidator] parser
|
|
// option or used standalone to validate an already parsed [Claim]. It can be
|
|
// further customized with a range of specified [ValidatorOption]s.
|
|
type Validator struct {
|
|
// leeway is an optional leeway that can be provided to account for clock skew.
|
|
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
|
|
|
|
// expectedAud contains the audiences this token expects. Supplying an empty
|
|
// string will disable aud checking.
|
|
expectedAud string
|
|
}
|
|
|
|
// CustomClaims represents a custom claims interface, which can be built upon the integrated
|
|
// claim types, such as map claims or registered claims.
|
|
type CustomClaims interface {
|
|
// CustomValidation can be implemented by a user-specific claim to support
|
|
// additional validation steps in addition to the regular validation.
|
|
CustomValidation() error
|
|
}
|
|
|
|
func NewValidator(opts ...ValidatorOption) *Validator {
|
|
v := &Validator{}
|
|
|
|
// Apply the validator options
|
|
for _, o := range opts {
|
|
o(v)
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
// Validate validates the given claims. It will also perform any custom validation if claims implements the CustomValidator interface.
|
|
func (v *Validator) Validate(claims Claims) error {
|
|
var now time.Time
|
|
vErr := new(ValidationError)
|
|
|
|
// Check, if we have a time func
|
|
if v.timeFunc != nil {
|
|
now = v.timeFunc()
|
|
} else {
|
|
now = time.Now()
|
|
}
|
|
|
|
if !v.VerifyExpiresAt(claims, now, false) {
|
|
exp := claims.GetExpirationTime()
|
|
delta := now.Sub(exp.Time)
|
|
vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta)
|
|
vErr.Errors |= ValidationErrorExpired
|
|
}
|
|
|
|
// Check iat if the option is enabled
|
|
if v.verifyIat && !v.VerifyIssuedAt(claims, now, false) {
|
|
vErr.Inner = ErrTokenUsedBeforeIssued
|
|
vErr.Errors |= ValidationErrorIssuedAt
|
|
}
|
|
|
|
if !v.VerifyNotBefore(claims, now, false) {
|
|
vErr.Inner = ErrTokenNotValidYet
|
|
vErr.Errors |= ValidationErrorNotValidYet
|
|
}
|
|
|
|
if v.expectedAud != "" && !v.VerifyAudience(claims, v.expectedAud, false) {
|
|
vErr.Inner = ErrTokenNotValidYet
|
|
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.(CustomClaims)
|
|
if ok {
|
|
if err := cvt.CustomValidation(); err != nil {
|
|
vErr.Inner = err
|
|
vErr.Errors |= ValidationErrorClaimsInvalid
|
|
}
|
|
}
|
|
|
|
if vErr.valid() {
|
|
return nil
|
|
}
|
|
|
|
return vErr
|
|
}
|
|
|
|
// VerifyAudience compares the aud claim against cmp.
|
|
// If required is false, this method will return true if the value matches or is unset
|
|
func (v *Validator) VerifyAudience(claims Claims, cmp string, req bool) bool {
|
|
return verifyAud(claims.GetAudience(), cmp, req)
|
|
}
|
|
|
|
// VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
|
|
// If req is false, it will return true, if exp is unset.
|
|
func (v *Validator) VerifyExpiresAt(claims Claims, cmp time.Time, req bool) bool {
|
|
exp := claims.GetExpirationTime()
|
|
if exp == nil {
|
|
return verifyExp(nil, cmp, req, v.leeway)
|
|
}
|
|
|
|
return verifyExp(&exp.Time, cmp, req, v.leeway)
|
|
}
|
|
|
|
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
|
|
// If req is false, it will return true, if iat is unset.
|
|
func (v *Validator) VerifyIssuedAt(claims Claims, cmp time.Time, req bool) bool {
|
|
iat := claims.GetIssuedAt()
|
|
if iat == nil {
|
|
return verifyIat(nil, cmp, req, v.leeway)
|
|
}
|
|
|
|
return verifyIat(&iat.Time, cmp, req, v.leeway)
|
|
}
|
|
|
|
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
|
|
// If req is false, it will return true, if nbf is unset.
|
|
func (v *Validator) VerifyNotBefore(claims Claims, cmp time.Time, req bool) bool {
|
|
nbf := claims.GetNotBefore()
|
|
if nbf == nil {
|
|
return verifyNbf(nil, cmp, req, v.leeway)
|
|
}
|
|
|
|
return verifyNbf(&nbf.Time, cmp, req, v.leeway)
|
|
}
|
|
|
|
// VerifyIssuer compares the iss claim against cmp.
|
|
// If required is false, this method will return true if the value matches or is unset
|
|
func (v *Validator) VerifyIssuer(claims Claims, cmp string, req bool) bool {
|
|
return verifyIss(claims.GetIssuer(), 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 stringClaims == "" {
|
|
return !required
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func verifyExp(exp *time.Time, now time.Time, required bool, skew time.Duration) bool {
|
|
if exp == nil {
|
|
return !required
|
|
}
|
|
|
|
return now.Before((*exp).Add(+skew))
|
|
}
|
|
|
|
func verifyIat(iat *time.Time, now time.Time, required bool, skew time.Duration) bool {
|
|
if iat == nil {
|
|
return !required
|
|
}
|
|
|
|
t := iat.Add(-skew)
|
|
return !now.Before(t)
|
|
}
|
|
|
|
func verifyNbf(nbf *time.Time, now time.Time, required bool, skew time.Duration) bool {
|
|
if nbf == nil {
|
|
return !required
|
|
}
|
|
|
|
t := nbf.Add(-skew)
|
|
return !now.Before(t)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|