This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 13215013377968611150e42f3822d79868377484 Author: opencode <[email protected]> AuthorDate: Wed Oct 7 16:04:43 2026 +0200 Fix HTTP DIGEST authentication via JAASRealm For DIGEST, JAASRealm.authenticate() passes the raw client digest to JAASCallbackHandler as the password. The handler constructor pre-mutated that value via CredentialHandler.mutate() whenever the realm's MessageDigestCredentialHandler used the same algorithm as the digest authentication. JAASMemoryLoginModule then handed the mutated value back as the clientDigest of the RealmBase digest comparison, where it was compared against the deterministic digest computed from the stored credentials. A salted or re-digested value can never equal that server digest, so DIGEST authentication always failed in this configuration. The pre-mutation was a leftover of the pre-Credential Handler realm.digest() design and the CredentialHandler API does not expect pre-mutated input anywhere else. Pass the password or client digest to the LoginModule unchanged. The branch could only trigger for DIGEST with a matching algorithm, so BASIC, FORM and CLIENT-CERT flows are unaffected. Update the class and constructor documentation, the JAASRealm javadoc and the realm howto accordingly, and add a regression test for the DIGEST flow. Include a changelog entry. --- .../apache/catalina/realm/JAASCallbackHandler.java | 18 +++--- java/org/apache/catalina/realm/JAASRealm.java | 8 +-- test/org/apache/catalina/realm/TestJAASRealm.java | 66 ++++++++++++++++++++++ webapps/docs/changelog.xml | 10 ++++ webapps/docs/realm-howto.xml | 8 ++- 5 files changed, 92 insertions(+), 18 deletions(-) diff --git a/java/org/apache/catalina/realm/JAASCallbackHandler.java b/java/org/apache/catalina/realm/JAASCallbackHandler.java index 0158a6904b..11af6bbaba 100644 --- a/java/org/apache/catalina/realm/JAASCallbackHandler.java +++ b/java/org/apache/catalina/realm/JAASCallbackHandler.java @@ -32,8 +32,8 @@ import org.apache.tomcat.util.res.StringManager; * Implementation of the JAAS <code>CallbackHandler</code> interface, used to negotiate delivery of the username and * credentials that were specified to our constructor. No interaction with the user is required (or possible). * <p> - * This <code>CallbackHandler</code> will pre-digest the supplied password, if required by the - * <code><Realm></code> element in <code>server.xml</code>. + * The supplied password or client digest is passed to the <code>LoginModule</code> unchanged. Any digest comparison is + * expected to be performed by the <code>LoginModule</code> when it calls back into the <code>Realm</code>. * <p> * At present, <code>JAASCallbackHandler</code> knows how to handle callbacks of type * <code>javax.security.auth.callback.NameCallback</code>, @@ -46,8 +46,7 @@ public class JAASCallbackHandler implements CallbackHandler { /** - * Construct a callback handler configured with the specified values. Note that if the <code>JAASRealm</code> - * instance specifies digested passwords, the <code>password</code> parameter will be pre-digested here. + * Construct a callback handler configured with the specified values. * * @param realm Our associated JAASRealm instance * @param username Username to be authenticated with @@ -64,7 +63,9 @@ public class JAASCallbackHandler implements CallbackHandler { * * @param realm Our associated JAASRealm instance * @param username Username to be authenticated with - * @param password Password to be authenticated with + * @param password Password to be authenticated with. For DIGEST authentication this is the client digest, which + * is passed through unchanged since the digest comparison is performed by the LoginModule + * when it calls back into the Realm * @param nonce Server generated nonce * @param nc Nonce count * @param cnonce Client generated nonce @@ -78,12 +79,7 @@ public class JAASCallbackHandler implements CallbackHandler { String cnonce, String qop, String realmName, String digestA2, String algorithm, String authMethod) { this.realm = realm; this.username = username; - - if (password != null && realm.hasMessageDigest(algorithm)) { - this.password = realm.getCredentialHandler().mutate(password); - } else { - this.password = password; - } + this.password = password; this.nonce = nonce; this.nc = nc; this.cnonce = cnonce; diff --git a/java/org/apache/catalina/realm/JAASRealm.java b/java/org/apache/catalina/realm/JAASRealm.java index 28a0258ba0..40f8d0c8e1 100644 --- a/java/org/apache/catalina/realm/JAASRealm.java +++ b/java/org/apache/catalina/realm/JAASRealm.java @@ -110,10 +110,10 @@ import org.apache.tomcat.util.file.ConfigurationSource; * <li>As part of the login process, JAASRealm registers its own <code>CallbackHandler</code>, called (unsurprisingly) * <code>JAASCallbackHandler</code>. This handler supplies the HTTP requests' username and credentials to the * user-supplied <code>LoginModule</code></li> - * <li>As with other <code>Realm</code> implementations, digested passwords are supported if the - * <code><Realm></code> element in <code>server.xml</code> contains a <code>digest</code> attribute; - * <code>JAASCallbackHandler</code> will digest the password prior to passing it back to the - * <code>LoginModule</code></li> + * <li>As with other <code>Realm</code> implementations, digested passwords are supported via the configured + * <code>CredentialHandler</code>. The credentials are passed to the <code>LoginModule</code> unchanged; any digest + * comparison is expected to be performed by the <code>LoginModule</code> when it calls back into the + * <code>Realm</code></li> * </ul> */ public class JAASRealm extends RealmBase { diff --git a/test/org/apache/catalina/realm/TestJAASRealm.java b/test/org/apache/catalina/realm/TestJAASRealm.java index 49ab665ff9..2ce87e8289 100644 --- a/test/org/apache/catalina/realm/TestJAASRealm.java +++ b/test/org/apache/catalina/realm/TestJAASRealm.java @@ -18,7 +18,10 @@ package org.apache.catalina.realm; import java.io.File; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.security.Principal; +import java.util.Locale; import org.junit.Assert; import org.junit.Test; @@ -26,9 +29,12 @@ import org.junit.Test; import org.apache.catalina.Context; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.HexUtils; public class TestJAASRealm extends TomcatBaseTest { + private static final String ALGORITHM = "MD5"; + private static final String CONFIG = "CustomLogin {\n" + " org.apache.catalina.realm.TesterLoginModule\n" + @@ -115,4 +121,64 @@ public class TestJAASRealm extends TomcatBaseTest { gp.logout(); } + @Test + public void testMemoryLoginModuleDigestAuth() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File tomcatUsersXml = new File(getTemporaryDirectory(), "tomcat-users-lm-digest.xml"); + try (PrintWriter writer = new PrintWriter(tomcatUsersXml)) { + writer.write(TestMemoryRealm.CONFIG); + } + addDeleteOnTearDown(tomcatUsersXml); + + // Write login config to the temp path + File loginConfFile = new File(getTemporaryDirectory(), "memoryLoginConfigDigest.conf"); + try (PrintWriter writer = new PrintWriter(loginConfFile)) { + String path = tomcatUsersXml.getAbsolutePath(); + if (File.separatorChar == '\\') { + path = path.replace("\\", "\\\\"); + } + writer.write(CONFIG_MEMORY.replace("tomcat-users-lm.xml", path)); + } + addDeleteOnTearDown(loginConfFile); + + JAASRealm jaasRealm = new JAASRealm(); + jaasRealm.setAppName("MemoryLogin"); + // Use the same algorithm for the realm credential handler as used for + // the digest authentication + MessageDigestCredentialHandler credentialHandler = new MessageDigestCredentialHandler(); + credentialHandler.setAlgorithm(ALGORITHM); + jaasRealm.setCredentialHandler(credentialHandler); + jaasRealm.setUserClassNames(GenericPrincipal.class.getName()); + jaasRealm.setRoleClassNames(GenericPrincipal.class.getName()); + jaasRealm.setConfigFile(loginConfFile.getAbsolutePath()); + Context context = tomcat.addContext("/jaastest", null); + context.setRealm(jaasRealm); + + tomcat.start(); + + String nonce = "test-nonce"; + String nc = "00000001"; + String cnonce = "test-cnonce"; + String qop = "auth"; + String realmName = "test-realm"; + + MessageDigest md5 = MessageDigest.getInstance(ALGORITHM); + String digestA2 = HexUtils.toHexString(md5.digest("GET:/jaastest".getBytes(StandardCharsets.UTF_8))); + // digestA1 as the client computes it from the stored (clear text in + // the login module's user file) password + String digestA1 = HexUtils.toHexString( + md5.digest(("admin:" + realmName + ":sekr3t").getBytes(StandardCharsets.UTF_8))); + String clientDigest = HexUtils.toHexString(md5.digest( + (digestA1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + digestA2) + .getBytes(StandardCharsets.UTF_8))); + + Principal p = jaasRealm.authenticate("admin", clientDigest.toLowerCase(Locale.ENGLISH), nonce, nc, cnonce, + qop, realmName, digestA2, ALGORITHM); + Assert.assertNotNull(p); + Assert.assertTrue(p instanceof GenericPrincipal); + GenericPrincipal gp = (GenericPrincipal) p; + Assert.assertTrue(gp.hasRole("testrole")); + gp.logout(); + } } diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index c0c9b1aa85..49e6a44fdf 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -1893,6 +1893,16 @@ and changes the signature of the protected <code>getDirectoryContextEnvironment()</code> method. (remm) </fix> + <fix> + Stop pre-digesting the HTTP DIGEST client digest in + <code>JAASCallbackHandler</code>. The pre-digest, a leftover from the + pre-<code>CredentialHandler</code> design, could never match the + deterministic server digest and so made DIGEST authentication via + <code>JAASRealm</code> always fail whenever the realm's credential + handler algorithm matched the digest algorithm. The client digest is + now passed to the <code>LoginModule</code> unchanged, consistent with + the non-JAAS realms. (remm) + </fix> </changelog> </subsection> <subsection name="Coyote"> diff --git a/webapps/docs/realm-howto.xml b/webapps/docs/realm-howto.xml index e1959eafaa..8a73db5870 100644 --- a/webapps/docs/realm-howto.xml +++ b/webapps/docs/realm-howto.xml @@ -1021,9 +1021,11 @@ and restarting the server, without any code changes to your application.</li> security information for an already authenticated user will <strong>not</strong> be reflected until the next time that user logs on again.</li> <li>As with other <code>Realm</code> implementations, digested passwords - are supported if the <code><Realm></code> element in <code>server.xml</code> - contains a <code>digest</code> attribute; JAASRealm's <code>CallbackHandler</code> - will digest the password prior to passing it back to the <code>LoginModule</code></li> + are supported via the configured <code>CredentialHandler</code>. + JAASRealm's <code>CallbackHandler</code> passes the password or + client digest to the <code>LoginModule</code> unchanged; any digest + comparison is expected to be performed by the <code>LoginModule</code> + when it calls back into the <code>Realm</code></li> </ul> </subsection> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
