Skip to content
4 changes: 2 additions & 2 deletions opencloud/pkg/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
return fmt.Errorf("could not generate random secret for urlSigningSecret: %s", err)
}
}
authGuestJWTSecret = oldCfg.AuthGuest.TokenManager.JWTSecret
authGuestJWTSecret = oldCfg.AuthGuest.JWT.Secret
if authGuestJWTSecret == "" {
authGuestJWTSecret, err = generators.GenerateRandomPassword(passwordLength)
if err != nil {
Expand Down Expand Up @@ -226,7 +226,7 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
},
AuthGuest: AuthGuest{
ServiceAccount: serviceAccount,
TokenManager: TokenManager{JWTSecret: authGuestJWTSecret},
JWT: AuthGuestJWT{Secret: authGuestJWTSecret},
},
Users: UsersAndGroupsService{
Drivers: LdapBasedService{
Expand Down
7 changes: 6 additions & 1 deletion opencloud/pkg/init/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ type Activitylog struct {
// AuthGuest is the configuration for the auth-guest service
type AuthGuest struct {
ServiceAccount ServiceAccount `yaml:"service_account"`
TokenManager TokenManager `yaml:"token_manager"`
JWT AuthGuestJWT `yaml:"jwt"`
}

// AuthGuestJWT is the configuration for the guest session tokens
type AuthGuestJWT struct {
Secret string `yaml:"secret"`
}

// App is the configuration for the collaboration service
Expand Down
5 changes: 3 additions & 2 deletions services/auth-guest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ sequenceDiagram
Redeem->>+Reva: Get Share
Reva->>-Redeem: Share
Note right of Redeem: Validate Share, Mark Token used
Redeem->>-Web: Set Cookie, return shareid
Redeem->>-Web: Set Cookie, return shareid
Note right of Web: HTTP only cookie with signed JWT (JWT lifetime 24h)
Web->>+Proxy: "/graph/me/drives/sharedWithMe"
Proxy->>+Reva: validate token extracted from JWT
Expand Down Expand Up @@ -91,7 +91,8 @@ the event consumer, set `AUTH_GUEST_HTTP_DISABLED=true`.

Relevant options:

- `AUTH_GUEST_JWT_SECRET` — secret used to sign session tokens.
- `AUTH_GUEST_SESSION_JWT_SECRET` — secret used to sign guest session tokens.
It must differ from `OC_JWT_SECRET`.
- `AUTH_GUEST_JWT_COOKIE_NAME`, `AUTH_GUEST_JWT_TTL` — session cookie name and
lifetime.
- `AUTH_GUEST_TOKENS_STORAGE_ROOT` — where guest link token records are stored.
Expand Down
21 changes: 19 additions & 2 deletions services/auth-guest/pkg/command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import (
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config/parser"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/metrics"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/revaconfig"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/server/debug"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/server/http"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest"
svcEvents "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/events"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/jwt"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/storage"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token"
"github.com/opencloud-eu/reva/v2/cmd/revad/runtime"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
Expand Down Expand Up @@ -77,7 +79,7 @@ func Server(cfg *config.Config) *cobra.Command {

tokenSvc := token.NewTokenService()
store := storage.NewFileManager(cfg.Storage.RootDirectory)
jwtService := jwt.NewJwtService(cfg.TokenManager.JWTSecret, cfg.JWT.TTL)
jwtService := jwt.NewJwtService(cfg.JWT.Secret, cfg.JWT.TTL)

authGuest := authguest.NewAuthGuestService(tokenSvc, store,
authguest.GatewaySelector(gatewaySelector),
Expand Down Expand Up @@ -143,7 +145,18 @@ func Server(cfg *config.Config) *cobra.Command {
} else {
logger.Info().Msg("event listening disabled, not starting event service")
}

{
//FIXME: Does this need to be optional? Similar to cfg.HTTP.Disabled?
// run the appropriate reva servers based on the config
rCfg := revaconfig.GuestLinksConfigFromStruct(cfg)
if rServer := runtime.NewDrivenGRPCServerWithOptions(rCfg,
runtime.WithLogger(&logger.Logger),
runtime.WithRegistry(registry.GetRegistry()),
runtime.WithTraceProvider(tracerProvider),
); rServer != nil {
gr.Add(runner.NewRevaServiceRunner(cfg.Service.Name+".rgrpc", rServer))
}
}
{
debugServer, err := debug.Server(
debug.Logger(logger),
Expand All @@ -157,6 +170,10 @@ func Server(cfg *config.Config) *cobra.Command {

gr.Add(runner.NewGolangHttpServerRunner(cfg.Service.Name+".debug", debugServer))
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, logger, grpcSvc, cfg.Debug.Addr); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}

grResults := gr.Run(ctx)

Expand Down
12 changes: 11 additions & 1 deletion services/auth-guest/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Config struct {
RevaGateway string `yaml:"reva_gateway" env:"OC_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" introductionVersion:"%%NEXT%%"`
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`

GRPC GRPCConfig `yaml:"grpc"`
HTTP HTTP `yaml:"http"`
Storage Storage `yaml:"storage"`
TokenManager *TokenManager `yaml:"token_manager"`
Expand Down Expand Up @@ -73,18 +74,27 @@ type HTTP struct {
TLS shared.HTTPServiceTLS `yaml:"tls"`
}

// GRPCConfig defines the GRPC configuration
type GRPCConfig struct {
Addr string `yaml:"addr" env:"AUTH_GUEST_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"%%NEXT%%"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"OC_GRPC_PROTOCOL;AUTH_GUEST_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"%%NEXT%%"`
}

// Storage defines the configuration for the token storage.
type Storage struct {
RootDirectory string `yaml:"root_directory" env:"AUTH_GUEST_TOKENS_STORAGE_ROOT" desc:"The directory where the guest share tokens are stored. If not defined, the root directory derives from $OC_BASE_DATA_PATH/auth-guest." introductionVersion:"%%NEXT%%"`
}

// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string `yaml:"jwt_secret" env:"AUTH_GUEST_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"%%NEXT%%"`
JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;AUTH_GUEST_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"%%NEXT%%"`
}

// JWT defines the configuration for guest session tokens.
type JWT struct {
Secret string `yaml:"secret" env:"AUTH_GUEST_SESSION_JWT_SECRET" desc:"The secret used to sign and validate guest session tokens. It must differ from OC_JWT_SECRET." introductionVersion:"%%NEXT%%" mask:"password"`
CookieName string `yaml:"cookie_name" env:"AUTH_GUEST_JWT_COOKIE_NAME" desc:"The name of the session cookie set when a guest token is redeemed." introductionVersion:"%%NEXT%%"`
TTL time.Duration `yaml:"ttl" env:"AUTH_GUEST_JWT_TTL" desc:"The lifetime of a redeemed guest session token." introductionVersion:"%%NEXT%%"`
}
15 changes: 14 additions & 1 deletion services/auth-guest/pkg/config/defaults/defaultconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ func DefaultConfig() *config.Config {
EnableTLS: false,
},
RevaGateway: shared.DefaultRevaConfig().Address,
GRPC: config.GRPCConfig{
Addr: "127.0.0.1:9268",
Namespace: "eu.opencloud.api",
Protocol: "tcp",
},
HTTP: config.HTTP{
Addr: "127.0.0.1:9266",
Root: "/graph",
Expand Down Expand Up @@ -70,7 +75,15 @@ func EnsureDefaults(cfg *config.Config) {
cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
}

if cfg.TokenManager == nil {
if cfg.GRPC.TLS == nil && cfg.Commons != nil {
cfg.GRPC.TLS = structs.CopyOrZeroValue(cfg.Commons.GRPCServiceTLS)
}

if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil {
cfg.TokenManager = &config.TokenManager{
JWTSecret: cfg.Commons.TokenManager.JWTSecret,
}
} else if cfg.TokenManager == nil {
cfg.TokenManager = &config.TokenManager{}
}

Expand Down
14 changes: 14 additions & 0 deletions services/auth-guest/pkg/config/parser/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ package parser

import (
"errors"
"fmt"

occfg "github.com/opencloud-eu/opencloud/pkg/config"
ocdefaults "github.com/opencloud-eu/opencloud/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config/defaults"
Expand Down Expand Up @@ -41,5 +43,17 @@ func Validate(cfg *config.Config) error {
if cfg.TokenManager == nil || cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
if cfg.JWT.Secret == "" {
return fmt.Errorf("the guest session secret has not been set properly in your config for %s. "+
"Make sure your %s config contains the proper values "+
"(e.g. by using 'opencloud init --diff' and applying the patch or setting a value manually in "+
"the config/corresponding environment variable AUTH_GUEST_SESSION_JWT_SECRET)",
cfg.Service.Name, ocdefaults.BaseConfigPath())
}
// The guest session token and the reva access token are both HS256 JWTs. Signing them
// with the same key would make them interchangeable.
if cfg.JWT.Secret == cfg.TokenManager.JWTSecret {
return fmt.Errorf("the guest session secret (AUTH_GUEST_SESSION_JWT_SECRET) of %s must differ from the jwt secret (OC_JWT_SECRET)", cfg.Service.Name)
}
return nil
}
38 changes: 38 additions & 0 deletions services/auth-guest/pkg/config/parser/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
// SPDX-License-Identifier: Apache-2.0

package parser

import (
"testing"

"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config"
)

func TestValidate(t *testing.T) {
tests := []struct {
name string
jwtSecret string
sessionSecret string
wantErr bool
}{
{name: "distinct secrets", jwtSecret: "reva-secret", sessionSecret: "session-secret"},
{name: "missing jwt secret", sessionSecret: "session-secret", wantErr: true},
{name: "missing session secret", jwtSecret: "reva-secret", wantErr: true},
{name: "shared secret", jwtSecret: "same-secret", sessionSecret: "same-secret", wantErr: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &config.Config{
TokenManager: &config.TokenManager{JWTSecret: tt.jwtSecret},
JWT: config.JWT{Secret: tt.sessionSecret},
}

err := Validate(cfg)
if (err != nil) != tt.wantErr {
t.Fatalf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
46 changes: 46 additions & 0 deletions services/auth-guest/pkg/revaconfig/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package revaconfig

import (
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config"
)

// GuestLinksConfigFromStruct will adapt an OpenCloud config struct into a reva mapstructure to start a reva service.
func GuestLinksConfigFromStruct(cfg *config.Config) map[string]any {
rcfg := map[string]any{
"shared": map[string]any{
"jwt_secret": cfg.TokenManager.JWTSecret,
"gatewaysvc": cfg.RevaGateway,
"grpc_client_options": cfg.GRPCClientTLS,
"multi_tenant_enabled": cfg.Commons.MultiTenantEnabled,
},
"grpc": map[string]any{
"network": cfg.GRPC.Protocol,
"address": cfg.GRPC.Addr,
"tls_settings": map[string]any{
"enabled": cfg.GRPC.TLS.Enabled,
"certificate": cfg.GRPC.TLS.Cert,
"key": cfg.GRPC.TLS.Key,
},
"services": map[string]any{
"authprovider": map[string]any{
"auth_manager": "guestlinks",
"auth_managers": map[string]any{
"guestlinks": map[string]any{
"gateway_addr": cfg.RevaGateway,
"jwt_secret": cfg.JWT.Secret,
"service_account_id": cfg.ServiceAccount.ServiceAccountID,
"service_account_secret": cfg.ServiceAccount.ServiceAccountSecret,
},
},
},
},
"interceptors": map[string]any{
"prometheus": map[string]any{
"namespace": "opencloud",
"subsystem": "auth_guest",
},
},
},
}
return rcfg
}
5 changes: 0 additions & 5 deletions services/auth-guest/pkg/server/http/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"net/http"

"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/storage"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token"
)

Expand Down Expand Up @@ -39,10 +38,6 @@ func writeRedeemError(w http.ResponseWriter, err error) {
status, errorType = http.StatusUnauthorized, "tokenExpired"
case errors.Is(re.ErrorType, token.ErrInvalidToken):
status, errorType = http.StatusUnauthorized, "tokenInvalid"
case errors.Is(re.ErrorType, storage.ErrNotFound):
status, errorType = http.StatusNotFound, "tokenNotFound"
case errors.Is(re.ErrorType, storage.ErrInvalidHash):
status, errorType = http.StatusUnauthorized, "tokenInvalid"
case errors.Is(re.ErrorType, authguest.ErrAlreadyRedeemed):
status, errorType = http.StatusConflict, "tokenAlreadyRedeemed"
case errors.Is(re.ErrorType, authguest.ErrShareNotFound):
Expand Down
6 changes: 6 additions & 0 deletions services/auth-guest/pkg/server/http/redeem.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import (
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest"
)

// maxRedeemBodySize limits the body of the unauthenticated redeem request. A
// token is about 90 bytes, so this leaves plenty of room for the JSON wrapping.
const maxRedeemBodySize = 4 << 10

// RedeemRequest is the request body for token redemption.
type RedeemRequest struct {
Token string `json:"token"`
Expand All @@ -24,6 +28,8 @@ type redeemResponse struct {
// RedeemHandler validates the token submitted to the redeem endpoint.
func RedeemHandler(log log.Logger, s authguest.AuthGuest, cfg *config.Config) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRedeemBodySize)

var req RedeemRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Debug().Err(err).Msg("request body is malformed")
Expand Down
18 changes: 11 additions & 7 deletions services/auth-guest/pkg/server/http/redeem_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest/mocks"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/storage"
"github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
Expand Down Expand Up @@ -83,12 +82,6 @@ func TestRedeemHandlerErrorMapping(t *testing.T) {
wantStatus: http.StatusUnauthorized,
wantType: "tokenInvalid",
},
{
name: "token not found",
err: &authguest.RedeemError{ErrorType: storage.ErrNotFound},
wantStatus: http.StatusNotFound,
wantType: "tokenNotFound",
},
{
name: "token already redeemed",
err: &authguest.RedeemError{ErrorType: authguest.ErrAlreadyRedeemed},
Expand Down Expand Up @@ -130,6 +123,17 @@ func TestRedeemHandlerErrorMapping(t *testing.T) {
}
}

func TestRedeemHandlerBodyTooLarge(t *testing.T) {
svcMock := mocks.NewAuthGuest(t)

body := `{"token":"` + strings.Repeat("a", maxRedeemBodySize) + `"}`
rr := httptest.NewRecorder()
newRedeemHandler(t, svcMock)(rr, httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)))

assert.Equal(t, http.StatusBadRequest, rr.Code)
svcMock.AssertNotCalled(t, "Redeem", mock.Anything, mock.Anything)
}

func TestRedeemHandlerMalformedBody(t *testing.T) {
svcMock := mocks.NewAuthGuest(t)

Expand Down
15 changes: 11 additions & 4 deletions services/auth-guest/pkg/service/authguest/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,26 @@ func (s *AuthGuestService) CleanupShare(shareID string) error {
}

// VerifyToken validates a token and returns its stored record.
//
// Until the secret has been verified, all failures are reported as
// token.ErrInvalidToken without a share id, so that a caller holding only part
// of a token learns neither the share id nor whether a record exists.
func (s *AuthGuestService) verifyToken(tokenString string) (*storage.Record, error) {
tok, err := s.tokenSvc.Parse(tokenString)
if err != nil {
return nil, &RedeemError{ErrorType: err}
return nil, &RedeemError{ErrorType: token.ErrInvalidToken}
}

rec, err := s.store.Get(tok.ShareIDHash)
if err != nil {
return nil, &RedeemError{ErrorType: err}
switch {
case errors.Is(err, storage.ErrNotFound), errors.Is(err, storage.ErrInvalidHash):
return nil, &RedeemError{ErrorType: token.ErrInvalidToken}
case err != nil:
return nil, err
}

if err := s.tokenSvc.Verify(*tok, rec.SecretHash); err != nil {
return nil, &RedeemError{ErrorType: err, ShareID: rec.ShareID}
return nil, &RedeemError{ErrorType: token.ErrInvalidToken}
}

if !rec.Expiry.IsZero() && rec.Expiry.Before(time.Now()) {
Expand Down
Loading