mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
fix: make HTTP start JWT secret loading idempotent (#2165)
* fix: make HTTP start JWT secret loading idempotent * fix: check unlock password before loading jwt * fix: use mutex to guard jwtsecret in config
This commit is contained in:
parent
9e93cff8ea
commit
51b867e992
6 changed files with 110 additions and 75 deletions
|
|
@ -19,11 +19,12 @@ import (
|
|||
)
|
||||
|
||||
type config struct {
|
||||
Env *AppConfig
|
||||
db *gorm.DB
|
||||
cache map[string]map[string]string // key -> encryptionKeyHash -> value
|
||||
cacheMutex sync.Mutex
|
||||
jwtSecret string
|
||||
Env *AppConfig
|
||||
db *gorm.DB
|
||||
cache map[string]map[string]string // key -> encryptionKeyHash -> value
|
||||
cacheMutex sync.Mutex
|
||||
jwtSecret string
|
||||
jwtSecretMutex sync.Mutex
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
@ -127,18 +128,30 @@ func (cfg *config) SetupCompleted() (bool, error) {
|
|||
}
|
||||
|
||||
func (cfg *config) GetJWTSecret() (string, error) {
|
||||
if cfg.jwtSecret == "" {
|
||||
cfg.jwtSecretMutex.Lock()
|
||||
jwtSecret := cfg.jwtSecret
|
||||
cfg.jwtSecretMutex.Unlock()
|
||||
|
||||
if jwtSecret == "" {
|
||||
return "", errors.New("config not unlocked")
|
||||
}
|
||||
|
||||
return cfg.jwtSecret, nil
|
||||
return jwtSecret, nil
|
||||
}
|
||||
|
||||
func (cfg *config) Unlock(encryptionKey string) error {
|
||||
// Decrypt and store the JWT secret in memory
|
||||
func (cfg *config) LoadJWTSecret(encryptionKey string) error {
|
||||
if !cfg.CheckUnlockPassword(encryptionKey) {
|
||||
return errors.New("incorrect password")
|
||||
}
|
||||
|
||||
cfg.jwtSecretMutex.Lock()
|
||||
if cfg.jwtSecret != "" {
|
||||
cfg.jwtSecretMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
cfg.jwtSecretMutex.Unlock()
|
||||
|
||||
// TODO: remove encryptedJwtSecret check after 2027-01-01
|
||||
// - all hubs should have updated to use an encrypted JWT secret by then
|
||||
encryptedJwtSecret, err := cfg.Get("JWTSecret", "")
|
||||
|
|
@ -165,7 +178,9 @@ func (cfg *config) Unlock(encryptionKey string) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
cfg.jwtSecretMutex.Lock()
|
||||
cfg.jwtSecret = jwtSecret
|
||||
cfg.jwtSecretMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -350,7 +365,9 @@ func (cfg *config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockP
|
|||
}
|
||||
|
||||
// JWT secret will be set on config unlock (required after password change)
|
||||
cfg.jwtSecretMutex.Lock()
|
||||
cfg.jwtSecret = ""
|
||||
cfg.jwtSecretMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,10 +74,10 @@ func (c *AppConfig) GetBaseFrontendUrl() string {
|
|||
}
|
||||
|
||||
type Config interface {
|
||||
Unlock(encryptionKey string) error
|
||||
Get(key string, encryptionKey string) (string, error)
|
||||
SetIgnore(key string, value string, encryptionKey string) error
|
||||
SetUpdate(key string, value string, encryptionKey string) error
|
||||
LoadJWTSecret(encryptionKey string) error
|
||||
GetJWTSecret() (string, error)
|
||||
GetRelayUrls() []string
|
||||
GetNetwork() string
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ func TestSetUpdate_EncryptionKeyToNoEncryptionKey(t *testing.T) {
|
|||
assert.Equal(t, "value2", updatedValue)
|
||||
}
|
||||
|
||||
func TestJWTSecret_GeneratedOnUnlock(t *testing.T) {
|
||||
func TestJWTSecret_GeneratedOnLoad(t *testing.T) {
|
||||
svc, err := tests.CreateTestService(t)
|
||||
require.NoError(t, err)
|
||||
defer svc.Remove()
|
||||
|
|
@ -222,7 +222,7 @@ func TestJWTSecret_GeneratedOnUnlock(t *testing.T) {
|
|||
err = cfg.ChangeUnlockPassword("", "123")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cfg.Unlock("123")
|
||||
err = cfg.LoadJWTSecret("123")
|
||||
require.NoError(t, err)
|
||||
|
||||
jwtSecret, err := cfg.GetJWTSecret()
|
||||
|
|
@ -235,14 +235,35 @@ func TestJWTSecret_GeneratedOnUnlock(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.NotEqual(t, encryptedSecret, decryptedSecret)
|
||||
|
||||
// unlock again without doing anything, ensure the same JWT secret is returned
|
||||
err = cfg.Unlock("123")
|
||||
// load again without doing anything, ensure the same JWT secret is returned
|
||||
err = cfg.LoadJWTSecret("123")
|
||||
require.NoError(t, err)
|
||||
jwtSecret2, err := cfg.GetJWTSecret()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, jwtSecret, jwtSecret2)
|
||||
}
|
||||
|
||||
func TestJWTSecret_WrongPassword(t *testing.T) {
|
||||
svc, err := tests.CreateTestService(t)
|
||||
require.NoError(t, err)
|
||||
defer svc.Remove()
|
||||
|
||||
cfg, err := config.NewConfig(&config.AppConfig{}, svc.DB)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cfg.ChangeUnlockPassword("", "123")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cfg.SaveUnlockPasswordCheck("123")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cfg.LoadJWTSecret("123")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cfg.LoadJWTSecret("wrong")
|
||||
require.ErrorContains(t, err, "incorrect password")
|
||||
}
|
||||
|
||||
func TestJWTSecret_ChangePassword(t *testing.T) {
|
||||
svc, err := tests.CreateTestService(t)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -254,7 +275,7 @@ func TestJWTSecret_ChangePassword(t *testing.T) {
|
|||
err = cfg.ChangeUnlockPassword("", "123")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cfg.Unlock("123")
|
||||
err = cfg.LoadJWTSecret("123")
|
||||
require.NoError(t, err)
|
||||
|
||||
jwtSecret, err := cfg.GetJWTSecret()
|
||||
|
|
@ -267,7 +288,7 @@ func TestJWTSecret_ChangePassword(t *testing.T) {
|
|||
newJwtSecret, err := cfg.GetJWTSecret()
|
||||
require.ErrorContains(t, err, "unlock")
|
||||
|
||||
err = cfg.Unlock("1234")
|
||||
err = cfg.LoadJWTSecret("1234")
|
||||
require.NoError(t, err)
|
||||
|
||||
// a new JWT secret must be generated after password change
|
||||
|
|
@ -277,7 +298,7 @@ func TestJWTSecret_ChangePassword(t *testing.T) {
|
|||
assert.NotEqual(t, newJwtSecret, jwtSecret)
|
||||
}
|
||||
|
||||
func TestJWTSecret_ReplaceUnencryptedSecretOnUnlock(t *testing.T) {
|
||||
func TestJWTSecret_ReplaceUnencryptedSecretOnLoad(t *testing.T) {
|
||||
svc, err := tests.CreateTestService(t)
|
||||
require.NoError(t, err)
|
||||
defer svc.Remove()
|
||||
|
|
@ -293,7 +314,7 @@ func TestJWTSecret_ReplaceUnencryptedSecretOnUnlock(t *testing.T) {
|
|||
err = svc.Cfg.SetUpdate("JWTSecret", oldJwtSecret, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cfg.Unlock("123")
|
||||
err = cfg.LoadJWTSecret("123")
|
||||
require.NoError(t, err)
|
||||
|
||||
jwtSecret, err := cfg.GetJWTSecret()
|
||||
|
|
|
|||
|
|
@ -294,14 +294,11 @@ func (httpSvc *HttpService) startHandler(c echo.Context) error {
|
|||
})
|
||||
}
|
||||
|
||||
// NOTE: the config is also unlocked as part of the start
|
||||
// goroutine below. But since we execute start asynchronously it's hard to
|
||||
// know when the config has been unlocked before being able to create the JWT token
|
||||
err := httpSvc.cfg.Unlock(startRequest.UnlockPassword)
|
||||
err := httpSvc.cfg.LoadJWTSecret(startRequest.UnlockPassword)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Message: fmt.Sprintf("Failed to unlock config: %s", err.Error()),
|
||||
Message: fmt.Sprintf("Failed to load JWT secret: %s", err.Error()),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -276,9 +276,9 @@ func (svc *service) StartApp(encryptionKey string) error {
|
|||
return errors.New("invalid password")
|
||||
}
|
||||
|
||||
err = svc.cfg.Unlock(encryptionKey)
|
||||
err = svc.cfg.LoadJWTSecret(encryptionKey)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to unlock config")
|
||||
logger.Logger.WithError(err).Error("Failed to load JWT secret")
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -531,6 +531,57 @@ func (_c *MockConfig_GetRelayUrls_Call) RunAndReturn(run func() []string) *MockC
|
|||
return _c
|
||||
}
|
||||
|
||||
// LoadJWTSecret provides a mock function for the type MockConfig
|
||||
func (_mock *MockConfig) LoadJWTSecret(encryptionKey string) error {
|
||||
ret := _mock.Called(encryptionKey)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for LoadJWTSecret")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = returnFunc(encryptionKey)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConfig_LoadJWTSecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadJWTSecret'
|
||||
type MockConfig_LoadJWTSecret_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// LoadJWTSecret is a helper method to define mock.On call
|
||||
// - encryptionKey string
|
||||
func (_e *MockConfig_Expecter) LoadJWTSecret(encryptionKey interface{}) *MockConfig_LoadJWTSecret_Call {
|
||||
return &MockConfig_LoadJWTSecret_Call{Call: _e.mock.On("LoadJWTSecret", encryptionKey)}
|
||||
}
|
||||
|
||||
func (_c *MockConfig_LoadJWTSecret_Call) Run(run func(encryptionKey string)) *MockConfig_LoadJWTSecret_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 string
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConfig_LoadJWTSecret_Call) Return(err error) *MockConfig_LoadJWTSecret_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConfig_LoadJWTSecret_Call) RunAndReturn(run func(encryptionKey string) error) *MockConfig_LoadJWTSecret_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SaveUnlockPasswordCheck provides a mock function for the type MockConfig
|
||||
func (_mock *MockConfig) SaveUnlockPasswordCheck(encryptionKey string) error {
|
||||
ret := _mock.Called(encryptionKey)
|
||||
|
|
@ -913,54 +964,3 @@ func (_c *MockConfig_SetupCompleted_Call) RunAndReturn(run func() (bool, error))
|
|||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Unlock provides a mock function for the type MockConfig
|
||||
func (_mock *MockConfig) Unlock(encryptionKey string) error {
|
||||
ret := _mock.Called(encryptionKey)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Unlock")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = returnFunc(encryptionKey)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConfig_Unlock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unlock'
|
||||
type MockConfig_Unlock_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Unlock is a helper method to define mock.On call
|
||||
// - encryptionKey string
|
||||
func (_e *MockConfig_Expecter) Unlock(encryptionKey interface{}) *MockConfig_Unlock_Call {
|
||||
return &MockConfig_Unlock_Call{Call: _e.mock.On("Unlock", encryptionKey)}
|
||||
}
|
||||
|
||||
func (_c *MockConfig_Unlock_Call) Run(run func(encryptionKey string)) *MockConfig_Unlock_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 string
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConfig_Unlock_Call) Return(err error) *MockConfig_Unlock_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConfig_Unlock_Call) RunAndReturn(run func(encryptionKey string) error) *MockConfig_Unlock_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue