Issue
I'm using Tomcat 8.5.59 and have the following Realm in my context.xml:
<Realm className="org.apache.catalina.realm.LockOutRealm" >
<Realm className="org.apache.catalina.realm.DataSourceRealm" dataSourceName="jdbc/MyName" localDataSource="true">
<CredentialHandler className="org.apache.catalina.realm.NestedCredentialHandler">
<CredentialHandler className="org.apache.catalina.realm.SecretKeyCredentialHandler" />
<CredentialHandler className="org.apache.catalina.realm.MessageDigestCredentialHandler" algorithm="SHA-512" />
</CredentialHandler>
</Realm>
</Realm>
I would like to get the CredentialHandler in my Java app to hash and store passwords. As per Christopher Schultz's presentation at http://people.apache.org/~schultz/ApacheCon%20NA%202016/Seamless%20Upgrades%20for%20Credential%20Security%20in%20Apache%20Tomcat.pdf I get it like this:
CredentialHandler ch = (CredentialHandler) application.getAttribute(Globals.CREDENTIAL_HANDLER);
The problem is that it returns a blank/default credential handler, not the one I have configured.
If I remove the LockoutRealm definition, it works fine, so it seems that having a nested realm (LockoutRealm, DataSourceRealm), causes it to fail. Looking through the Tomcat code, it seems that the code that calls setAttribute(Globals.CREDENTIAL_HANDLER)
doesn't take CombinedRealm
into account.
How can I get the configured credential handler in my app so that I can call the .matches() and .mutate() methods? I would rather not remove the Lockout Realm because that would compromise security.
Edit:
This use-case is a common one: The application must be able to save the mutated password to the database so that tomcat can do form-based authentication against it when users log in. Every time a new user account is created, the user's password must be mutated and saved to the database.
Additionally, when a user wants to change their password, a form would ask for the current and new passwords - .matches() is used to confirm that the "current" password matches their existing password before changing it to the new password (which again must be mutated).
Using the credential handler defined in the context is important to ensure the password is mutated in the exact way that tomcat requires. The alternative would be for every application to provide their own corresponding libraries, which seems wasteful and error-prone given that tomcat already has them.
This is all described in Christopher Schultz's link at the start of my question.
I think this is all pretty standard and Tomcat provides CredentialHandler ch = (CredentialHandler) application.getAttribute(Globals.CREDENTIAL_HANDLER)
for this purpose. The issue is that the implementation doesn't account for LockoutRealm being used - it assumes that the credential handler is defined directly under the top-level realm, so I'm wondering if it's an oversight/bug in tomcat or if it works like that by design and if there's some way I can access the .mutate() and .matches() functions of the defined CredentialHandler
while also using the LockOutRealm
.
Also, tomcat used to provide RealmBase.Digest(String credentials, String algorithm, String encoding)
for this use-case, but that method is deprecated in 8.5 and removed in 9, so I understand CredentialHandler ch = (CredentialHandler) application.getAttribute(Globals.CREDENTIAL_HANDLER)
is the new way we're supposed to be doing it.
Edit 2:
getAttribute(Globals.CREDENTIAL_HANDLER)
returns a default CredentialHandler
that doesn't encrypt the password at all. The debugger shows the class being StandardContext
. Stepping through the code, that all seems correct, but it doesn't drill into the DataSourceRealm
to get the defined NestedCredentialHandler
, instead, it looks at LockoutRealm
, doesn't find an immediate child CredentialHandler
, so creates and returns a default one. I believe this is as documented in https://tomcat.apache.org/tomcat-8.5-doc/config/credentialhandler.html:
A CredentialHandler element MUST be nested inside a Realm component. If it is not included, a default CredentialHandler will be created using the MessageDigestCredentialHandler.
Solution
Now that the use cases are clear. What you mention in your edits is correct. It starts with the RealmBase class (refer: protected void startInternal() method). This, if teh credential handler is null (which in your case it is on LockoutRealm), sets the default credential handler as MessageDigestCredentialHandler without any algorithm, the behaviour of the credential handler if no algorithm is specified is to use plainText.
Next look at the StandardContext (refer: protected synchronized void startInternal() method, line 5016-5027), lines 5019 and 5024 are important. Here the realm is being retrieved with the 'getRealmInternal()' call, which returns the configured realm, which in your case is LockoutRealm and the subsequent 'getCredentialHandler()' returns the default credential handler as mentioned above (as the method is dispatched to RealmBase, a point to be noted, the CombinedRealm or the LockoutRealm does not override this method).
This is why the password is not being mutated.
What you require looks to be possible. It is apparent from the above that when the 'getCredentialHandler()' is called on the realm, the realm should handle it instead of dispatching to RealmBase.
Therefore, you could do the below:
- Extend LockoutRealm with your realm class (say XRealm)
- Override the getCredentialHandler method
- In the method, access the 'protected final List realms' of CombinedRealm, your DataSourceRealm should be the 0th element
- Call the getCredentialHandler on the DataSourceRealm and return
- When StandardContext calls getCredentialHandler, it should get the NestedCredentialHandler you have configured
The above should solve the problem.
Answered By - Ironluca
Answer Checked By - Marie Seifert (JavaFixing Admin)