From d45af8700d59c4e7da8789e1d48cd8d72d3f67c2 Mon Sep 17 00:00:00 2001 From: Mitch Gaffigan Date: Wed, 26 Aug 2026 15:19:26 -0500 Subject: [PATCH] Generate password on first boot Signed-off-by: Mitch Gaffigan --- server/conf/mirth.properties | 4 + server/dbconf/derby/derby-database.sql | 2 - server/dbconf/mysql/mysql-database.sql | 2 - server/dbconf/oracle/oracle-database.sql | 2 - server/dbconf/postgres/postgres-database.sql | 2 - .../dbconf/sqlserver/sqlserver-database.sql | 2 - server/docs/README.txt | 7 +- .../api/servlets/UserServletInterface.java | 2 +- .../server/migration/ServerMigrator.java | 75 +++++++++++++++---- 9 files changed, 70 insertions(+), 28 deletions(-) diff --git a/server/conf/mirth.properties b/server/conf/mirth.properties index fdf96a8453..6b71ebdbf3 100644 --- a/server/conf/mirth.properties +++ b/server/conf/mirth.properties @@ -54,6 +54,10 @@ server.api.accesscontrolallowheaders = Content-Type server.api.accesscontrolexposeheaders = server.api.accesscontrolmaxage = +# Password for the admin user, only applied when the database is first created. +# If blank, a random password is generated and written to the log once on first startup. +server.initialadminpassword = + # Determines whether or not channels are deployed on server startup. server.startupdeploy = true diff --git a/server/dbconf/derby/derby-database.sql b/server/dbconf/derby/derby-database.sql index 1cd957b261..b71b13977e 100644 --- a/server/dbconf/derby/derby-database.sql +++ b/server/dbconf/derby/derby-database.sql @@ -105,8 +105,6 @@ CREATE TABLE DEBUGGER_USAGE INSERT INTO PERSON (USERNAME, LOGGED_IN) VALUES('admin', 0); -INSERT INTO PERSON_PASSWORD (PERSON_ID, PASSWORD) VALUES(1, 'b8cA3mDkavInMc2JBYa6/C3EGxDp7ppqh7FsoXx0x8+3LWK3Ed3ELg=='); - INSERT INTO SCHEMA_INFO (VERSION) VALUES ('4.6.0'); INSERT INTO CONFIGURATION (CATEGORY, NAME, VALUE) VALUES ('core', 'stats.enabled', '1'); diff --git a/server/dbconf/mysql/mysql-database.sql b/server/dbconf/mysql/mysql-database.sql index 332c55e65e..33c77acddf 100644 --- a/server/dbconf/mysql/mysql-database.sql +++ b/server/dbconf/mysql/mysql-database.sql @@ -111,8 +111,6 @@ CREATE TABLE DEBUGGER_USAGE INSERT INTO PERSON (USERNAME, LOGGED_IN) VALUES('admin', FALSE); -INSERT INTO PERSON_PASSWORD (PERSON_ID, PASSWORD) VALUES(1, 'b8cA3mDkavInMc2JBYa6/C3EGxDp7ppqh7FsoXx0x8+3LWK3Ed3ELg=='); - INSERT INTO SCHEMA_INFO (VERSION) VALUES ('4.6.0'); INSERT INTO CONFIGURATION (CATEGORY, NAME, VALUE) VALUES ('core', 'stats.enabled', '1'); diff --git a/server/dbconf/oracle/oracle-database.sql b/server/dbconf/oracle/oracle-database.sql index e059d1cafb..541bbfbc2d 100644 --- a/server/dbconf/oracle/oracle-database.sql +++ b/server/dbconf/oracle/oracle-database.sql @@ -108,8 +108,6 @@ CREATE TABLE DEBUGGER_USAGE INSERT INTO PERSON (ID, USERNAME, LOGGED_IN, USERCONSENT) VALUES (PERSON_SEQUENCE.NEXTVAL, 'admin', '0', '0'); -INSERT INTO PERSON_PASSWORD (PERSON_ID, PASSWORD) VALUES(PERSON_SEQUENCE.CURRVAL, 'b8cA3mDkavInMc2JBYa6/C3EGxDp7ppqh7FsoXx0x8+3LWK3Ed3ELg=='); - INSERT INTO SCHEMA_INFO (VERSION) VALUES ('4.6.0'); INSERT INTO CONFIGURATION (CATEGORY, NAME, VALUE) VALUES ('core', 'stats.enabled', '1'); diff --git a/server/dbconf/postgres/postgres-database.sql b/server/dbconf/postgres/postgres-database.sql index 74011b7ac0..4e1e76b4c9 100644 --- a/server/dbconf/postgres/postgres-database.sql +++ b/server/dbconf/postgres/postgres-database.sql @@ -107,8 +107,6 @@ CREATE TABLE DEBUGGER_USAGE INSERT INTO PERSON (USERNAME, LOGGED_IN) VALUES('admin', FALSE); -INSERT INTO PERSON_PASSWORD (PERSON_ID, PASSWORD) VALUES(1, 'b8cA3mDkavInMc2JBYa6/C3EGxDp7ppqh7FsoXx0x8+3LWK3Ed3ELg=='); - INSERT INTO SCHEMA_INFO (VERSION) VALUES ('4.6.0'); INSERT INTO CONFIGURATION (CATEGORY, NAME, VALUE) VALUES ('core', 'stats.enabled', '1'); diff --git a/server/dbconf/sqlserver/sqlserver-database.sql b/server/dbconf/sqlserver/sqlserver-database.sql index b0bd53ec10..dbfbe93aac 100644 --- a/server/dbconf/sqlserver/sqlserver-database.sql +++ b/server/dbconf/sqlserver/sqlserver-database.sql @@ -114,8 +114,6 @@ CREATE TABLE DEBUGGER_USAGE INSERT INTO PERSON (USERNAME, LOGGED_IN, USERCONSENT) VALUES('admin', 0, 0); -INSERT INTO PERSON_PASSWORD (PERSON_ID, PASSWORD) VALUES(1, 'b8cA3mDkavInMc2JBYa6/C3EGxDp7ppqh7FsoXx0x8+3LWK3Ed3ELg=='); - INSERT INTO SCHEMA_INFO (VERSION) VALUES ('4.6.0'); INSERT INTO CONFIGURATION (CATEGORY, NAME, VALUE) VALUES ('core', 'stats.enabled', '1'); diff --git a/server/docs/README.txt b/server/docs/README.txt index 5c99dbb330..f050101423 100644 --- a/server/docs/README.txt +++ b/server/docs/README.txt @@ -46,7 +46,12 @@ After the installation, the Mirth Connect directory layout will look as follows: Once Mirth Connect has been installed, there are several ways to connect to launch the Mirth Connect Administrator. On a Windows installation, there is a Mirth Connect Administrator item in the Start Menu which launches the application directly. -If the option is not available, you can connect to the Mirth Connect Administrator launch page which by default should be available at http://[server address]:8080 (i.e. http://localhost:8080). Clicking the Launch Mirth Connect Administrator button will connect you to the server which will be listening on https://[server address]:8443 (i.e. https://localhost:8443). If running a new installation, the default username and password for the login screen is admin and admin. This should be changed immediately for security purposes. +If the option is not available, you can connect to the Mirth Connect Administrator launch page which by default should be available at http://[server address]:8080 (i.e. http://localhost:8080). Clicking the Launch Mirth Connect Administrator button will connect you to the server which will be listening on https://[server address]:8443 (i.e. https://localhost:8443). + +If running a new installation, the username for the login screen is admin, and its initial password is generated +randomly when the database is first created. You can find that password in logs/mirth.log, where it is written +once on first startup. You will be asked to change it when you log in. To choose the initial password yourself +instead, set server.initialadminpassword in conf/mirth.properties before starting the server for the first time. If you are launching the administrator for the first time, you will notice that the libraries for the Mirth Connect Administrator will be loaded. This feature allows you run the Administrator from any remote Mirth Connect server without having to download and install a separate client. diff --git a/server/src/main/java/com/mirth/connect/client/core/api/servlets/UserServletInterface.java b/server/src/main/java/com/mirth/connect/client/core/api/servlets/UserServletInterface.java index 906c9f4647..954ae9e278 100644 --- a/server/src/main/java/com/mirth/connect/client/core/api/servlets/UserServletInterface.java +++ b/server/src/main/java/com/mirth/connect/client/core/api/servlets/UserServletInterface.java @@ -63,7 +63,7 @@ public interface UserServletInterface extends BaseServletInterface { @MirthOperation(name = "login", display = "Login") public LoginStatus login(// @formatter:off @Param("username") @Parameter(description = "The username to login with.", required = true, schema = @Schema(defaultValue = "admin")) @FormParam("username") String username, - @Param(value = "password", excludeFromAudit = true) @Parameter(description = "The password to login with.", required = true, schema = @Schema(defaultValue = "admin")) @FormParam("password") String password) throws ClientException; + @Param(value = "password", excludeFromAudit = true) @Parameter(description = "The password to login with.", required = true) @FormParam("password") String password) throws ClientException; // @formatter:on @POST diff --git a/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java b/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java index 09a021c373..e1a74f10ac 100644 --- a/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java +++ b/server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java @@ -25,11 +25,13 @@ import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.commons.configuration2.PropertiesConfigurationLayout; import org.apache.commons.dbutils.DbUtils; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import com.mirth.connect.client.core.PropertiesConfigurationUtil; import com.mirth.connect.client.core.Version; import com.mirth.connect.model.Channel; import com.mirth.connect.model.ExportClearable; @@ -38,9 +40,16 @@ import com.mirth.connect.model.codetemplates.CodeTemplateLibrary; import com.mirth.connect.model.converters.ObjectXMLSerializer; import com.mirth.connect.model.util.MigrationException; +import com.mirth.connect.server.Mirth; +import com.mirth.connect.server.controllers.ConfigurationController; +import com.mirth.connect.server.controllers.ControllerFactory; import com.mirth.connect.server.util.DatabaseUtil; public class ServerMigrator extends Migrator { + private static final String INITIAL_ADMIN_USERNAME = "admin"; + private static final String INITIAL_ADMIN_PASSWORD_PROPERTY = "server.initialadminpassword"; + private static final int GENERATED_PASSWORD_LENGTH = 20; + private Logger logger = LogManager.getLogger(getClass()); public ServerMigrator() { @@ -250,27 +259,61 @@ private void initDatabase(Connection connection) throws MigrationException { if (!DatabaseUtil.tableExists(connection, "CONFIGURATION")) { executeScript("/" + getDatabaseType() + "/" + getDatabaseType() + "-database.sql"); - /* - * We must update the password date for the initial user. Previously we let the database - * set this via CURRENT_TIMESTAMP, however this could create problems if the database is - * running on a separate machine in a different timezone. (MIRTH-2902) - */ - PreparedStatement statement = null; - - try { - statement = getConnection().prepareStatement("UPDATE PERSON_PASSWORD SET PASSWORD_DATE = ?"); - statement.setTimestamp(1, new Timestamp(System.currentTimeMillis())); - statement.executeUpdate(); - } catch (SQLException e) { - throw new MigrationException(e); - } finally { - DbUtils.closeQuietly(statement); - } + initializeAdminPassword(); updateVersion(Version.getLatest()); } } + /** + * Sets the password for the initial administrator account. The password is taken from the + * server.initialadminpassword property if it is set, otherwise a random password is generated + * and logged once so that it can be used to log in for the first time. + * + * The password date is set here rather than letting the database default it via + * CURRENT_TIMESTAMP, since that could create problems if the database is running on a separate + * machine in a different timezone. (MIRTH-2902) + */ + private void initializeAdminPassword() throws MigrationException { + ConfigurationController configurationController = ControllerFactory.getFactory().createConfigurationController(); + PropertiesConfiguration mirthProperties = PropertiesConfigurationUtil.create(); + configurationController.updatePropertiesConfiguration(mirthProperties); + // Log through the central Mirth.class logger to hit log4j filter configs + Logger startupLogger = LogManager.getLogger(Mirth.class); + + String password = mirthProperties.getString(INITIAL_ADMIN_PASSWORD_PROPERTY); + if (StringUtils.isBlank(password)) { + // Prefix ensures the password meets complexity requirements + password = "Aa1!" + RandomStringUtils.secure().nextAlphanumeric(GENERATED_PASSWORD_LENGTH); + + startupLogger.warn(System.lineSeparator() + + "********************************************************************************" + System.lineSeparator() + + "************ Initial {} password is {} ************" + System.lineSeparator() + + "********************************************************************************", + INITIAL_ADMIN_USERNAME, password); + } + + PreparedStatement statement = null; + try { + String digestedPassword = configurationController.getDigester().digest(password); + + statement = getConnection().prepareStatement( + "INSERT INTO PERSON_PASSWORD (PERSON_ID, PASSWORD, PASSWORD_DATE) " + + "SELECT ID, ?, ? FROM PERSON WHERE USERNAME = ?"); + statement.setString(1, digestedPassword); + statement.setTimestamp(2, new Timestamp(System.currentTimeMillis())); + statement.setString(3, INITIAL_ADMIN_USERNAME); + + if (statement.executeUpdate() != 1) { + throw new MigrationException("Could not set the initial password for the \"" + INITIAL_ADMIN_USERNAME + "\" user."); + } + } catch (SQLException e) { + throw new MigrationException(e); + } finally { + DbUtils.closeQuietly(statement); + } + } + /** * In case multiple servers startup and initialize the database at the same time, this inserts a * row into a custom table as a simple lock mechanism.