1 package jwt 2 3 import ( 4 "crypto/subtle" 5 "fmt" 6 "time" 7 ) 8 9 // ClaimsValidator is an interface that can be implemented by custom claims who 10 // wish to execute any additional claims validation based on 11 // application-specific logic. The Validate function is then executed in 12 // addition to the regular claims validation and any error returned is appended 13 // to the final validation result. 14 // 15 // type MyCustomClaims struct { 16 // Foo string `json:"foo"` 17 // jwt.RegisteredClaims 18 // } 19 // 20 // func (m MyCustomClaims) Validate() error { 21 // if m.Foo != "bar" { 22 // return errors.New("must be foobar") 23 // } 24 // return nil 25 // } 26 type ClaimsValidator interface { 27 Claims 28 Validate() error 29 } 30 31 // Validator is the core of the new Validation API. It is automatically used by 32 // a [Parser] during parsing and can be modified with various parser options. 33 // 34 // The [NewValidator] function should be used to create an instance of this 35 // struct. 36 type Validator struct { 37 // leeway is an optional leeway that can be provided to account for clock skew. 38 leeway time.Duration 39 40 // timeFunc is used to supply the current time that is needed for 41 // validation. If unspecified, this defaults to time.Now. 42 timeFunc func() time.Time 43 44 // requireExp specifies whether the exp claim is required 45 requireExp bool 46 47 // verifyIat specifies whether the iat (Issued At) claim will be verified. 48 // According to https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6 this 49 // only specifies the age of the token, but no validation check is 50 // necessary. However, if wanted, it can be checked if the iat is 51 // unrealistic, i.e., in the future. 52 verifyIat bool 53 54 // expectedAud contains the audience this token expects. Supplying an empty 55 // string will disable aud checking. 56 expectedAud string 57 58 // expectedIss contains the issuer this token expects. Supplying an empty 59 // string will disable iss checking. 60 expectedIss string 61 62 // expectedSub contains the subject this token expects. Supplying an empty 63 // string will disable sub checking. 64 expectedSub string 65 } 66 67 // NewValidator can be used to create a stand-alone validator with the supplied 68 // options. This validator can then be used to validate already parsed claims. 69 // 70 // Note: Under normal circumstances, explicitly creating a validator is not 71 // needed and can potentially be dangerous; instead functions of the [Parser] 72 // class should be used. 73 // 74 // The [Validator] is only checking the *validity* of the claims, such as its 75 // expiration time, but it does NOT perform *signature verification* of the 76 // token. 77 func NewValidator(opts ...ParserOption) *Validator { 78 p := NewParser(opts...) 79 return p.validator 80 } 81 82 // Validate validates the given claims. It will also perform any custom 83 // validation if claims implements the [ClaimsValidator] interface. 84 // 85 // Note: It will NOT perform any *signature verification* on the token that 86 // contains the claims and expects that the [Claim] was already successfully 87 // verified. 88 func (v *Validator) Validate(claims Claims) error { 89 var ( 90 now time.Time 91 errs []error = make([]error, 0, 6) 92 err error 93 ) 94 95 // Check, if we have a time func 96 if v.timeFunc != nil { 97 now = v.timeFunc() 98 } else { 99 now = time.Now() 100 } 101 102 // We always need to check the expiration time, but usage of the claim 103 // itself is OPTIONAL by default. requireExp overrides this behavior 104 // and makes the exp claim mandatory. 105 if err = v.verifyExpiresAt(claims, now, v.requireExp); err != nil { 106 errs = append(errs, err) 107 } 108 109 // We always need to check not-before, but usage of the claim itself is 110 // OPTIONAL. 111 if err = v.verifyNotBefore(claims, now, false); err != nil { 112 errs = append(errs, err) 113 } 114 115 // Check issued-at if the option is enabled 116 if v.verifyIat { 117 if err = v.verifyIssuedAt(claims, now, false); err != nil { 118 errs = append(errs, err) 119 } 120 } 121 122 // If we have an expected audience, we also require the audience claim 123 if v.expectedAud != "" { 124 if err = v.verifyAudience(claims, v.expectedAud, true); err != nil { 125 errs = append(errs, err) 126 } 127 } 128 129 // If we have an expected issuer, we also require the issuer claim 130 if v.expectedIss != "" { 131 if err = v.verifyIssuer(claims, v.expectedIss, true); err != nil { 132 errs = append(errs, err) 133 } 134 } 135 136 // If we have an expected subject, we also require the subject claim 137 if v.expectedSub != "" { 138 if err = v.verifySubject(claims, v.expectedSub, true); err != nil { 139 errs = append(errs, err) 140 } 141 } 142 143 // Finally, we want to give the claim itself some possibility to do some 144 // additional custom validation based on a custom Validate function. 145 cvt, ok := claims.(ClaimsValidator) 146 if ok { 147 if err := cvt.Validate(); err != nil { 148 errs = append(errs, err) 149 } 150 } 151 152 if len(errs) == 0 { 153 return nil 154 } 155 156 return joinErrors(errs...) 157 } 158 159 // verifyExpiresAt compares the exp claim in claims against cmp. This function 160 // will succeed if cmp < exp. Additional leeway is taken into account. 161 // 162 // If exp is not set, it will succeed if the claim is not required, 163 // otherwise ErrTokenRequiredClaimMissing will be returned. 164 // 165 // Additionally, if any error occurs while retrieving the claim, e.g., when its 166 // the wrong type, an ErrTokenUnverifiable error will be returned. 167 func (v *Validator) verifyExpiresAt(claims Claims, cmp time.Time, required bool) error { 168 exp, err := claims.GetExpirationTime() 169 if err != nil { 170 return err 171 } 172 173 if exp == nil { 174 return errorIfRequired(required, "exp") 175 } 176 177 return errorIfFalse(cmp.Before((exp.Time).Add(+v.leeway)), ErrTokenExpired) 178 } 179 180 // verifyIssuedAt compares the iat claim in claims against cmp. This function 181 // will succeed if cmp >= iat. Additional leeway is taken into account. 182 // 183 // If iat is not set, it will succeed if the claim is not required, 184 // otherwise ErrTokenRequiredClaimMissing will be returned. 185 // 186 // Additionally, if any error occurs while retrieving the claim, e.g., when its 187 // the wrong type, an ErrTokenUnverifiable error will be returned. 188 func (v *Validator) verifyIssuedAt(claims Claims, cmp time.Time, required bool) error { 189 iat, err := claims.GetIssuedAt() 190 if err != nil { 191 return err 192 } 193 194 if iat == nil { 195 return errorIfRequired(required, "iat") 196 } 197 198 return errorIfFalse(!cmp.Before(iat.Add(-v.leeway)), ErrTokenUsedBeforeIssued) 199 } 200 201 // verifyNotBefore compares the nbf claim in claims against cmp. This function 202 // will return true if cmp >= nbf. Additional leeway is taken into account. 203 // 204 // If nbf is not set, it will succeed if the claim is not required, 205 // otherwise ErrTokenRequiredClaimMissing will be returned. 206 // 207 // Additionally, if any error occurs while retrieving the claim, e.g., when its 208 // the wrong type, an ErrTokenUnverifiable error will be returned. 209 func (v *Validator) verifyNotBefore(claims Claims, cmp time.Time, required bool) error { 210 nbf, err := claims.GetNotBefore() 211 if err != nil { 212 return err 213 } 214 215 if nbf == nil { 216 return errorIfRequired(required, "nbf") 217 } 218 219 return errorIfFalse(!cmp.Before(nbf.Add(-v.leeway)), ErrTokenNotValidYet) 220 } 221 222 // verifyAudience compares the aud claim against cmp. 223 // 224 // If aud is not set or an empty list, it will succeed if the claim is not required, 225 // otherwise ErrTokenRequiredClaimMissing will be returned. 226 // 227 // Additionally, if any error occurs while retrieving the claim, e.g., when its 228 // the wrong type, an ErrTokenUnverifiable error will be returned. 229 func (v *Validator) verifyAudience(claims Claims, cmp string, required bool) error { 230 aud, err := claims.GetAudience() 231 if err != nil { 232 return err 233 } 234 235 if len(aud) == 0 { 236 return errorIfRequired(required, "aud") 237 } 238 239 // use a var here to keep constant time compare when looping over a number of claims 240 result := false 241 242 var stringClaims string 243 for _, a := range aud { 244 if subtle.ConstantTimeCompare([]byte(a), []byte(cmp)) != 0 { 245 result = true 246 } 247 stringClaims = stringClaims + a 248 } 249 250 // case where "" is sent in one or many aud claims 251 if stringClaims == "" { 252 return errorIfRequired(required, "aud") 253 } 254 255 return errorIfFalse(result, ErrTokenInvalidAudience) 256 } 257 258 // verifyIssuer compares the iss claim in claims against cmp. 259 // 260 // If iss is not set, it will succeed if the claim is not required, 261 // otherwise ErrTokenRequiredClaimMissing will be returned. 262 // 263 // Additionally, if any error occurs while retrieving the claim, e.g., when its 264 // the wrong type, an ErrTokenUnverifiable error will be returned. 265 func (v *Validator) verifyIssuer(claims Claims, cmp string, required bool) error { 266 iss, err := claims.GetIssuer() 267 if err != nil { 268 return err 269 } 270 271 if iss == "" { 272 return errorIfRequired(required, "iss") 273 } 274 275 return errorIfFalse(iss == cmp, ErrTokenInvalidIssuer) 276 } 277 278 // verifySubject compares the sub claim against cmp. 279 // 280 // If sub is not set, it will succeed if the claim is not required, 281 // otherwise ErrTokenRequiredClaimMissing will be returned. 282 // 283 // Additionally, if any error occurs while retrieving the claim, e.g., when its 284 // the wrong type, an ErrTokenUnverifiable error will be returned. 285 func (v *Validator) verifySubject(claims Claims, cmp string, required bool) error { 286 sub, err := claims.GetSubject() 287 if err != nil { 288 return err 289 } 290 291 if sub == "" { 292 return errorIfRequired(required, "sub") 293 } 294 295 return errorIfFalse(sub == cmp, ErrTokenInvalidSubject) 296 } 297 298 // errorIfFalse returns the error specified in err, if the value is true. 299 // Otherwise, nil is returned. 300 func errorIfFalse(value bool, err error) error { 301 if value { 302 return nil 303 } else { 304 return err 305 } 306 } 307 308 // errorIfRequired returns an ErrTokenRequiredClaimMissing error if required is 309 // true. Otherwise, nil is returned. 310 func errorIfRequired(required bool, claim string) error { 311 if required { 312 return newError(fmt.Sprintf("%s claim is required", claim), ErrTokenRequiredClaimMissing) 313 } else { 314 return nil 315 } 316 } 317