diff --git a/changelog/unreleased/fix-allow-token-clock-skew.md b/changelog/unreleased/fix-allow-token-clock-skew.md new file mode 100644 index 0000000000..0e706455e0 --- /dev/null +++ b/changelog/unreleased/fix-allow-token-clock-skew.md @@ -0,0 +1,7 @@ +Bugfix: Allow small clock skew in reva token validation + +Allow for a small clock skew (3 seconds by default) when validating reva tokens +as the different services might be running on different machines. + +https://github.com/cs3org/reva/pull/49xx +https://github.com/cs3org/reva/issues/4952 diff --git a/go.mod b/go.mod index 2bb0e43a8e..e45067e28d 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.8.1 github.com/gofrs/flock v0.12.1 - github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang/protobuf v1.5.4 github.com/gomodule/redigo v1.9.2 github.com/google/go-cmp v0.6.0 diff --git a/go.sum b/go.sum index 9c4f696b4c..eb52b6a2f6 100644 --- a/go.sum +++ b/go.sum @@ -152,8 +152,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJffz4pz0o1WuQxJ28+5x5JgaHD8= github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4= -github.com/cs3org/go-cs3apis v0.0.0-20241105082517-48ba3368a5bd h1:Ji7OTOGVAOynkkiro1Rgv/0sXh1GEWV+4hmGiKmKV3A= -github.com/cs3org/go-cs3apis v0.0.0-20241105082517-48ba3368a5bd/go.mod h1:DedpcqXl193qF/08Y04IO0PpxyyMu8+GrkD6kWK2MEQ= github.com/cs3org/go-cs3apis v0.0.0-20241105092511-3ad35d174fc1 h1:RU6LT6mkD16xZs011+8foU7T3LrPvTTSWeTQ9OgfhkA= github.com/cs3org/go-cs3apis v0.0.0-20241105092511-3ad35d174fc1/go.mod h1:DedpcqXl193qF/08Y04IO0PpxyyMu8+GrkD6kWK2MEQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -288,8 +286,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 978a687831..b3fbb11e18 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -48,7 +48,7 @@ import ( "github.com/cs3org/reva/v2/pkg/share" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" "github.com/pkg/errors" gstatus "google.golang.org/grpc/status" ) @@ -78,7 +78,7 @@ import ( // transferClaims are custom claims for a JWT token to be used between the metadata and data gateways. type transferClaims struct { - jwt.StandardClaims + jwt.RegisteredClaims Target string `json:"target"` } @@ -86,10 +86,10 @@ func (s *svc) sign(_ context.Context, target string, expiresAt int64) (string, e // Tus sends a separate request to the datagateway service for every chunk. // For large files, this can take a long time, so we extend the expiration claims := transferClaims{ - StandardClaims: jwt.StandardClaims{ - ExpiresAt: expiresAt, - Audience: "reva", - IssuedAt: time.Now().Unix(), + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Unix(expiresAt, 0)), + Audience: jwt.ClaimStrings{"reva"}, + IssuedAt: jwt.NewNumericDate(time.Now()), }, Target: target, } diff --git a/internal/http/services/datagateway/datagateway.go b/internal/http/services/datagateway/datagateway.go index 53ba79af81..8f8fcf4666 100644 --- a/internal/http/services/datagateway/datagateway.go +++ b/internal/http/services/datagateway/datagateway.go @@ -32,7 +32,7 @@ import ( "github.com/cs3org/reva/v2/pkg/rhttp" "github.com/cs3org/reva/v2/pkg/rhttp/global" "github.com/cs3org/reva/v2/pkg/sharedconf" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -58,7 +58,7 @@ func init() { // transferClaims are custom claims for a JWT token to be used between the metadata and data gateways. type transferClaims struct { - jwt.StandardClaims + jwt.RegisteredClaims Target string `json:"target"` } type config struct { diff --git a/pkg/app/provider/wopi/wopi.go b/pkg/app/provider/wopi/wopi.go index d0356a3536..45167be1e2 100644 --- a/pkg/app/provider/wopi/wopi.go +++ b/pkg/app/provider/wopi/wopi.go @@ -47,7 +47,7 @@ import ( "github.com/cs3org/reva/v2/pkg/sharedconf" "github.com/cs3org/reva/v2/pkg/storage/utils/templates" "github.com/cs3org/reva/v2/pkg/storagespace" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" ) @@ -379,16 +379,16 @@ func getAppURLs(c *config) (map[string]map[string]string, error) { func (p *wopiProvider) getAccessTokenTTL(ctx context.Context) (string, error) { tkn := ctxpkg.ContextMustGetToken(ctx) - token, err := jwt.ParseWithClaims(tkn, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { + token, err := jwt.ParseWithClaims(tkn, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(p.conf.JWTSecret), nil }) if err != nil { return "", err } - if claims, ok := token.Claims.(*jwt.StandardClaims); ok && token.Valid { + if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid { // milliseconds since Jan 1, 1970 UTC as required in https://wopi.readthedocs.io/projects/wopirest/en/latest/concepts.html?highlight=access_token_ttl#term-access-token-ttl - return strconv.FormatInt(claims.ExpiresAt*1000, 10), nil + return strconv.FormatInt(claims.ExpiresAt.Unix()*1000, 10), nil } return "", errtypes.InvalidCredentials("wopi: invalid token present in ctx") diff --git a/pkg/siteacc/manager/token.go b/pkg/siteacc/manager/token.go index d1c0309b67..d83bdccbbe 100644 --- a/pkg/siteacc/manager/token.go +++ b/pkg/siteacc/manager/token.go @@ -21,13 +21,13 @@ package manager import ( "time" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" "github.com/pkg/errors" "github.com/sethvargo/go-password/password" ) type userToken struct { - jwt.StandardClaims + jwt.RegisteredClaims User string `json:"user"` Scope string `json:"scope"` @@ -45,10 +45,10 @@ var ( func generateUserToken(user string, scope string, timeout int) (string, error) { // Create a JWT as the user token claims := userToken{ - StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(time.Duration(timeout) * time.Second).Unix(), + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(timeout) * time.Second)), Issuer: tokenIssuer, - IssuedAt: time.Now().Unix(), + IssuedAt: jwt.NewNumericDate(time.Now()), }, User: user, Scope: scope, diff --git a/pkg/storage/utils/decomposedfs/upload/upload.go b/pkg/storage/utils/decomposedfs/upload/upload.go index f460c526ed..7476d10058 100644 --- a/pkg/storage/utils/decomposedfs/upload/upload.go +++ b/pkg/storage/utils/decomposedfs/upload/upload.go @@ -33,7 +33,7 @@ import ( userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" "github.com/pkg/errors" tusd "github.com/tus/tusd/v2/pkg/handler" "go.opentelemetry.io/otel" @@ -372,17 +372,17 @@ func (session *OcisSession) Cleanup(revertNodeMetadata, cleanBin, cleanInfo bool // URL returns a url to download an upload func (session *OcisSession) URL(_ context.Context) (string, error) { type transferClaims struct { - jwt.StandardClaims + jwt.RegisteredClaims Target string `json:"target"` } u := joinurl(session.store.tknopts.DownloadEndpoint, "tus/", session.ID()) ttl := time.Duration(session.store.tknopts.TransferExpires) * time.Second claims := transferClaims{ - StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(ttl).Unix(), - Audience: "reva", - IssuedAt: time.Now().Unix(), + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)), + Audience: jwt.ClaimStrings{"reva"}, + IssuedAt: jwt.NewNumericDate(time.Now()), }, Target: u, } diff --git a/pkg/token/manager/jwt/jwt.go b/pkg/token/manager/jwt/jwt.go index f331a26ee0..11cf993dfe 100644 --- a/pkg/token/manager/jwt/jwt.go +++ b/pkg/token/manager/jwt/jwt.go @@ -28,20 +28,22 @@ import ( "github.com/cs3org/reva/v2/pkg/sharedconf" "github.com/cs3org/reva/v2/pkg/token" "github.com/cs3org/reva/v2/pkg/token/manager/registry" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" ) const defaultExpiration int64 = 86400 // 1 day +const defaultLeeway int64 = 10 func init() { registry.Register("jwt", New) } type config struct { - Secret string `mapstructure:"secret"` - Expires int64 `mapstructure:"expires"` + Secret string `mapstructure:"secret"` + Expires int64 `mapstructure:"expires"` + tokenTimeLeeway int64 `mapstructure:"token_leeway"` } type manager struct { @@ -50,7 +52,7 @@ type manager struct { // claims are custom claims for the JWT token. type claims struct { - jwt.StandardClaims + jwt.RegisteredClaims User *user.User `json:"user"` Scope map[string]*auth.Scope `json:"scope"` } @@ -75,6 +77,10 @@ func New(value map[string]interface{}) (token.Manager, error) { c.Expires = defaultExpiration } + if c.tokenTimeLeeway == 0 { + c.tokenTimeLeeway = defaultLeeway + } + c.Secret = sharedconf.GetJWTSecret(c.Secret) if c.Secret == "" { @@ -87,31 +93,32 @@ func New(value map[string]interface{}) (token.Manager, error) { func (m *manager) MintToken(ctx context.Context, u *user.User, scope map[string]*auth.Scope) (string, error) { ttl := time.Duration(m.conf.Expires) * time.Second - claims := claims{ - StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(ttl).Unix(), + newClaims := claims{ + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)), Issuer: u.Id.Idp, - Audience: "reva", - IssuedAt: time.Now().Unix(), + Audience: jwt.ClaimStrings{"reva"}, + IssuedAt: jwt.NewNumericDate(time.Now()), }, User: u, Scope: scope, } - t := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims) + t := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), newClaims) tkn, err := t.SignedString([]byte(m.conf.Secret)) if err != nil { - return "", errors.Wrapf(err, "error signing token with claims %+v", claims) + return "", errors.Wrapf(err, "error signing token with claims %+v", newClaims) } return tkn, nil } func (m *manager) DismantleToken(ctx context.Context, tkn string) (*user.User, map[string]*auth.Scope, error) { - token, err := jwt.ParseWithClaims(tkn, &claims{}, func(token *jwt.Token) (interface{}, error) { + keyfunc := func(token *jwt.Token) (interface{}, error) { return []byte(m.conf.Secret), nil - }) + } + token, err := jwt.ParseWithClaims(tkn, &claims{}, keyfunc, jwt.WithLeeway(time.Duration(m.conf.tokenTimeLeeway)*time.Second)) if err != nil { return nil, nil, errors.Wrap(err, "error parsing token")