diff --git a/.vscode/launch.json b/.vscode/launch.json index 8b080b58..5d6ec016 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -55,7 +55,6 @@ "env": { //############ PERPETUAL BLOCK ###################### // "ASPNETCORE_ENVIRONMENT": "Development", - "AYANOVA_JWT_SECRET": "1111111MyRandom32CharacterSecret", "AYANOVA_LOG_LEVEL": "Info", //"AYANOVA_LOG_LEVEL": "Debug", //"AYANOVA_LOG_LEVEL": "Trace", @@ -111,7 +110,6 @@ //############ SUBSCRIPTION MODE BLOCK ###################### // "ASPNETCORE_ENVIRONMENT": "Development", //"AYANOVA_REPORT_RENDERING_TIMEOUT":"1", - "AYANOVA_JWT_SECRET": "1111111MyRandom32CharacterSecret", "AYANOVA_LOG_LEVEL": "Info", //"AYANOVA_LOG_LEVEL": "Debug", //"AYANOVA_LOG_LEVEL": "Trace", diff --git a/devdocs/modernization/NET10-PORT.md b/devdocs/modernization/NET10-PORT.md index e855dbbf..458c8262 100644 --- a/devdocs/modernization/NET10-PORT.md +++ b/devdocs/modernization/NET10-PORT.md @@ -178,7 +178,7 @@ John's "next release" cases for AyaNova (exported 2026-10-24). Each is its own s | Case | Name | Findings | |---|---|---| | 4710 | 2FA step requires the temp token (raven and sockeye) | F5, F41. **Done**: raven `141bdeb8` (proof script 401 on both bypasses; integration test `TwoFactor` in `8e5eec4`); sockeye `f7c1d7c` (same checks on local Sockeye with a temporary user). Sockeye to be deployed to its new droplet by John; 2FA can be turned back on once each server runs the fix | -| 4711 | Placeholder/short JWT secret treated as unset (4698 folded in: random secret generated at install, server ignores a leftover placeholder) | F1 | +| 4711 | JWT secret setting removed: the server generates its signing key once into `jwt-signing.key` in the data folder (4698 folded in) | F1. **Done** (placeholder-forged token 401; key survives restart; auth tests pass). Hosted setup script's secret step now does nothing; tidy later in `sockeye-client` | | 4712 | Customer contact route allowlist + ownership checks | F13, F14, F15, F16, F17 | | 4713 | Contact users limited to customer roles | F28 | | 4715 | Download file names checked (one helper) | F21 | @@ -205,7 +205,7 @@ John's "next release" cases for AyaNova (exported 2026-10-24). Each is its own s | 4654 | General notification delivers at most once per 12 hours (circuit breaker) | 00 | | 4694 | Standalone launcher not shutting down PostgreSQL (raven-launcher) | 1 | | 4695 | Shipping the Visual C++ runtime DLLs with the standalone install | 1 | -| 4696 | JWT secret missing from the standalone config.json | 1 | +| 4696 | JWT secret missing from the standalone config.json | 1. Resolved by 4711's server change; confirm on a fresh standalone install | | 4697 | Relative date filters using the server's time zone | 1 | | 4699 | Linux desktop data-path note in the manual | 1, docs | | 4700 | Restore note for customers with a dedicated database role | 1, docs | diff --git a/devdocs/modernization/SECURITY-REVIEW.md b/devdocs/modernization/SECURITY-REVIEW.md index af31840e..12669c57 100644 --- a/devdocs/modernization/SECURITY-REVIEW.md +++ b/devdocs/modernization/SECURITY-REVIEW.md @@ -192,7 +192,7 @@ take the next free number; a fixed or accepted finding keeps its ID and changes | F41 | **Critical** | **fixed in case 4710** (Sockeye; the business server gets it with 8.3.0) | F5 also in Sockeye (internet-facing admin login) and in the 8.2.x business AyaNova server | | F42 | High | confirmed (code) | The licence-signing RSA private keys are hardcoded in Sockeye's source and binaries | | F5 | **Critical** | **fixed in case 4710** | 2FA login step accepts an empty or missing temp token: the password is skipped for users with 2FA on | -| F1 | Critical (installs that kept it) | **confirmed** | Placeholder JWT secret in the LAN/IIS and Linux server config templates | +| F1 | Critical (installs that kept it) | **fixed in case 4711** | Placeholder JWT secret in the LAN/IIS and Linux server config templates | | F19 | Critical | **confirmed** (import) | Any logged-in user, customer contacts included, can import report templates, whose JavaScript runs in the server's browser | | F21 | Critical | candidate | Download file names from the URL are not checked: `report/download` and `export/download` may serve files outside the temp folder on Windows | | F18 | Critical | confirmed (code) | Pick-list search text is put into SQL without escaping; any logged-in user can reach it | diff --git a/dist/assets/lan-install-config.json b/dist/assets/lan-install-config.json index 6128cb4c..f781d260 100644 --- a/dist/assets/lan-install-config.json +++ b/dist/assets/lan-install-config.json @@ -4,8 +4,7 @@ "AYANOVA_DEFAULT_TRANSLATION":"en", "AYANOVA_DATA_PATH":"%ProgramData%\\ayanova", "AYANOVA_BACKUP_PG_DUMP_PATH": "C:\\Program Files\\PostgreSQL\\18\\bin", - "AYANOVA_LOG_LEVEL": "Info", - "AYANOVA_JWT_SECRET": "11111YourRandom32CharacterSecret" + "AYANOVA_LOG_LEVEL": "Info" } diff --git a/dist/assets/linux-server/config.json b/dist/assets/linux-server/config.json index e5505bbe..079bb6d9 100644 --- a/dist/assets/linux-server/config.json +++ b/dist/assets/linux-server/config.json @@ -3,6 +3,5 @@ "AYANOVA_DB_CONNECTION":"Server=localhost;Username=postgres;Password=YOUR_PASSWORD_HERE;Database=AyaNova;CommandTimeout=300;", "AYANOVA_DEFAULT_TRANSLATION":"en", "AYANOVA_DATA_PATH":"/var/ayanova/data/", - "AYANOVA_LOG_LEVEL": "Info", - "AYANOVA_JWT_SECRET": "12345YourRandom32CharacterSecret" + "AYANOVA_LOG_LEVEL": "Info" } \ No newline at end of file diff --git a/docs/8.0/ayanova/docs/ay-start-login.md b/docs/8.0/ayanova/docs/ay-start-login.md index 4af33afd..1ed1b573 100644 --- a/docs/8.0/ayanova/docs/ay-start-login.md +++ b/docs/8.0/ayanova/docs/ay-start-login.md @@ -66,9 +66,7 @@ A User's authentication token can be [revoked at any time](adm-users.md#revoking ## Loss of authentication on server restart -If the Users are forced to re-login any time the AyaNova server is restarted the [JWT Secret setting](ops-config-jwt-secret.md) was probably not set as recommended during installation and should be verified. - -If this setting is configured properly Users will be able to continue working after the server is restarted without the need to log back in again. +Users stay logged in when the AyaNova server restarts, because the server keeps its [sign-in key](ops-config-jwt-secret.md) in its data folder. If Users are forced to log in again after every restart, check the server log at startup: AyaNova notes there if it could not read or create the sign-in key file, which usually means the AyaNova server cannot write to its data folder. ## Logout diff --git a/docs/8.0/ayanova/docs/changelog.md b/docs/8.0/ayanova/docs/changelog.md index d1bbf424..97f6361b 100644 --- a/docs/8.0/ayanova/docs/changelog.md +++ b/docs/8.0/ayanova/docs/changelog.md @@ -19,6 +19,7 @@ IN PROGRESS **Changed** - Server: after several incorrect two-factor authentication codes, the password must be entered again before another code can be tried. +- Server: the `AYANOVA_JWT_SECRET` setting is no longer used. AyaNova now generates its own [sign-in key](ops-config-jwt-secret.md) once and keeps it in its data folder, so users stay logged in when the server restarts. The setting can be removed from `config.json`. - Server: updated to use .NET 10.x LTS from previously used .NET 8.x LTS. .NET 8.x reaches end of support in November 2026. With this update to .NET 10.x come performance and security gains. - Server: all 3rd party components used by the AyaNova server have been upgraded to their most recent release versions to use with .NET 10.x. This improves performance, stability and security for each component used. - Server: the report rendering system now uses the smaller and faster "Chrome headless shell" browser instead of the full Chrome browser. It is downloaded into a `report-browser` folder under the AyaNova data folder instead of the program folder, so it is no longer removed by an upgrade. There will be a longer than normal delay the _first time_ a report is rendered after upgrading while AyaNova automatically downloads the new report rendering browser. Any old `Chrome` and `ChromeHeadlessShell` folders in the AyaNova program folder are no longer used and can be removed. diff --git a/docs/8.0/ayanova/docs/ops-config-environment-variables.md b/docs/8.0/ayanova/docs/ops-config-environment-variables.md index b03f0da3..dd46cdbd 100644 --- a/docs/8.0/ayanova/docs/ops-config-environment-variables.md +++ b/docs/8.0/ayanova/docs/ops-config-environment-variables.md @@ -86,7 +86,7 @@ Command line parameters are appended to the command to start the ayanova server, ### API -- [AYANOVA_JWT_SECRET](ops-config-jwt-secret.md) +- [AYANOVA_JWT_SECRET](ops-config-jwt-secret.md) (no longer used as of version 8.3.0) - [AYANOVA_USE_URLS](ops-config-use-urls.md) ### REPORTING diff --git a/docs/8.0/ayanova/docs/ops-config-jwt-secret.md b/docs/8.0/ayanova/docs/ops-config-jwt-secret.md index 0739250c..f915f791 100644 --- a/docs/8.0/ayanova/docs/ops-config-jwt-secret.md +++ b/docs/8.0/ayanova/docs/ops-config-jwt-secret.md @@ -1,56 +1,23 @@ -# JWT secret setting +# Security and JWT web tokens AyaNova uses JSON Web Tokens (JWT) for authentication. -These time limited tokens are signed by the server using a JWT secret key and issued to users when they log in to the AyaNova server. +When a User logs in, the AyaNova server issues them a time limited token signed with the server's own sign-in key. The token is sent with every request the User makes and is verified by the server each time. -Every time the user makes a request to the server the JWT is sent along as well and verified to be valid. +Tokens expire 5 days after they are issued, and each User can only have one current token: logging in again replaces the previous one. -Tokens have a built in expiry mechanism of 5 days from issue. +(Individual Users can be prevented from logging in or accessing the server by [setting them inactive](adm-users.md#revoking-access-and-locking-out-users), which takes effect immediately regardless of how many days are left on their current token.) -All active tokens previously issued can be invalidated by changing this JWT Secret setting and restarting the server (or restarting the server and allowing it to choose a new secret value randomly if none is specified). +## The sign-in key -(Individual Users can be prevented from logging in or accessing the server by setting them inactive which takes effect immediately regardless of how many days are left on their current access token.) +The AyaNova server generates its sign-in key automatically the first time it starts and keeps it in a file named `jwt-signing.key` in the AyaNova [data folder](ops-config-data-path.md). It is used from then on, so Users stay logged in when the server restarts. There is nothing to configure. -## Default +Keep the data folder protected like the rest of AyaNova's data: anyone who can read the sign-in key file could create valid tokens. -If no secret key is specified the server will generate a new, random one each time it starts and this means that remote users who previously authenticated will need to login freshly if the server is restarted. +## Signing everyone out -If you would like to ensure that a server reboot does not affect remote users credentials then you can specify a value for the secret key so that the same key will always be used by the server even if it reboots. - -## Overriding - -AyaNova expects the JWT secret to be provided by a config.json property, environment variable or command line parameter named - -`AYANOVA_JWT_SECRET` - -The value specified should be a string of up to 32 characters, for example: -`02847This_is_my_secret_key456576` -If fewer than 32 characters are provided they secret will be padded out to 32 characters. If more than 32 characters are specified it will only use the first 32. - -You should use the same precautions as for choosing any other password and ensure the secret is not well known or easily looked up in a dictionary. 32 random characters would be sufficient. - -Example config.json entry - -```json -{ - ...other properties... - "AYANOVA_JWT_SECRET": "My32CharacterSecretKey0123456789" -} -``` - -Example command line parameter - -`ayanova.exe --AYANOVA_JWT_SECRET="02847This_is_my_secret_key456576"` - -Example environment variable - -Windows - -`set "AYANOVA_JWT_SECRET=02847This_is_my_secret_key456576"` - -Linux - -`export AYANOVA_JWT_SECRET="02847This_is_my_secret_key456576"` +To invalidate every token that has been issued (for example if you suspect a token or the key file was exposed): stop the AyaNova server, delete the `jwt-signing.key` file from the data folder, and start the server again. A new key is generated and every User will need to log in again. +## AYANOVA_JWT_SECRET (no longer used) +Before version 8.3.0 the sign-in key could be set with an `AYANOVA_JWT_SECRET` configuration setting. That setting is no longer used: if it is still present in config.json, an environment variable or the command line it is ignored, a note is written to the server log, and it can safely be removed. diff --git a/docs/8.0/ayanova/docs/ops-install-linux-server.md b/docs/8.0/ayanova/docs/ops-install-linux-server.md index 936fa562..c9bd169c 100644 --- a/docs/8.0/ayanova/docs/ops-install-linux-server.md +++ b/docs/8.0/ayanova/docs/ops-install-linux-server.md @@ -197,16 +197,6 @@ The [AYANOVA_DB_CONNECTION](ops-config-db.md) property must be edited and where The [AYANOVA_USE_URLS](ops-config-use-urls.md) property specifies which TCP / IP Port that AyaNova server should listen on for connections from web browsers. The default value is port 7575 which should be free in most cases, however if you need to use an alternate port due to conflict it must be set here. We recommend using the default 7575 to avoid current or future confusion or conflict with other servers that may be installed or present elsewhere on the network. -Optional but recommended: Add a 32 character [AYANOVA_JWT_SECRET](ops-config-jwt-secret.md) property to the configuration if you want to ensure that logged in User's session can survive a restart of the AyaNova server. If this is not set AyaNova will invalidate all login tokens any time the server restarts by generating a new random JWT secret on boot. - -```json -{ - ...existing properties... - "AYANOVA_LOG_LEVEL": "Info", - "AYANOVA_JWT_SECRET": "1111111MyRandom32CharacterSecret" -} -``` - Edit the config.json file in your favorite editor, here we'll use Nano: ```bash diff --git a/docs/8.0/ayanova/docs/ops-install-windows-iis.md b/docs/8.0/ayanova/docs/ops-install-windows-iis.md index ba6e6ea7..df67ca43 100644 --- a/docs/8.0/ayanova/docs/ops-install-windows-iis.md +++ b/docs/8.0/ayanova/docs/ops-install-windows-iis.md @@ -232,20 +232,6 @@ The [AYANOVA_BACKUP_PG_DUMP_PATH](ops-config-backup-pgdump-path.md) property spe A. if AyaNova server will be doing the automatic daily backups using AyaNova's Backup feature, confirm this path stated matches the path to YOUR PostgreSQL database backup utility pg_dump. The `18` in the path is the PostgreSQL version number: if you installed a different version, change it to match (for example `C:\\Program Files\\PostgreSQL\\17\\bin`) B. otherwise, If you already have a backup solution in place for your PostgreSQL databases AND WILL NOT BE USING AYANOVA'S Backup feature, remove this -OPTIONAL EDIT: - -Modify the 32 character [AYANOVA_JWT_SECRET](ops-config-jwt-secret.md) property of the configuration which ensures that the logged in User's session can survive a restart of the AyaNova server. If this is not set AyaNova will invalidate all login tokens any time the server restarts by generating a new random JWT secret on boot which under IIS happens frequently. - -Change this from the default of "11111YourRandom32CharacterSecret" to be unique to your site (must be 32 characters). - -```json -{ - ...existing properties... - "AYANOVA_LOG_LEVEL": "Info", - "AYANOVA_JWT_SECRET": "11111YourRandom32CharacterSecret" -} -``` - After editing, saving and exiting out of NotePad, open the config.json file AGAIN to confirm that the changes were saved correctly and that the file is still specifically labeled with the name and extension config.json ### 6\. Boot and test the server diff --git a/docs/8.0/ayanova/docs/ops-install-windows-lan.md b/docs/8.0/ayanova/docs/ops-install-windows-lan.md index b9ee512d..42719f55 100644 --- a/docs/8.0/ayanova/docs/ops-install-windows-lan.md +++ b/docs/8.0/ayanova/docs/ops-install-windows-lan.md @@ -164,20 +164,6 @@ The [AYANOVA_BACKUP_PG_DUMP_PATH](ops-config-backup-pgdump-path.md) property spe A. if AyaNova server will be doing the automatic daily backups using AyaNova's Backup feature, confirm this path stated matches the path to YOUR PostgreSQL database backup utility pg_dump. The `18` in the path is the PostgreSQL version number: if you installed a different version, change it to match (for example `C:\\Program Files\\PostgreSQL\\17\\bin`) B. otherwise, If you already have a backup solution in place for your PostgreSQL databases AND WILL NOT BE USING AYANOVA'S Backup feature, remove this -OPTIONAL EDIT: - -Modify the 32 character [AYANOVA_JWT_SECRET](ops-config-jwt-secret.md) property of the configuration which ensures that the logged in User's session can survive a restart of the AyaNova server. If this is not set AyaNova will invalidate all login tokens any time the server restarts by generating a new random JWT secret on boot. - -Change this from the default of "11111YourRandom32CharacterSecret" to be unique to your site (must be 32 characters). - -```json -{ - ...existing properties... - "AYANOVA_LOG_LEVEL": "Info", - "AYANOVA_JWT_SECRET": "11111YourRandom32CharacterSecret" -} -``` - After editing, saving and exiting out of NotePad, open the config.json file AGAIN to confirm that the changes were saved correctly and that the file is still specifically labeled with the name and extension config.json ### 5\. Boot and test the server diff --git a/server/AyaNova/Startup.cs b/server/AyaNova/Startup.cs index 1205dc59..cbc3331d 100644 --- a/server/AyaNova/Startup.cs +++ b/server/AyaNova/Startup.cs @@ -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"); diff --git a/server/AyaNova/util/ServerBootConfig.cs b/server/AyaNova/util/ServerBootConfig.cs index cb1a28f5..86b7c18a 100644 --- a/server/AyaNova/util/ServerBootConfig.cs +++ b/server/AyaNova/util/ServerBootConfig.cs @@ -59,7 +59,12 @@ namespace AyaNova.Util internal static long AYANOVA_DEFAULT_TRANSLATION_ID { get; set; } //internal setting set at boot by TranslationBiz::ValidateTranslations //API + //case 4711: the JWT signing key, set at boot from JWT_SIGNING_KEY_FILE_PATH (generated once); no longer a setting internal static string AYANOVA_JWT_SECRET { get; set; } + //case 4711: true if the old AYANOVA_JWT_SECRET setting is still present, so boot can say it is ignored + internal static bool AYANOVA_JWT_SECRET_SETTING_PRESENT { get; set; } + //case 4711: where the generated JWT signing key is kept (not a setting, derived from AYANOVA_DATA_PATH, else beside the app) + internal static string JWT_SIGNING_KEY_FILE_PATH { get; set; } internal static string AYANOVA_USE_URLS { get; set; } internal static int AYANOVA_REPORT_RENDERING_TIMEOUT { get; set; } @@ -174,7 +179,8 @@ namespace AyaNova.Util ? "http://*:7575" : AYANOVA_USE_URLS; - AYANOVA_JWT_SECRET = config.GetValue("AYANOVA_JWT_SECRET"); + //case 4711: no longer used, only noted so boot can log that it is ignored + AYANOVA_JWT_SECRET_SETTING_PRESENT = !string.IsNullOrWhiteSpace(config.GetValue("AYANOVA_JWT_SECRET")); //backdoor back door password superuser reset AYANOVA_SET_SUPERUSER_PW = config.GetValue("AYANOVA_SET_SUPERUSER_PW"); @@ -270,6 +276,10 @@ namespace AyaNova.Util (string.IsNullOrWhiteSpace(DataFolderPath)) ? string.Empty : Path.Combine(DataFolderPath, "report-browser"); + JWT_SIGNING_KEY_FILE_PATH = + (string.IsNullOrWhiteSpace(DataFolderPath)) + ? Path.Combine(AppContext.BaseDirectory, "jwt-signing.key") + : Path.Combine(DataFolderPath, "jwt-signing.key"); #endregion server BASICS }