feat(auth): store the token signing secret in the metadata layer
- app_settings table (PostgreSQL migration 000010, SQLite 000002) - internal/settings repository (Get/Set/Ensure) wired into store.Repos - secret generated with crypto/rand on first start and persisted; reused on restart - drop required JWT_SECRET from env/config; app.New takes a TokenManager and storage.Open takes the resolved secret (URL_SECRET still overrides) - docs updated
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/db"
|
||||
"shared-album/internal/photos"
|
||||
"shared-album/internal/settings"
|
||||
"shared-album/internal/sse"
|
||||
"shared-album/internal/storage"
|
||||
"shared-album/internal/store"
|
||||
@@ -137,6 +138,53 @@ func TestSQLiteEndToEndWithRestart(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestJWTSecretPersistsAcrossRestart(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
path := filepath.Join(t.TempDir(), "galo.db")
|
||||
migFS, err := fs.Sub(assets.Migrations, "migrations/sqlite")
|
||||
if err != nil {
|
||||
t.Fatalf("migrations fs: %v", err)
|
||||
}
|
||||
|
||||
open := func() (*sql.DB, store.Repos) {
|
||||
t.Helper()
|
||||
if err := db.MigrateSQLite(path, migFS); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
database, err := db.OpenSQLite(path)
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
return database, store.NewSQLite(database)
|
||||
}
|
||||
|
||||
db1, repos1 := open()
|
||||
secret1, err := repos1.Settings.Ensure(ctx, settings.JWTSecretKey, settings.GenerateSecret)
|
||||
if err != nil || secret1 == "" {
|
||||
t.Fatalf("ensure secret: %v %q", err, secret1)
|
||||
}
|
||||
tm1 := auth.NewTokenManager(secret1, 15*time.Minute, time.Hour)
|
||||
token, err := tm1.GenerateAccess("user-1", "u@test.local")
|
||||
if err != nil {
|
||||
t.Fatalf("generate: %v", err)
|
||||
}
|
||||
_ = db1.Close()
|
||||
|
||||
db2, repos2 := open()
|
||||
defer db2.Close()
|
||||
secret2, err := repos2.Settings.Ensure(ctx, settings.JWTSecretKey, settings.GenerateSecret)
|
||||
if err != nil {
|
||||
t.Fatalf("ensure after restart: %v", err)
|
||||
}
|
||||
if secret2 != secret1 {
|
||||
t.Fatalf("secret changed across restart")
|
||||
}
|
||||
tm2 := auth.NewTokenManager(secret2, 15*time.Minute, time.Hour)
|
||||
if _, err := tm2.ParseAccess(token); err != nil {
|
||||
t.Fatalf("token issued before restart not valid after: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func waitFor(t *testing.T, what string, cond func() bool) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
|
||||
Reference in New Issue
Block a user