Introduction
The idea behind this article was that, although Basic Auth is considered to be extremely unsafe, adding SSL to it fully compensates for most safety concerns. Why is that?
If your backend is SSL secured, data transfer happens through a tunnel which encrypts the whole HTTP request, effectively encrypting also the Basic-Auth authorization header which contains the Base64 encoded username and password. Base64 encoding is no encryption by any means, so any encoded password can easily be decoded again. But if the whole HTTP request is encrypted, decryption can only be done by the WebContainer, because it has access to the keystore where the private key is stored. In more sophisticated IT infrastructures, the TLS/SSL connection might terminate somewhere else, thus forwarding the now decrypted HTTP request further down the company’s intranet until it finally reaches its destination. Many companies don’t accept this because they even don’t trust their own employees. Nevertheless, there are other means of securing data transfer within companies intranets, so Basic Auth might still be an option for user authentication.
In this article I will explain how to secure a Tomcat 8 WebContainer with TLS/SSL, and how to do user authentication within you web application using Basic Auth.
Step 1: Create a keystore and add a self-signed certificate to it
We will not import a commercial certificate here. We just create a JDK keystore and add a self-signed certificate to it. Using a self-signed certificate will cause a certificate error when entering the target web page URL in the browser’s address field. But you can overcome the error if you add a security exception.
This step presents a popular pitfall when the certificate is created and the server URL doesn’t match the name for which the certificate was created. But first things first.
As described in the Tomcat 8 Configuration Guide (see: https://tomcat.apache.org/tomcat-8.0-doc/ssl-howto.html#Configuration)
We now create a new JKS keystore from scratch, containing a single self-signed certificate. To do that, we execute the following from a terminal command line:
Windows:
"%JAVA_HOME%\bin\keytool" -genkey -alias tomcat -keyalg RSA
Unix:
$JAVA_HOME/bin/keytool -genkey -alias tomcat -keyalg RSA
We are now asked a few questions about the organization behind this certificate.
Just follow the instructions outlined on the Tomcat How-To page, but caution: The How-To doesn’t tell you that when asked for the first and last name, the text which you type in decides of the URL the certificate is registered for. So, for example, if you are running Tomcat on your localhost and you don’t have an own domain name (DDNS or ICANN), when you are asked for first and last name, you have to enter “localhost”. As a result, you will be able to open your web app’s main page by using the following URL: https://localhost:8443/<your_context_path>.
The last thing keytool will ask you is to specify is the key password, which is the password specific to this certificate. Rather than enter anything at this prompt, just press ENTER. This will cause keytool to set the key password to the same string value as the keystore password. If these two passwords don’t match, Tomcat cannot access the certificate.
A typical console session after the keytool finished looks like this:
PS C:\Program Files\Java\jdk1.8.0_144\bin> .\keytool -genkey -alias tomcat -keyalg RSA Enter keystore password: Re-enter new password: What is your first and last name? [Unknown]: localhost What is the name of your organizational unit? [Unknown]: Finance What is the name of your organization? [Unknown]: Enron What is the name of your City or Locality? [Unknown]: Houston What is the name of your State or Province? [Unknown]: Texas What is the two-letter country code for this unit? [Unknown]: US Is CN=localhost, OU=Finance, O=Enron, L=Houston, ST=Texas, C=US correct? [no]: yes Enter key password for <tomcat> (RETURN if same as keystore password): PS C:\Program Files\Java\jdk1.8.0_144\bin>
Step 2: Add the SSL connector to Tomcat’s server configuration file “server.xml”
Tomcat’s server configuration file is located in %TOMCAT_HOME%/conf.
Add a new Connector element within the Service element (or reuse an existing one from the comments) and make it look like this:
<Connector protocol="org.apache.coyote.http11.Http11NioProtocol" port="8443" maxThreads="150" scheme="https" secure="true" SSLEnabled="true" keystoreFile="yourKeystoreFileLocation" keystorePass="youKeystorePassword" clientAuth="false" keyAlias="yourAlias" sslProtocol="TLS"/>
This change requires Tomcat to be restarted. After restart, take a look at the console output and make sure that the https procotol handler is installed. The line you are looking for is
INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["https-openssl-nio-8443"]
If this line is not present, and there is no error message concerning the ssl protocol handler, then the modifications to server.xml have not been recognized. Make absolutely sure that the CATALINA_HOME environment variable is set correctly and points to your Tomcat 8 installation. This is particularly important if you have more than one version of Tomcat 8 installed.
You can try whether you have successfully enabled Tomcat 88 SSL security by opening the manager app using https:
https://localhost:8443/manager/html
Good luck !
Step 3: Add ServletSecurity to your WebApp
Now that Tomcat 8 is able to do SSL, we need to enable Basic Authentication. A Realm is (at least in the Tomcat context/nomenklature) a username/password storage and retrieval scheme. Beginners usually use the MemoryRealm where the usernames, passwords and roles are store in the tomcat-users.xml file. This file itself can be a big security hole because it usually contains clear text passwords. For now, we will accept this and continue setting up Basic Auth. Later we will learn how to encrypt the password in tomcat-users.xml by using a Tomcat hashing mechanism.
We will implement Basic Auth security by using a mixture of web.xml and Java code annotations. Although we can put the complete security configuration into web.xml, best practice is to put as much configuration into the Java code at exactly the location you would expect it to be. For example, a developer would expect the security constraint configuration in the corresponding servlet class of the web application. Different servlets can have different security contraints, so instead of coding dozens of contraints into web.xml, each particular constraint is put in the servlet it belongs to.
For Basic Auth, there is still one configuration left that needs to be done in web.xml, and due to this little inconsistency, it is not as intuitive as it could be. Many developers forget about this, so Basic Auth fails. So here is that missing piece that you must put into the web.xml:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <display-name>arduino_recv</display-name> <login-config> <auth-method>BASIC</auth-method> <realm-name>default</realm-name> </login-config> </web-app>
With this little bugger out of the way, let’s take a look at some servlet security annotations:
@ServletSecurity( value = @HttpConstraint( rolesAllowed = { "role1" }), httpMethodConstraints = { @HttpMethodConstraint(value = "GET", rolesAllowed = "role2"), @HttpMethodConstraint(value = "POST", rolesAllowed = { "role3", "role4" })
Lets go quickly through the different contraints. HttpConstraints will be applied to all HTTP protocol methods for which a corresponding HttpMethodConstraint element does NOT occur within the ServletSecurity annotation. As a result, users assigned to “role1” have clearance for all Http-Methods except GET and POST. GET can only be used by users assigned to “role2”, and PUT can only be used by users of “role2” or “role4”.
Let’s take a final look at the most simple variant of @ServletSecurity:
@ServletSecurity(@HttpConstraint( rolesAllowed = "tomcat" ))
Only users assigned to role “tomcat” are allowed for this servlet, and they can use all Http methods.
Here is a full servlet example:
package de.psychomechanics.tutorial; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.HttpConstraint; import javax.servlet.annotation.ServletSecurity; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/myServlet") @ServletSecurity(@HttpConstraint( rolesAllowed = "tomcat" )) public class MyServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public MyServlet() { super(); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.getWriter().append("Served at: ").append(request.getContextPath()); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
Even if we deploy the above web application (Dynamic Web Application <- Eclipse Project) to Tomcat, we are not done yet. Users and roles need to be defined (Step 4) and one entry in server.xml ought to be checked. Stay tuned.
Step 4: Setting up users and roles
Tomcat provides several mechanisms of retrieving user credentials, called realms. More sophisticated realms use databases and different techniques of accessing these databases. Using one of these professional solutions is out of scope of this article. Instead, we go for the MemoryReals which relys on Tomcat’s tomcat-users.xml configuration file. Storing user credentials in a text file is definitely bad practice, but for our purpose it will do. The following example of the tomcat-users.xml file contains the role “tomcat”. This is the corresponding role from the previous step where the @ServletSecurity entry contained the “rolesAllowed” attribute:
<?xml version="1.0" encoding="UTF-8"?> <tomcat-users version="1.0" xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://tomcat.apache.org/xml"> <role rolename="tomcat"/> <role rolename="manager-gui"/> <role rolename="manager-script"/> <role rolename="manager-jmx"/> <role rolename="manager-status"/> <user username="tomcat" password="password" roles="manager-gui,manager-status" /> <user username="user" password="userpsw" roles="manager-script,manager-jmx,tomcat" /> <user username="user1" password="userpsw1" roles="tomcat" /> <user username="user2" password="userpsw2" roles="tomcat" /> </tomcat-users>
Step 5: Adding the MemoryRealm to Tomcat’s server.xml
Remember what I’ve written in Step 3 about the MemoryRealm? We have to check server.xml if the MemoryRealm has been configured. This is usually the default, but we better check. For Basic Auth to work in this article, make sure that the file contains the following entry within the “Engine”-element:
<Realm className="org.apache.catalina.realm.MemoryRealm"/>
Now its time to restart Tomcat for the configuration changes to become effective. After the restart,
Additional step (not required):
Storing passwords in plain text is bad practice. Plaintext passwords stored somewhere on a server is enough reason to fire the security administrator. When attackers break into your network, this is what they are looking for.
Tomcat provides an out of the box solution for this. Tomcat allows to store hashed passwords instead of plaintext passwords. Hashing is irreversible, that is, it works only one way. Therefore it is not possible to retrieve the cleartext password from the password hash. The only thing you can do with a hashed password is comparing it with another hashed string. If it is the same, the originator of the hashed string obviously entered the correct cleartext password before it was hashed.
Enabling password hashing in tomcat requires a few steps.
First we need to configure the hashing algorithm into the Tomcat realm we are going to use. A realm is configured in the server.xml file of the Tomcat server. Tomcat supports MD5, MD2 and SHA hashing algorithms. Given below is a sample configuration in server.xml to enable hashing.
<Realm className="org.apache.catalina.realm.MemoryRealm" digest="md5" />
Now the passwords in tomcat-users.xml can be converted into their hashed value. Tomcat provides a tool which lets you convert the cleartext passwords into hashed passwords. THe following log of my console session shows how I converted the passwords from tomcat-users.xml into their hashed version.
sgoemans@zotac:/var/lib/tomcat8/conf$ cd /usr/share/tomcat8/bin/ sgoemans@zotac:/usr/share/tomcat8/bin$ sh digest.sh -s 0 -a MD5 userpsw userpsw:4421d6f302ab3df20042e5510fdd879c sgoemans@zotac:/usr/share/tomcat8/bin$ sh digest.sh -s 0 -a MD5 userpsw1 userpsw1:f0d2ed5ced2058e7f4430f1615d60f07 sgoemans@zotac:/usr/share/tomcat8/bin$ sh digest.sh -s 0 -a MD5 userpsw2 userpsw2:9c2d202d3549e00c1b3a5436f934b852 sgoemans@zotac:/usr/share/tomcat8/bin$ sh digest.sh -s 0 -a MD5 password password:5f4dcc3b5aa765d61d8327deb882cf99
After replacing the cleartext passwords in tomcat-users.xml with their hashed counterparts, the file now looks like below:
<?xml version="1.0" encoding="UTF-8"?> <tomcat-users version="1.0" xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://tomcat.apache.org/xml"> <role rolename="tomcat"/> <role rolename="manager-gui"/> <role rolename="manager-script"/> <role rolename="manager-jmx"/> <role rolename="manager-status"/> <user username="tomcat" password="5f4dcc3b5aa765d61d8327deb882cf99" roles="manager-gui,manager-status" /> <user username="user" password="4421d6f302ab3df20042e5510fdd879c" roles="manager-script,manager-jmx,tomcat" /> <user username="user1" password="f0d2ed5ced2058e7f4430f1615d60f07" roles="tomcat" /> <user username="user2" password="9c2d202d3549e00c1b3a5436f934b852" roles="tomcat" /> </tomcat-users>
Restart Tomcat again and you are done.