This is an automated email from the ASF dual-hosted git repository.

mridulpathak pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/trunk by this push:
     new ca93f77054 Improved: added a rotateSecretKeys Gradle task to 
unconditionally replace an already-set JWT/password secret key
ca93f77054 is described below

commit ca93f77054e4b73f77c8d05db42f7dde1c7b362c
Author: Mridul Pathak <[email protected]>
AuthorDate: Fri Sep 25 14:26:56 2026 +0530

    Improved: added a rotateSecretKeys Gradle task to unconditionally replace 
an already-set JWT/password secret key
    
    generateSecretKeys only fills in security.token.key and 
login.secret_key_string when they are missing or blank, and leaves an 
already-set value untouched. There was previously no supported way to replace 
an existing value other than editing security.properties by hand. Added 
rotateSecretKeys, which unconditionally regenerates both keys, and pointed the 
security.properties comments at it. generateSecretKeys itself is unchanged in 
behavior.
---
 build.gradle                                  | 57 +++++++++++++++++----------
 framework/security/config/security.properties |  8 +++-
 2 files changed, 43 insertions(+), 22 deletions(-)

diff --git a/build.gradle b/build.gradle
index ecb93b19bd..88a02e105a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1161,29 +1161,31 @@ task reEncryptAllSecrets(group: sysadminGroup,
     }
 }
 
+// Writes a cryptographically secure random key for propertyName into 
propertiesFile. Unless force is
+// true, a property that is already set (uncommented, non-blank) is left 
untouched and the call is a no-op.
+def generateAndWriteKey(File propertiesFile, String propertyName, boolean 
force = false) {
+    def content = propertiesFile.text
+    def escapedName = propertyName.replace('.', '\\.')
+    if (!force && content =~ /(?m)^${escapedName}=.+$/) {
+        println "Secret key '${propertyName}' is already set, skipping."
+        return
+    }
+    def keyBytes = new byte[48] // 48 bytes * 4/3 = 64 Base64 chars (no 
padding needed)
+    new java.security.SecureRandom().nextBytes(keyBytes)
+    def key = java.util.Base64.getEncoder().encodeToString(keyBytes)
+    if (content =~ /(?m)^#?${escapedName}=.*$/) {
+        content = content.replaceAll(/(?m)^#?${escapedName}=.*$/, 
"${propertyName}=${key}")
+    } else {
+        content += "\n${propertyName}=${key}\n"
+    }
+    propertiesFile.text = content
+}
+
 task generateSecretKeys(group: sysadminGroup,
         description: 'Generate cryptographically secure secret keys for JWT 
token signing, password encryption, '
-                + 'and AdminServer authentication, and write them to their 
properties files') {
+                + 'and AdminServer authentication, and write them to their 
properties files. A property that is '
+                + 'already set is left untouched; use rotateSecretKeys to 
replace an existing JWT/password key.') {
     doLast {
-        def generateAndWriteKey = { File propertiesFile, String propertyName ->
-            def content = propertiesFile.text
-            def escapedName = propertyName.replace('.', '\\.')
-            // Only generate a key if the property is missing or commented out
-            if (content =~ /(?m)^${escapedName}=.+$/) {
-                println "Secret key '${propertyName}' is already set, 
skipping."
-                return
-            }
-            def keyBytes = new byte[48] // 48 bytes * 4/3 = 64 Base64 chars 
(no padding needed)
-            new java.security.SecureRandom().nextBytes(keyBytes)
-            def key = java.util.Base64.getEncoder().encodeToString(keyBytes)
-            if (content =~ /(?m)^#?${escapedName}=.*$/) {
-                content = content.replaceAll(/(?m)^#?${escapedName}=.*$/, 
"${propertyName}=${key}")
-            } else {
-                content += "\n${propertyName}=${key}\n"
-            }
-            propertiesFile.text = content
-        }
-
         def securityPropertiesFile = 
file('framework/security/config/security.properties')
         generateAndWriteKey(securityPropertiesFile, 'login.secret_key_string')
         generateAndWriteKey(securityPropertiesFile, 'security.token.key')
@@ -1195,6 +1197,21 @@ task generateSecretKeys(group: sysadminGroup,
     }
 }
 
+task rotateSecretKeys(group: sysadminGroup,
+        description: 'Unconditionally regenerate security.token.key (JWT 
signing) and login.secret_key_string '
+                + '(password encryption) in security.properties, overwriting 
any existing value. Unlike '
+                + 'generateSecretKeys, this always overwrites: existing JWT 
tokens will stop validating and any '
+                + 'in-flight forgot-password link encrypted with the old 
login.secret_key_string will stop '
+                + 'decrypting.') {
+    doLast {
+        def securityPropertiesFile = 
file('framework/security/config/security.properties')
+        generateAndWriteKey(securityPropertiesFile, 'login.secret_key_string', 
true)
+        generateAndWriteKey(securityPropertiesFile, 'security.token.key', true)
+
+        println "Secret keys rotated. Keep the new keys secret and do not 
commit them to version control."
+    }
+}
+
 // ========== OFBiz Plugin Management ==========
 task createPlugin(group: ofbizPlugin, description: 'create a new plugin 
component based on specified templates') {
     doLast {
diff --git a/framework/security/config/security.properties 
b/framework/security/config/security.properties
index 5adcf87f1a..71745c21a2 100644
--- a/framework/security/config/security.properties
+++ b/framework/security/config/security.properties
@@ -144,7 +144,9 @@ security.login.externalLoginKey.enabled=true
 # -- Security key used to encrypt and decrypt the autogenerated password in 
forgot password functionality.
 #    Read Passwords and JWT (JSON Web Tokens) usage documentation to choose 
the way you want to store this key
 #    The key must be 512 bits (ie 64 chars) as we use HMAC512 to create the 
token, cf. OFBIZ-12724
-#    Run './gradlew generateSecretKeys' to generate a cryptographically secure 
random key.
+#    Run './gradlew generateSecretKeys' to generate a cryptographically secure 
random key; this only fills in
+#    a missing/blank value and leaves an existing one untouched. To replace an 
already-set value, run
+#    './gradlew rotateSecretKeys' instead.
 login.secret_key_string=
 
 # -- Time To Live of the token send to the external server in seconds
@@ -160,7 +162,9 @@ security.internal.sso.enabled=false
 # -- The secret key for the JWT token signature.
 #    Read Passwords and JWT (JSON Web Tokens) usage documentation to choose 
the way you want to store this key
 #    The key must be 512 bits (ie 64 chars) as we use HMAC512 to create the 
token, cf. OFBIZ-12724
-#    Run './gradlew generateSecretKeys' to generate a cryptographically secure 
random key.
+#    Run './gradlew generateSecretKeys' to generate a cryptographically secure 
random key; this only fills in
+#    a missing/blank value and leaves an existing one untouched. To replace an 
already-set value, run
+#    './gradlew rotateSecretKeys' instead.
 security.token.key=
 
 # -- Specifies the expected issuer (the "iss" claim) of JSON Web Tokens (JWTs).

Reply via email to