diff --git a/jwt.go b/jwt.go index 60546c5..53972c3 100644 --- a/jwt.go +++ b/jwt.go @@ -1,6 +1,7 @@ package jwt import ( + "crypto/subtle" "encoding/base64" "encoding/json" "net/http" @@ -25,7 +26,8 @@ type Claims interface { Valid() error } -// Structured version of Claims Section, as referenced at https://tools.ietf.org/html/rfc7519#section-4.1 +// Structured version of Claims Section, as referenced at +// https://tools.ietf.org/html/rfc7519#section-4.1 type StandardClaims struct { Audience string `json:"aud,omitempty"` ExpiresAt int64 `json:"exp,omitempty"` @@ -36,138 +38,27 @@ type StandardClaims struct { Subject string `json:"sub,omitempty"` } +// 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 +// be considered a valid claim. func (c StandardClaims) Valid() error { vErr := new(ValidationError) now := TimeFunc().Unix() - // The claims below are optional, so if they are set to the default value in Go, let's not - // verify them. - - if c.ExpiresAt != 0 { - if c.VerifyExpiresAt(now) == false { - vErr.err = "Token is expired" - vErr.Errors |= ValidationErrorExpired - } - } - - if c.IssuedAt != 0 { - if c.VerifyIssuedAt(now) == false { - vErr.err = "Token used before issued, clock skew issue?" - vErr.Errors |= ValidationErrorIssuedAt - } - } - - if c.NotBefore != 0 { - if c.VerifyNotBefore(now) == false { - vErr.err = "Token is not valid yet" - vErr.Errors |= ValidationErrorNotValidYet - } - } - - if vErr.valid() { - return nil - } - - return vErr -} - -func (c *StandardClaims) VerifyAudience(cmp string) bool { - return verifyAud(c.Audience, cmp) -} - -func (c *StandardClaims) VerifyExpiresAt(cmp int64) bool { - return verifyExp(c.ExpiresAt, cmp) -} - -func (c *StandardClaims) VerifyIssuedAt(cmp int64) bool { - return verifyIat(c.IssuedAt, cmp) -} - -func (c *StandardClaims) VerifyIssuer(cmp string) bool { - return verifyIss(c.Issuer, cmp) -} - -func (c *StandardClaims) VerifyNotBefore(cmp int64) bool { - return verifyNbf(c.NotBefore, cmp) -} - -type MapClaim map[string]interface{} - -func (m MapClaim) VerifyAudience(cmp string) bool { - val, exists := m["aud"] - if !exists { - return true // Don't fail validation if claim doesn't exist - } - - if aud, ok := val.(string); ok { - return verifyAud(aud, cmp) - } - return false -} - -func (m MapClaim) VerifyExpiresAt(cmp int64) bool { - val, exists := m["exp"] - if !exists { - return true - } - - if exp, ok := val.(float64); ok { - return verifyExp(int64(exp), cmp) - } - return false -} - -func (m MapClaim) VerifyIssuedAt(cmp int64) bool { - val, exists := m["iat"] - if !exists { - return true - } - - if iat, ok := val.(float64); ok { - return verifyIat(int64(iat), cmp) - } - return false -} - -func (m MapClaim) VerifyIssuer(cmp string) bool { - val, exists := m["iss"] - if !exists { - return true - } - - if iss, ok := val.(string); ok { - return verifyIss(iss, cmp) - } - return false -} - -func (m MapClaim) VerifyNotBefore(cmp int64) bool { - val, exists := m["nbf"] - if !exists { - return true - } - - if nbf, ok := val.(float64); ok { - return verifyNbf(int64(nbf), cmp) - } - return false -} - -func (m MapClaim) Valid() error { - vErr := new(ValidationError) - now := TimeFunc().Unix() - - if m.VerifyExpiresAt(now) == false { + // The claims below are optional, by default, so if they are set to the + // default value in Go, let's not fail the verification for them. + if c.VerifyExpiresAt(now, false) == false { vErr.err = "Token is expired" vErr.Errors |= ValidationErrorExpired } - if m.VerifyIssuedAt(now) == false { + if c.VerifyIssuedAt(now, false) == false { vErr.err = "Token used before issued, clock skew issue?" vErr.Errors |= ValidationErrorIssuedAt } - if m.VerifyNotBefore(now) == false { + if c.VerifyNotBefore(now, false) == false { vErr.err = "Token is not valid yet" vErr.Errors |= ValidationErrorNotValidYet } @@ -179,23 +70,143 @@ func (m MapClaim) Valid() error { return vErr } -func verifyAud(aud string, cmp string) bool { - return aud == cmp +// Compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { + return verifyAud(c.Audience, cmp, req) } -func verifyExp(exp int64, now int64) bool { +// Compares the exp claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { + return verifyExp(c.ExpiresAt, cmp, req) +} + +// Compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { + return verifyIat(c.IssuedAt, cmp, req) +} + +// Compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool { + return verifyIss(c.Issuer, cmp, req) +} + +// Compares the nbf claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { + return verifyNbf(c.NotBefore, cmp, req) +} + +type MapClaim map[string]interface{} + +// Compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyAudience(cmp string, req bool) bool { + aud, _ := m["aud"].(string) + return verifyAud(aud, cmp, req) +} + +// Compares the exp claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyExpiresAt(cmp int64, req bool) bool { + exp, _ := m["exp"].(float64) + return verifyExp(int64(exp), cmp, req) +} + +// Compares the iat claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyIssuedAt(cmp int64, req bool) bool { + iat, _ := m["iat"].(float64) + return verifyIat(int64(iat), cmp, req) +} + +// Compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyIssuer(cmp string, req bool) bool { + iss, _ := m["iss"].(string) + return verifyIss(iss, cmp, req) +} + +// Compares the nbf claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (m MapClaim) VerifyNotBefore(cmp int64, req bool) bool { + nbf, _ := m["nbf"].(float64) + return verifyNbf(int64(nbf), cmp, req) +} + +// 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 +// be considered a valid claim. +func (m MapClaim) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + if m.VerifyExpiresAt(now, false) == false { + vErr.err = "Token is expired" + vErr.Errors |= ValidationErrorExpired + } + + if m.VerifyIssuedAt(now, false) == false { + vErr.err = "Token used before issued, clock skew issue?" + vErr.Errors |= ValidationErrorIssuedAt + } + + if m.VerifyNotBefore(now, false) == false { + vErr.err = "Token is not valid yet" + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} + +func verifyAud(aud string, cmp string, required bool) bool { + if aud == "" { + return !required + } + if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 { + return true + } else { + return false + } +} + +func verifyExp(exp int64, now int64, required bool) bool { + if exp == 0 { + return !required + } return now <= exp } -func verifyIat(iat int64, now int64) bool { +func verifyIat(iat int64, now int64, required bool) bool { + if iat == 0 { + return !required + } return now >= iat } -func verifyIss(iss string, cmp string) bool { - return iss == cmp +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 + } } -func verifyNbf(nbf int64, now int64) bool { +func verifyNbf(nbf int64, now int64, required bool) bool { + if nbf == 0 { + return !required + } return now >= nbf }