feat: use short tokens as access tokens

The "real" access token will be stored using the short token as key.
This short token will be sent to the clients to be used as access token
for the WOPI server.

This is configurable, and requires a store in order to keep the tokens.
This commit is contained in:
Juan Pablo Villafáñez
2024-10-22 18:44:04 +02:00
parent 4dfba210e1
commit b8f8ca813e
14 changed files with 127 additions and 15 deletions

View File

@@ -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")

View File

@@ -13,6 +13,7 @@ type Config struct {
Service Service `yaml:"-"`
App App `yaml:"app"`
Store Store `yaml:"store"`
TokenManager *TokenManager `yaml:"token_manager"`

View File

@@ -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",

View File

@@ -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:"pre5.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:"pre5.0"`
Database string `yaml:"database" env:"COLLABORATION_STORE_DATABASE" desc:"The database name the configured store should use." introductionVersion:"pre5.0"`
Table string `yaml:"table" env:"COLLABORATION_STORE_TABLE" desc:"The database table the store should use." introductionVersion:"pre5.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:"pre5.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:"5.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:"5.0"`
}

View File

@@ -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 Online, which has URL length restrictions." introductionVersion:"7.0.0"`
}

View File

@@ -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

View File

@@ -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),
),
}

View File

@@ -2,10 +2,13 @@ package middleware
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/http"
"strings"
"time"
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
@@ -15,6 +18,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 +48,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 +71,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 +184,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 +212,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 := sha256.New()
c.Write([]byte(accessToken))
shortAccessToken := hex.EncodeToString(c.Sum(nil))
errWrite := st.Write(&microstore.Record{
Key: shortAccessToken,
Value: []byte(accessToken),
Expiry: claims.ExpiresAt.Sub(time.Now()),
})
return shortAccessToken, claims.ExpiresAt.UnixMilli(), errWrite
}
return accessToken, claims.ExpiresAt.UnixMilli(), err
}

View File

@@ -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 funtion to set the Store option
func Store(val microstore.Store) Option {
return func(o *Options) {
o.Store = val
}
}

View File

@@ -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().

View File

@@ -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 funtion to set the Store option
func Store(val microstore.Store) Option {
return func(o *Options) {
o.Store = val
}
}

View File

@@ -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,
)

View File

@@ -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
}
}

View File

@@ -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{