Repository: owncloud/ocis
Pull request #10391 (2024-10-23 09:34:48)

Collaboration short tokens
<!--
Thanks for submitting a change to ownCloud!

This is the bug tracker for the oCIS component. Find other components at https://github.com/owncloud/core/blob/master/.github/CONTRIBUTING.md#guidelines

For fixing potential security issues please see https://owncloud.org/security/

To make it possible for us to get your change reviewed and merged please carefully fill out the requested information below.

Please note that any kind of change needs first be submitted to the master branch which holds the next version of oCIS.

Please set the following labels:

- Set label "Status:Needs-Review" for review or "Status:In-Progress" if the PR still has open tasks.
- Assignment: assign to self
- Reviewers: pick at least one
-->

## Description
Generate short tokens to be used as access tokens for WOPI.

Currently, the access tokens being used might be too long. In particular, Office Online complains about the URL (which contains the access token) is too long and refuses to work.

A new config option `COLLABORATION_WOPI_SHORTTOKENS` has been added in order to overcome that limitation. Note that this option can be used with any WOPI application (Collabora, OnlyOffice, etc)
In order to use this option, **you must configure a store** so OCIS can store the access tokens.

By default, the store will be configured to connect to "127.0.0.1:9233", which is where the nats service should be listening. **If you have the collaboration in a different container**, you probably want to connect to a different container such as "ocis:9233".
In addition, the TTL is configured to 30 minutes. You can rise the value if needed, but lowering it will likely cause problems because valid tokens will be deleted prematurely.

To sum up, if you enable the `COLLABORATION_WOPI_SHORTTOKENS`, make sure either `OCIS_PERSISTENT_STORE_NODES` or `COLLABORATION_STORE_NODES` are configured properly to store the tokens.

## Related Issue
https://github.com/owncloud/enterprise/issues/6790

## Motivation and Context
The URL is too long with the access token and we need to make it shorter.

## How Has This Been Tested?
Manually tested with Collabora. The access token has around 48 chars.

## Screenshots (if appropriate):

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Technical debt
- [ ] Tests only (no source changes)

## Checklist:
<!-- Tick the checkboxes when done. -->
<!-- Raise documentation ticket in owncloud.github.io/ -->
- [x] Code changes
- [ ] Unit tests added
- [ ] Acceptance tests added
- [ ] Documentation ticket raised: <link> 


update-docs[bot]: Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a [changelog](https://github.com/owncloud/ocis/blob/master/changelog/README.md) item based on your changes.

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=10391) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=10391&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=10391&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=10391&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=10391&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=10391&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=10391)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=10391) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [3 New Bugs](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=10391) (required ≤ 0)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=10391)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


micbar: @jvillafanez
Thanks for the improvement.

I tested it in a test environment with MS365 and MSOO. It works perfectly fine.

With this improvement, the online WOPI validator is running also.

I could find a small bug in PutRelative. Will issue a new PR.

Patch:
diff --git a/changelog/unreleased/collaboration-short-tokens.md b/changelog/unreleased/collaboration-short-tokens.md
new file mode 100644
index 00000000000..2ad3047744d
--- /dev/null
+++ b/changelog/unreleased/collaboration-short-tokens.md
@@ -0,0 +1,7 @@
+Bugfix: Generate short tokens to be used as access tokens for WOPI
+
+Currently, the access tokens being used might be too long.
+In particular, Microsoft Office Online complains about the URL (which contains the access token)
+is too long and refuses to work. 
+
+https://github.com/owncloud/ocis/pull/10391
diff --git a/services/collaboration/README.md b/services/collaboration/README.md
index 5daa8db5143..fd4e3e0daa7 100644
--- a/services/collaboration/README.md
+++ b/services/collaboration/README.md
@@ -41,4 +41,27 @@ There are a few variables that you need to set:
   The external address of the collaboration service. The target app (onlyoffice, collabora, etc) will use this address to read and write files from Infinite Scale.\
   For example: `https://wopi.example.com`.
 
+* `COLLABORATION_WOPI_SHORTTOKENS`:\
+  Needs to be set if the office application like `Microsoft Office Online` complains about the URL is too long  (which contains the access token) and refuses to work. If enabled, a store must be configured.
+
 The application can be customized further by changing the `COLLABORATION_APP_*` options to better describe the application.
+
+## Storing
+
+The `collaboration` service persists information via the configured store in `COLLABORATION_STORE`. Possible stores are:
+  -   `memory`: Basic in-memory store. Will not survive a restart. This is not recommended for this service.
+  -   `redis-sentinel`: Stores data in a configured Redis Sentinel cluster.
+  -   `nats-js-kv`: Stores data using key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store). This is the default value.
+  -   `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
+
+Other store types may work but are not supported currently.
+
+Note: The service can only be scaled if not using `memory` store and the stores are configured identically over all instances!
+
+Note that if you have used one of the deprecated stores, you should reconfigure to one of the supported ones as the deprecated stores will be removed in a later version.
+
+Store specific notes:
+  -   When using `redis-sentinel`, the Redis master to use is configured via e.g. `OCIS_CACHE_STORE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
+  -   When using `nats-js-kv` it is recommended to set `OCIS_CACHE_STORE_NODES` to the same value as `OCIS_EVENTS_ENDPOINT`. That way the cache uses the same nats instance as the event bus.
+  -   When using the `nats-js-kv` store, it is possible to set `OCIS_CACHE_DISABLE_PERSISTENCE` to instruct nats to not persist cache data on disc.
+
diff --git a/services/collaboration/pkg/command/server.go b/services/collaboration/pkg/command/server.go
index 38d36e3b220..fbfcc88f91b 100644
--- a/services/collaboration/pkg/command/server.go
+++ b/services/collaboration/pkg/command/server.go
@@ -6,6 +6,7 @@ import (
 	"net"
 
 	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
+	"github.com/cs3org/reva/v2/pkg/store"
 	"github.com/oklog/run"
 	"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
 	registry "github.com/owncloud/ocis/v2/ocis-pkg/registry"
@@ -19,6 +20,7 @@ import (
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/server/grpc"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/server/http"
 	"github.com/urfave/cli/v2"
+	microstore "go-micro.dev/v4/store"
 )
 
 // Server is the entrypoint for the server command.
@@ -70,12 +72,22 @@ func Server(cfg *config.Config) *cli.Command {
 				return err
 			}
 
+			st := store.Create(
+				store.Store(cfg.Store.Store),
+				store.TTL(cfg.Store.TTL),
+				microstore.Nodes(cfg.Store.Nodes...),
+				microstore.Database(cfg.Store.Database),
+				microstore.Table(cfg.Store.Table),
+				store.Authentication(cfg.Store.AuthUsername, cfg.Store.AuthPassword),
+			)
+
 			// start GRPC server
 			grpcServer, teardown, err := grpc.Server(
 				grpc.AppURLs(appUrls),
 				grpc.Config(cfg),
 				grpc.Logger(logger),
 				grpc.TraceProvider(traceProvider),
+				grpc.Store(st),
 			)
 			defer teardown()
 			if err != nil {
@@ -113,11 +125,12 @@ func Server(cfg *config.Config) *cli.Command {
 
 			// start HTTP server
 			httpServer, err := http.Server(
-				http.Adapter(connector.NewHttpAdapter(gatewaySelector, cfg)),
+				http.Adapter(connector.NewHttpAdapter(gatewaySelector, cfg, st)),
 				http.Logger(logger),
 				http.Config(cfg),
 				http.Context(ctx),
 				http.TracerProvider(traceProvider),
+				http.Store(st),
 			)
 			if err != nil {
 				logger.Error().Err(err).Str("transport", "http").Msg("Failed to initialize server")
diff --git a/services/collaboration/pkg/config/config.go b/services/collaboration/pkg/config/config.go
index b7079343f11..ecedf4b2755 100644
--- a/services/collaboration/pkg/config/config.go
+++ b/services/collaboration/pkg/config/config.go
@@ -13,6 +13,7 @@ type Config struct {
 
 	Service Service `yaml:"-"`
 	App     App     `yaml:"app"`
+	Store   Store   `yaml:"store"`
 
 	TokenManager *TokenManager `yaml:"token_manager"`
 
diff --git a/services/collaboration/pkg/config/defaults/defaultconfig.go b/services/collaboration/pkg/config/defaults/defaultconfig.go
index 64810013a53..d8bc4aab3c4 100644
--- a/services/collaboration/pkg/config/defaults/defaultconfig.go
+++ b/services/collaboration/pkg/config/defaults/defaultconfig.go
@@ -1,6 +1,8 @@
 package defaults
 
 import (
+	"time"
+
 	"github.com/owncloud/ocis/v2/ocis-pkg/shared"
 	"github.com/owncloud/ocis/v2/ocis-pkg/structs"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
@@ -32,6 +34,13 @@ func DefaultConfig() *config.Config {
 				Duration: "12h",
 			},
 		},
+		Store: config.Store{
+			Store:    "nats-js-kv",
+			Nodes:    []string{"127.0.0.1:9233"},
+			Database: "collaboration",
+			Table:    "",
+			TTL:      30 * time.Minute,
+		},
 		GRPC: config.GRPC{
 			Addr:      "127.0.0.1:9301",
 			Protocol:  "tcp",
diff --git a/services/collaboration/pkg/config/store.go b/services/collaboration/pkg/config/store.go
new file mode 100644
index 00000000000..6f2051e4b10
--- /dev/null
+++ b/services/collaboration/pkg/config/store.go
@@ -0,0 +1,14 @@
+package config
+
+import "time"
+
+// Store configures the store to use
+type Store struct {
+	Store        string        `yaml:"store" env:"OCIS_PERSISTENT_STORE;COLLABORATION_STORE" desc:"The type of the store. Supported values are: 'memory', 'nats-js-kv', 'redis-sentinel', 'noop'. See the text description for details." introductionVersion:"7.0.0"`
+	Nodes        []string      `yaml:"nodes" env:"OCIS_PERSISTENT_STORE_NODES;COLLABORATION_STORE_NODES" desc:"A list of nodes to access the configured store. This has no effect when 'memory' store is configured. Note that the behaviour how nodes are used is dependent on the library of the configured store. See the Environment Variable Types description for more details." introductionVersion:"7.0.0"`
+	Database     string        `yaml:"database" env:"COLLABORATION_STORE_DATABASE" desc:"The database name the configured store should use." introductionVersion:"7.0.0"`
+	Table        string        `yaml:"table" env:"COLLABORATION_STORE_TABLE" desc:"The database table the store should use." introductionVersion:"7.0.0"`
+	TTL          time.Duration `yaml:"ttl" env:"OCIS_PERSISTENT_STORE_TTL;COLLABORATION_STORE_TTL" desc:"Time to live for events in the store. Defaults to '30m' (30 minutes). See the Environment Variable Types description for more details." introductionVersion:"7.0.0"`
+	AuthUsername string        `yaml:"username" env:"OCIS_PERSISTENT_STORE_AUTH_USERNAME;COLLABORATION_STORE_AUTH_USERNAME" desc:"The username to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"7.0.0"`
+	AuthPassword string        `yaml:"password" env:"OCIS_PERSISTENT_STORE_AUTH_PASSWORD;COLLABORATION_STORE_AUTH_PASSWORD" desc:"The password to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"7.0.0"`
+}
diff --git a/services/collaboration/pkg/config/wopi.go b/services/collaboration/pkg/config/wopi.go
index 428c7f5b996..f4705a026a5 100644
--- a/services/collaboration/pkg/config/wopi.go
+++ b/services/collaboration/pkg/config/wopi.go
@@ -7,4 +7,5 @@ type Wopi struct {
 	DisableChat bool   `yaml:"disable_chat" env:"COLLABORATION_WOPI_DISABLE_CHAT;OCIS_WOPI_DISABLE_CHAT" desc:"Disable chat in the office web frontend. This feature applies to OnlyOffice and Microsoft." introductionVersion:"7.0.0"`
 	ProxyURL    string `yaml:"proxy_url" env:"COLLABORATION_WOPI_PROXY_URL" desc:"The URL to the ownCloud Office365 WOPI proxy. Optional. To use this feature, you need an office365 proxy subscription. If you become part of the Microsoft CSP program (https://learn.microsoft.com/en-us/partner-center/enroll/csp-overview), you can use WebOffice without a proxy." introductionVersion:"7.0.0"`
 	ProxySecret string `yaml:"proxy_secret" env:"COLLABORATION_WOPI_PROXY_SECRET" desc:"Optional, the secret to authenticate against the ownCloud Office365 WOPI proxy. This secret can be obtained from ownCloud via the office365 proxy subscription." introductionVersion:"7.0.0"`
+	ShortTokens bool   `yaml:"short_tokens" env:"COLLABORATION_WOPI_SHORTTOKENS" desc:"Use short access tokens for WOPI access. This is useful for office packages, like Microsoft Office Online, which have URL length restrictions. If enabled, a persistent store must be configured." introductionVersion:"7.0.0"`
 }
diff --git a/services/collaboration/pkg/connector/fileconnector.go b/services/collaboration/pkg/connector/fileconnector.go
index d63a4592ed1..f6156ac4931 100644
--- a/services/collaboration/pkg/connector/fileconnector.go
+++ b/services/collaboration/pkg/connector/fileconnector.go
@@ -29,6 +29,7 @@ import (
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
 	"github.com/rs/zerolog"
+	microstore "go-micro.dev/v4/store"
 )
 
 const (
@@ -95,15 +96,17 @@ type FileConnectorService interface {
 // Currently, it handles file locks and getting the file info.
 // Note that operations might return any kind of error, not just ConnectorError
 type FileConnector struct {
-	gws pool.Selectable[gatewayv1beta1.GatewayAPIClient]
-	cfg *config.Config
+	gws   pool.Selectable[gatewayv1beta1.GatewayAPIClient]
+	cfg   *config.Config
+	store microstore.Store
 }
 
 // NewFileConnector creates a new file connector
-func NewFileConnector(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config) *FileConnector {
+func NewFileConnector(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config, st microstore.Store) *FileConnector {
 	return &FileConnector{
-		gws: gws,
-		cfg: cfg,
+		gws:   gws,
+		cfg:   cfg,
+		store: st,
 	}
 }
 
@@ -1265,7 +1268,7 @@ func (f *FileConnector) createDownloadURL(wopiContext middleware.WopiContext, co
 	templateContext.FileReference = wopiContext.TemplateReference
 	templateContext.TemplateReference = nil
 
-	token, _, err := middleware.GenerateWopiToken(templateContext, f.cfg)
+	token, _, err := middleware.GenerateWopiToken(templateContext, f.cfg, f.store)
 	if err != nil {
 		return "", err
 	}
@@ -1389,7 +1392,7 @@ func (f *FileConnector) generatePrefix() string {
 // will be ignored
 func (f *FileConnector) generateWOPISrc(wopiContext middleware.WopiContext, logger zerolog.Logger) (*url.URL, error) {
 	// get the WOPI token for the new file
-	accessToken, _, err := middleware.GenerateWopiToken(wopiContext, f.cfg)
+	accessToken, _, err := middleware.GenerateWopiToken(wopiContext, f.cfg, f.store)
 	if err != nil {
 		logger.Error().Err(err).Msg("generateWOPISrc: failed to generate access token for the new file")
 		return nil, err
diff --git a/services/collaboration/pkg/connector/httpadapter.go b/services/collaboration/pkg/connector/httpadapter.go
index 0054df39fac..5cb594731e2 100644
--- a/services/collaboration/pkg/connector/httpadapter.go
+++ b/services/collaboration/pkg/connector/httpadapter.go
@@ -12,6 +12,7 @@ import (
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/connector/utf7"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/locks"
 	"github.com/rs/zerolog"
+	microstore "go-micro.dev/v4/store"
 )
 
 const (
@@ -44,10 +45,10 @@ type HttpAdapter struct {
 
 // NewHttpAdapter will create a new HTTP adapter. A new connector using the
 // provided gateway API client and configuration will be used in the adapter
-func NewHttpAdapter(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config) *HttpAdapter {
+func NewHttpAdapter(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config, st microstore.Store) *HttpAdapter {
 	httpAdapter := &HttpAdapter{
 		con: NewConnector(
-			NewFileConnector(gws, cfg),
+			NewFileConnector(gws, cfg, st),
 			NewContentConnector(gws, cfg),
 		),
 	}
diff --git a/services/collaboration/pkg/middleware/wopicontext.go b/services/collaboration/pkg/middleware/wopicontext.go
index 02ed2ea32fe..d16df6c66bd 100644
--- a/services/collaboration/pkg/middleware/wopicontext.go
+++ b/services/collaboration/pkg/middleware/wopicontext.go
@@ -2,10 +2,14 @@ package middleware
 
 import (
 	"context"
+	"crypto/md5"
+	"encoding/hex"
 	"errors"
 	"fmt"
 	"net/http"
+	"strconv"
 	"strings"
+	"time"
 
 	appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
 	providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
@@ -15,6 +19,7 @@ import (
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
 	"github.com/rs/zerolog"
+	microstore "go-micro.dev/v4/store"
 	"google.golang.org/grpc/metadata"
 )
 
@@ -44,7 +49,7 @@ type WopiContext struct {
 // * The created WopiContext for the request
 // * A contextual zerologger containing information about the request
 // and the WopiContext
-func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handler {
+func WopiContextAuthMiddleware(cfg *config.Config, st microstore.Store, next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		ctx := r.Context()
 
@@ -67,6 +72,23 @@ func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handl
 			return
 		}
 
+		if cfg.Wopi.ShortTokens {
+			records, err := st.Read(accessToken)
+			if err != nil {
+				wopiLogger.Error().Err(err).Msg("cannot retrieve access token from store")
+				http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+				return
+			}
+
+			if len(records) != 1 {
+				wopiLogger.Error().Int("records", len(records)).Msg("no record found for the token")
+				http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+				return
+			}
+
+			accessToken = string(records[0].Value)
+		}
+
 		claims := &Claims{}
 		_, err := jwt.ParseWithClaims(accessToken, claims, func(token *jwt.Token) (interface{}, error) {
 
@@ -163,7 +185,11 @@ func WopiContextToCtx(ctx context.Context, wopiContext WopiContext) context.Cont
 // The access token inside the wopiContext is expected to be decrypted.
 // In order to generate the access token for WOPI, the reva token inside the
 // wopiContext will be encrypted
-func GenerateWopiToken(wopiContext WopiContext, cfg *config.Config) (string, int64, error) {
+func GenerateWopiToken(wopiContext WopiContext, cfg *config.Config, st microstore.Store) (string, int64, error) {
+	if cfg.Wopi.ShortTokens && st == nil {
+		return "", 0, errors.New("Cannot generate a short token without microstore")
+	}
+
 	cryptedReqAccessToken, err := EncryptAES([]byte(cfg.Wopi.Secret), wopiContext.AccessToken)
 	if err != nil {
 		return "", 0, err
@@ -187,6 +213,20 @@ func GenerateWopiToken(wopiContext WopiContext, cfg *config.Config) (string, int
 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
 	accessToken, err := token.SignedString([]byte(cfg.Wopi.Secret))
 
+	if cfg.Wopi.ShortTokens {
+		c := md5.New()
+		c.Write([]byte(accessToken))
+		shortAccessToken := hex.EncodeToString(c.Sum(nil)) + strconv.FormatInt(time.Now().UnixNano(), 16)
+
+		errWrite := st.Write(&microstore.Record{
+			Key:    shortAccessToken,
+			Value:  []byte(accessToken),
+			Expiry: time.Until(claims.ExpiresAt.Time),
+		})
+
+		return shortAccessToken, claims.ExpiresAt.UnixMilli(), errWrite
+	}
+
 	return accessToken, claims.ExpiresAt.UnixMilli(), err
 }
 
diff --git a/services/collaboration/pkg/server/grpc/option.go b/services/collaboration/pkg/server/grpc/option.go
index df6d23d7d10..4fda3077e4f 100644
--- a/services/collaboration/pkg/server/grpc/option.go
+++ b/services/collaboration/pkg/server/grpc/option.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
+	microstore "go-micro.dev/v4/store"
 	"go.opentelemetry.io/otel/trace"
 )
 
@@ -19,6 +20,7 @@ type Options struct {
 	Context       context.Context
 	Config        *config.Config
 	TraceProvider trace.TracerProvider
+	Store         microstore.Store
 }
 
 // newOptions initializes the available default options.
@@ -73,3 +75,10 @@ func TraceProvider(val trace.TracerProvider) Option {
 		o.TraceProvider = val
 	}
 }
+
+// Store provides a function to set the Store option
+func Store(val microstore.Store) Option {
+	return func(o *Options) {
+		o.Store = val
+	}
+}
diff --git a/services/collaboration/pkg/server/grpc/server.go b/services/collaboration/pkg/server/grpc/server.go
index 43964afe229..db383e6635e 100644
--- a/services/collaboration/pkg/server/grpc/server.go
+++ b/services/collaboration/pkg/server/grpc/server.go
@@ -27,6 +27,7 @@ func Server(opts ...Option) (*grpc.Server, func(), error) {
 		svc.Config(options.Config),
 		svc.Logger(options.Logger),
 		svc.AppURLs(options.AppURLs),
+		svc.Store(options.Store),
 	)
 	if err != nil {
 		options.Logger.Error().
diff --git a/services/collaboration/pkg/server/http/option.go b/services/collaboration/pkg/server/http/option.go
index 10628a0fc28..11491a27971 100644
--- a/services/collaboration/pkg/server/http/option.go
+++ b/services/collaboration/pkg/server/http/option.go
@@ -6,6 +6,7 @@ import (
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/connector"
+	microstore "go-micro.dev/v4/store"
 	"go.opentelemetry.io/otel/trace"
 )
 
@@ -19,6 +20,7 @@ type Options struct {
 	Context        context.Context
 	Config         *config.Config
 	TracerProvider trace.TracerProvider
+	Store          microstore.Store
 }
 
 // newOptions initializes the available default options.
@@ -66,3 +68,10 @@ func TracerProvider(val trace.TracerProvider) Option {
 		o.TracerProvider = val
 	}
 }
+
+// Store provides a function to set the Store option
+func Store(val microstore.Store) Option {
+	return func(o *Options) {
+		o.Store = val
+	}
+}
diff --git a/services/collaboration/pkg/server/http/server.go b/services/collaboration/pkg/server/http/server.go
index cf2c3b4ebda..0dec4604047 100644
--- a/services/collaboration/pkg/server/http/server.go
+++ b/services/collaboration/pkg/server/http/server.go
@@ -119,7 +119,7 @@ func prepareRoutes(r *chi.Mux, options Options) {
 			r.Use(
 				func(h stdhttp.Handler) stdhttp.Handler {
 					// authentication and wopi context
-					return colabmiddleware.WopiContextAuthMiddleware(options.Config, h)
+					return colabmiddleware.WopiContextAuthMiddleware(options.Config, options.Store, h)
 				},
 				colabmiddleware.CollaborationTracingMiddleware,
 			)
@@ -186,7 +186,7 @@ func prepareRoutes(r *chi.Mux, options Options) {
 			r.Use(
 				func(h stdhttp.Handler) stdhttp.Handler {
 					// authentication and wopi context
-					return colabmiddleware.WopiContextAuthMiddleware(options.Config, h)
+					return colabmiddleware.WopiContextAuthMiddleware(options.Config, options.Store, h)
 				},
 				colabmiddleware.CollaborationTracingMiddleware,
 			)
diff --git a/services/collaboration/pkg/service/grpc/v0/option.go b/services/collaboration/pkg/service/grpc/v0/option.go
index 3b89ec725bc..2fafbee812d 100644
--- a/services/collaboration/pkg/service/grpc/v0/option.go
+++ b/services/collaboration/pkg/service/grpc/v0/option.go
@@ -4,6 +4,7 @@ import (
 	gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
+	microstore "go-micro.dev/v4/store"
 )
 
 // Option defines a single option function.
@@ -15,6 +16,7 @@ type Options struct {
 	Config  *config.Config
 	AppURLs map[string]map[string]string
 	Gwc     gatewayv1beta1.GatewayAPIClient
+	Store   microstore.Store
 }
 
 // newOptions initializes the available default options.
@@ -55,3 +57,10 @@ func GatewayAPIClient(val gatewayv1beta1.GatewayAPIClient) Option {
 		o.Gwc = val
 	}
 }
+
+// Store proivdes a function to set the store
+func Store(val microstore.Store) Option {
+	return func(o *Options) {
+		o.Store = val
+	}
+}
diff --git a/services/collaboration/pkg/service/grpc/v0/service.go b/services/collaboration/pkg/service/grpc/v0/service.go
index 162d7947c43..5fa63b9ab63 100644
--- a/services/collaboration/pkg/service/grpc/v0/service.go
+++ b/services/collaboration/pkg/service/grpc/v0/service.go
@@ -22,6 +22,7 @@ import (
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
 	"github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware"
+	microstore "go-micro.dev/v4/store"
 )
 
 // NewHandler creates a new grpc service implementing the OpenInApp interface
@@ -47,6 +48,7 @@ func NewHandler(opts ...Option) (*Service, func(), error) {
 		logger:  options.Logger,
 		config:  options.Config,
 		gwc:     gwc,
+		store:   options.Store,
 	}, teardown, nil
 }
 
@@ -57,6 +59,7 @@ type Service struct {
 	logger  log.Logger
 	config  *config.Config
 	gwc     gatewayv1beta1.GatewayAPIClient
+	store   microstore.Store
 }
 
 // OpenInApp will implement the OpenInApp interface of the app provider
@@ -138,7 +141,7 @@ func (s *Service) OpenInApp(
 		}
 	}
 
-	accessToken, accessExpiration, err := middleware.GenerateWopiToken(wopiContext, s.config)
+	accessToken, accessExpiration, err := middleware.GenerateWopiToken(wopiContext, s.config, s.store)
 	if err != nil {
 		logger.Error().Err(err).Msg("OpenInApp: error generating the token")
 		return &appproviderv1beta1.OpenInAppResponse{


Test patch:
diff --git a/services/collaboration/pkg/connector/fileconnector_test.go b/services/collaboration/pkg/connector/fileconnector_test.go
index 923951d2948..34685ff16ae 100644
--- a/services/collaboration/pkg/connector/fileconnector_test.go
+++ b/services/collaboration/pkg/connector/fileconnector_test.go
@@ -59,7 +59,7 @@ var _ = Describe("FileConnector", func() {
 
 		gatewaySelector = mocks.NewSelectable[gateway.GatewayAPIClient](GinkgoT())
 		gatewaySelector.On("Next").Return(gatewayClient, nil)
-		fc = connector.NewFileConnector(gatewaySelector, cfg)
+		fc = connector.NewFileConnector(gatewaySelector, cfg, nil)
 
 		wopiCtx = middleware.WopiContext{
 			// a real token is needed for the PutRelativeFileSuggested tests
diff --git a/services/collaboration/pkg/middleware/wopicontext_test.go b/services/collaboration/pkg/middleware/wopicontext_test.go
index af8f6e49a6a..0a8c1c661df 100644
--- a/services/collaboration/pkg/middleware/wopicontext_test.go
+++ b/services/collaboration/pkg/middleware/wopicontext_test.go
@@ -47,7 +47,7 @@ var _ = Describe("Wopi Context Middleware", func() {
 		next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			w.WriteHeader(http.StatusOK)
 		})
-		mw = middleware.WopiContextAuthMiddleware(cfg, next)
+		mw = middleware.WopiContextAuthMiddleware(cfg, nil, next)
 
 		tknMngr, err = rjwt.New(map[string]interface{}{
 			"secret":  cfg.TokenManager.JWTSecret,
@@ -110,7 +110,7 @@ var _ = Describe("Wopi Context Middleware", func() {
 				Path: ".",
 			},
 		}
-		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
+		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg, nil)
 		q := req.URL.Query()
 		q.Add("access_token", wopiToken)
 		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
@@ -132,7 +132,7 @@ var _ = Describe("Wopi Context Middleware", func() {
 		// use wrong wopi secret when generating the wopi token
 		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, &config.Config{Wopi: config.Wopi{
 			Secret: "wrongSecret",
-		}})
+		}}, nil)
 		q := req.URL.Query()
 		q.Add("access_token", wopiToken)
 		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
@@ -155,7 +155,7 @@ var _ = Describe("Wopi Context Middleware", func() {
 				Path:       ".",
 			},
 		}
-		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
+		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg, nil)
 		q := req.URL.Query()
 		q.Add("access_token", wopiToken)
 		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
@@ -185,7 +185,7 @@ var _ = Describe("Wopi Context Middleware", func() {
 				},
 			},
 		}
-		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
+		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg, nil)
 		q := req.URL.Query()
 		q.Add("access_token", wopiToken)
 		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
@@ -219,7 +219,7 @@ var _ = Describe("Wopi Context Middleware", func() {
 				},
 			},
 		}
-		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
+		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg, nil)
 		q := req.URL.Query()
 		q.Add("access_token", wopiToken)
 		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
@@ -250,7 +250,7 @@ var _ = Describe("Wopi Context Middleware", func() {
 				Path: ".",
 			},
 		}
-		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
+		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg, nil)
 		q := req.URL.Query()
 		q.Add("access_token", wopiToken)
 		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
@@ -277,7 +277,7 @@ var _ = Describe("Wopi Context Middleware", func() {
 				Path:       ".",
 			},
 		}
-		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
+		wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg, nil)
 		q := req.URL.Query()
 		q.Add("access_token", wopiToken)
 		q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))