From 226fdbafab91b0e506f962e393319c298d7359e3 Mon Sep 17 00:00:00 2001 From: albertony <12441419+albertony@users.noreply.github.com> Date: Tue, 4 Jul 2023 17:41:34 +0200 Subject: [PATCH] Compatibility with jwt-go (golang-jwt) v5 Includes the StandardClaims implementation from jwt-go v4, where it was marked as deprecated before removed in v5. The box backend needs this. See #7115 --- backend/box/box.go | 9 +-- lib/jwtutil/claims.go | 160 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 lib/jwtutil/claims.go diff --git a/backend/box/box.go b/backend/box/box.go index 2698f9485..50a33b9a7 100644 --- a/backend/box/box.go +++ b/backend/box/box.go @@ -27,7 +27,6 @@ import ( "sync/atomic" "time" - "github.com/golang-jwt/jwt/v4" "github.com/rclone/rclone/backend/box/api" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/config" @@ -77,7 +76,7 @@ var ( ) type boxCustomClaims struct { - jwt.StandardClaims + jwtutil.StandardClaims BoxSubType string `json:"box_sub_type,omitempty"` } @@ -225,10 +224,8 @@ func getClaims(boxConfig *api.ConfigJSON, boxSubType string) (claims *boxCustomC } claims = &boxCustomClaims{ - //lint:ignore SA1019 since we need to use jwt.StandardClaims even if deprecated in jwt-go v4 until a more permanent solution is ready in time before jwt-go v5 where it is removed entirely - //nolint:staticcheck // Don't include staticcheck when running golangci-lint to avoid SA1019 - StandardClaims: jwt.StandardClaims{ - Id: val, + StandardClaims: jwtutil.StandardClaims{ + ID: val, Issuer: boxConfig.BoxAppSettings.ClientID, Subject: boxConfig.EnterpriseID, Audience: tokenURL, diff --git a/lib/jwtutil/claims.go b/lib/jwtutil/claims.go new file mode 100644 index 000000000..d5340267a --- /dev/null +++ b/lib/jwtutil/claims.go @@ -0,0 +1,160 @@ +package jwtutil + +import ( + "crypto/subtle" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v4" +) + +// StandardClaims implementation from jwt-go v4, where it was marked as deprecated +// before removed in v5. +// See: https://github.com/golang-jwt/jwt/blob/v4/claims.go +// +// StandardClaims are a structured version of the JWT Claims Set, as referenced at +// https://datatracker.ietf.org/doc/html/rfc7519#section-4. They do not follow the +// specification exactly, since they were based on an earlier draft of the +// specification and not updated. The main difference is that they only +// support integer-based date fields and singular audiences. This might lead to +// incompatibilities with other JWT implementations. The use of this is discouraged, instead +// the newer RegisteredClaims struct should be used. +type StandardClaims struct { + Audience string `json:"aud,omitempty"` + ExpiresAt int64 `json:"exp,omitempty"` + ID string `json:"jti,omitempty"` + IssuedAt int64 `json:"iat,omitempty"` + Issuer string `json:"iss,omitempty"` + NotBefore int64 `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` +} + +// Valid 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(jwt.ValidationError) + now := jwt.TimeFunc().Unix() + + // 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) { + delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0)) + vErr.Inner = fmt.Errorf("%s by %s", jwt.ErrTokenExpired, delta) + vErr.Errors |= jwt.ValidationErrorExpired + } + + if !c.VerifyIssuedAt(now, false) { + vErr.Inner = jwt.ErrTokenUsedBeforeIssued + vErr.Errors |= jwt.ValidationErrorIssuedAt + } + + if !c.VerifyNotBefore(now, false) { + vErr.Inner = jwt.ErrTokenNotValidYet + vErr.Errors |= jwt.ValidationErrorNotValidYet + } + + if vErr.Errors == 0 { + 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 (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { + return verifyAud([]string{c.Audience}, cmp, req) +} + +// VerifyExpiresAt compares the exp claim against cmp (cmp < exp). +// If req is false, it will return true, if exp is unset. +func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { + if c.ExpiresAt == 0 { + return verifyExp(nil, time.Unix(cmp, 0), req) + } + + t := time.Unix(c.ExpiresAt, 0) + return verifyExp(&t, time.Unix(cmp, 0), req) +} + +// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat). +// If req is false, it will return true, if iat is unset. +func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { + if c.IssuedAt == 0 { + return verifyIat(nil, time.Unix(cmp, 0), req) + } + + t := time.Unix(c.IssuedAt, 0) + return verifyIat(&t, time.Unix(cmp, 0), req) +} + +// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). +// If req is false, it will return true, if nbf is unset. +func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { + if c.NotBefore == 0 { + return verifyNbf(nil, time.Unix(cmp, 0), req) + } + + t := time.Unix(c.NotBefore, 0) + return verifyNbf(&t, time.Unix(cmp, 0), req) +} + +// VerifyIssuer 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) +} + +// ----- 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 len(stringClaims) == 0 { + return !required + } + + return result +} + +func verifyExp(exp *time.Time, now time.Time, required bool) bool { + if exp == nil { + return !required + } + return now.Before(*exp) +} + +func verifyIat(iat *time.Time, now time.Time, required bool) bool { + if iat == nil { + return !required + } + return now.After(*iat) || now.Equal(*iat) +} + +func verifyNbf(nbf *time.Time, now time.Time, required bool) bool { + if nbf == nil { + return !required + } + return now.After(*nbf) || now.Equal(*nbf) +} + +func verifyIss(iss string, cmp string, required bool) bool { + if iss == "" { + return !required + } + return subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 +}