Derive the guest session key from OC_JWT_SECRET

Replace AUTH_GUEST_SESSION_JWT_SECRET with a key derived via
HMAC-SHA256 from the reva JWT secret. Guest session tokens and reva
access tokens remain unforgeable across each other without a second
secret to configure.
This commit is contained in:
André Duffeck committed 2026-10-07 08:46:53 +02:00
1 parent 1df2eb21d4
commit 10b93edded
9 files changed
+58 -47

No files matched your search

-13
View File
@@ -69,7 +69,6 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
idmServicePassword, idpServicePassword, ocAdminServicePassword, revaServicePassword string
tokenManagerJwtSecret, collaborationWOPISecret, machineAuthAPIKey, systemUserAPIKey string
revaTransferSecret, thumbnailsTransferSecret, serviceAccountSecret, urlSigningSecret string
authGuestJWTSecret string
adminPasswdwordGenerated bool
)
@@ -104,13 +103,6 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
return fmt.Errorf("could not generate random secret for urlSigningSecret: %s", err)
}
}
authGuestJWTSecret = oldCfg.AuthGuest.JWT.Secret
if authGuestJWTSecret == "" {
authGuestJWTSecret, err = generators.GenerateRandomPassword(passwordLength)
if err != nil {
return fmt.Errorf("could not generate random secret for authGuestJWTSecret: %s", err)
}
}
} else {
systemUserID = uuid.NewString()
adminUserID = uuid.NewString()
@@ -163,10 +155,6 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
if err != nil {
return fmt.Errorf("could not generate random secret for urlSigningSecret: %s", err)
}
authGuestJWTSecret, err = generators.GenerateRandomPassword(passwordLength)
if err != nil {
return fmt.Errorf("could not generate random secret for authGuestJWTSecret: %s", err)
}
thumbnailsTransferSecret, err = generators.GenerateRandomPassword(passwordLength)
if err != nil {
return fmt.Errorf("could not generate random password for thumbnailsTransferSecret: %s", err)
@@ -226,7 +214,6 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
},
AuthGuest: AuthGuest{
ServiceAccount: serviceAccount,
JWT: AuthGuestJWT{Secret: authGuestJWTSecret},
},
Users: UsersAndGroupsService{
Drivers: LdapBasedService{
-6
View File
@@ -57,12 +57,6 @@ type Activitylog struct {
// AuthGuest is the configuration for the auth-guest service
type AuthGuest struct {
ServiceAccount ServiceAccount `yaml:"service_account"`
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
+3 -2
View File
@@ -91,10 +91,11 @@ the event consumer, set `AUTH_GUEST_HTTP_DISABLED=true`.
Relevant options:
- `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.
- Guest session tokens are signed with a key derived from `OC_JWT_SECRET`. There
is no separate secret to configure; rotating `OC_JWT_SECRET` invalidates all
guest sessions.
- `AUTH_GUEST_TOKENS_STORAGE_ROOT` — where guest link token records are stored.
- `AUTH_GUEST_SERVICE_ACCOUNT_ID`, `AUTH_GUEST_SERVICE_ACCOUNT_SECRET` — service
account used to query the gateway for share metadata.
+1 -1
View File
@@ -79,7 +79,7 @@ func Server(cfg *config.Config) *cobra.Command {
tokenSvc := token.NewTokenService()
store := storage.NewFileManager(cfg.Storage.RootDirectory)
jwtService := jwt.NewJwtService(cfg.JWT.Secret, cfg.JWT.TTL)
jwtService := jwt.NewJwtService(cfg.SessionSecret(), cfg.JWT.TTL)
authGuest := authguest.NewAuthGuestService(tokenSvc, store,
authguest.GatewaySelector(gatewaySelector),
+23 -1
View File
@@ -5,6 +5,9 @@ package config
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"time"
"github.com/opencloud-eu/opencloud/pkg/shared"
@@ -94,7 +97,26 @@ type TokenManager struct {
// 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%%"`
}
// sessionSecretLabel binds the derived guest session key to its purpose. Changing it
// invalidates all existing guest sessions.
const sessionSecretLabel = "opencloud auth-guest session jwt v1"
// SessionSecret returns the key used to sign and validate guest session tokens.
// The guest session token and the reva access token are both HS256 JWTs, so they must
// not share a key, otherwise they would be interchangeable. The key is derived from the
// reva JWT secret to keep them apart without requiring an additional secret.
//
// An empty reva secret yields an empty session key rather than a key anyone could
// compute from the label alone. The guestlinks auth manager refuses an empty key.
func (c *Config) SessionSecret() string {
if c.TokenManager == nil || c.TokenManager.JWTSecret == "" {
return ""
}
mac := hmac.New(sha256.New, []byte(c.TokenManager.JWTSecret))
mac.Write([]byte(sessionSecretLabel))
return hex.EncodeToString(mac.Sum(nil))
}
@@ -0,0 +1,25 @@
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
// SPDX-License-Identifier: Apache-2.0
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSessionSecret(t *testing.T) {
cfg := &Config{TokenManager: &TokenManager{JWTSecret: "reva-secret"}}
other := &Config{TokenManager: &TokenManager{JWTSecret: "other-secret"}}
assert.Len(t, cfg.SessionSecret(), 64)
assert.Equal(t, cfg.SessionSecret(), cfg.SessionSecret(), "derivation must be deterministic")
assert.NotEqual(t, cfg.TokenManager.JWTSecret, cfg.SessionSecret())
assert.NotEqual(t, cfg.SessionSecret(), other.SessionSecret())
}
func TestSessionSecretMissingJWTSecret(t *testing.T) {
assert.Empty(t, (&Config{}).SessionSecret(), "nil token manager")
assert.Empty(t, (&Config{TokenManager: &TokenManager{}}).SessionSecret(), "empty jwt secret")
}
@@ -5,10 +5,8 @@ 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"
@@ -43,17 +41,5 @@ 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
}
@@ -11,22 +11,18 @@ import (
func TestValidate(t *testing.T) {
tests := []struct {
name string
jwtSecret string
sessionSecret string
wantErr bool
name string
jwtSecret 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},
{name: "jwt secret set", jwtSecret: "reva-secret"},
{name: "missing jwt 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)
+1 -1
View File
@@ -27,7 +27,7 @@ func GuestLinksConfigFromStruct(cfg *config.Config) map[string]any {
"auth_managers": map[string]any{
"guestlinks": map[string]any{
"gateway_addr": cfg.RevaGateway,
"jwt_secret": cfg.JWT.Secret,
"jwt_secret": cfg.SessionSecret(),
"service_account_id": cfg.ServiceAccount.ServiceAccountID,
"service_account_secret": cfg.ServiceAccount.ServiceAccountSecret,
},