case 4711 - JWT signing key generated by the server; AYANOVA_JWT_SECRET removed

The LAN/IIS and Linux templates shipped a placeholder secret printed in the
manual, and installs that kept it signed logins with a public key; any setting
also invited short or reused values (security review F1). The setting is now
ignored (one warning at boot if still present): the server generates a key once
(48 random bytes) into jwt-signing.key in the data folder (beside the app if
there is none; owner-only on Linux) and reuses it, so sign-ins also survive a
restart where the server used to make a new key each boot (standalone, Linux
desktop). If the file cannot be used, a temporary key is used for that run.

Templates and dev launch configs drop the setting; manual updated (key page
rewritten, setting steps removed from install pages); changelog line.

Checked locally: token forged with the old placeholder 401; key file created
and reused after restart (old token 200); auth and 2FA tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 12:01:08 -07:00
parent 5bf1f2700a
commit a843bd3132
14 changed files with 61 additions and 113 deletions

View File

@@ -228,27 +228,41 @@ namespace AyaNova
#endregion
#region JWT AUTHENTICATION
//get the key if specified
var secretKey = ServerBootConfig.AYANOVA_JWT_SECRET;
//case 4711: the signing key is no longer a setting (placeholders and weak values could be used to forge tokens);
//it is generated once and kept in a file so sign-ins survive a restart. Deleting the file signs everyone out.
if (ServerBootConfig.AYANOVA_JWT_SECRET_SETTING_PRESENT)
_newLog.LogWarning("AYANOVA_JWT_SECRET is no longer used and can be removed from the configuration: AyaNova generates and keeps its own sign-in key");
//If no key specified make a unique one
//This means the jwt creds won't survive a server reboot
//so in that case users need to specify an AyaNova_JWT_SECRET environment variable
if (string.IsNullOrWhiteSpace(secretKey))
ServerBootConfig.AYANOVA_JWT_SECRET = LoadOrCreateJwtSigningKey(ServerBootConfig.JWT_SIGNING_KEY_FILE_PATH);
string LoadOrCreateJwtSigningKey(string path)
{
_newLog.LogWarning("AYANOVA_JWT_SECRET configuration setting is missing; AyaNova will randomly generate one. Any Users who were logged in when the server restarted will need to login to get a fresh auth token. See manual 'AYANOVA_JWT_SECRET' page for details.");
secretKey = Util.Hasher.GenerateSalt();
}
//WAS "UNLICENSED5G*QQJ8#bQ7$Xr_@sXfHq4"
//If secretKey is less than 32 characters, pad it
if (secretKey.Length < 32)
{
secretKey = secretKey.PadRight(32, '-');
try
{
if (System.IO.File.Exists(path))
{
var existing = System.IO.File.ReadAllText(path).Trim();
if (existing.Length >= 32)
return existing;
_newLog.LogWarning($"Sign-in key file {path} is not valid; generating a new one (users will need to log in again)");
}
var newKey = Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(48));
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path));
var options = new System.IO.FileStreamOptions { Mode = System.IO.FileMode.Create, Access = System.IO.FileAccess.Write };
if (!OperatingSystem.IsWindows())
options.UnixCreateMode = System.IO.UnixFileMode.UserRead | System.IO.UnixFileMode.UserWrite;
using (var writer = new System.IO.StreamWriter(path, System.Text.Encoding.ASCII, options))
writer.Write(newKey);
_newLog.LogInformation($"Generated a new sign-in key in {path}");
return newKey;
}
catch (Exception ex)
{
_newLog.LogWarning(ex, $"Could not read or create the sign-in key file {path}; using a temporary key for this run (users will need to log in again after a restart)");
return Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(48));
}
}
ServerBootConfig.AYANOVA_JWT_SECRET = secretKey;
var signingKey = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_JWT_SECRET));
_newLog.LogDebug("Authorization");