mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-10-08 20:03:05 -04:00
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:
1 parent
1df2eb21d4
commit
10b93edded
9 files changed
+58
-47
No files matched your search
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user