Last week a colleague of mine called me up because he was struggling with setting up Tomcat JDBCRealm for use with a relatively simple straightforward webapp. Wed recently released the 1.0 version of GateKeeper, and he wanted to use it to manage his users and roles. The GateKeeper side of things went just fine, but the JDBCRealm setup on his webapp was proving very balky.
He fired up VNC, and I logged in and checked out his settings. And there was nothing wrong! At least, nothing that looked odd in any way. We then googled for some help, maybe a simple sample setup, but to no avail! Oddly enough, nowhere on the internet is a good example of using a Tomcat Realm to protect a webapp. There are various examples, but they just show a part of the puzzle, maybe just the
<security -constaint/> section in the web.xml, or a Realm definition:
<realm className="org.apache.catalina.realm.MemoryRealm" />. Reading the servlet spec for web.xml helps, but doesnt give you details on the Tomcat specific details.
So, over the weekend I set out to create a simple example, and I planned on using the jsp-examples app the ships with Tomcat. After poking around jsp-examples I discovered what I had been looking for all along! There is a full example of protecting a webapp using a Realm! The example for some odd reason is NOT linked from the home page of jsp-examples, but it is there. http://localhost:8080/security/protected and log in as user role1, with the password tomcat.
Ill walk you really quickly through the relevant portions of the configuration files. First off you define what URLs you want protected by which roles in your web-apps
<security -constraint> <display -name>Example Security Constraint</display> <web -resource-collection> </web><web -resource-name>Protected Area</web> <!-- Define the context-relative URL(s) to be protected --> <url -pattern>/security/protected/*</url> <!-- If you list http methods, only those methods are protected --> <http -method>DELETE</http> <http -method>GET</http> <http -method>POST</http> <http -method>PUT</http> <auth -constraint> <!-- Anyone with one of the listed roles may access this area --> <role -name>tomcat</role> <role -name>role1</role> </auth> </security> <!-- Default login configuration uses form-based authentication --> <login -config> <auth -method>FORM</auth> <realm -name>Example Form-Based Authentication Area</realm> </login> <!-- Security roles referenced by this web application --> <security -role> <role -name>role1</role> </security> <security -role> <role -name>tomcat</role> </security>
<security -constraint/> stanza specifies the URLs of the pages that we want to protect, as well as what roles are required to allow access. Anything in the
/security/protected/* pattern will require the user has either the role role1 or tomcat.
The next stanza,
<login -config/> specifies how you gain access to the protected URLs. In this example, we are using FORM based security, so we specify the URLs for the login/logout pages. If you are using BASIC authentication then your stanza is much simpler:
<login -config> <auth -method>BASIC</auth> <realm -name>Example BASIC Authentication Area</realm> </login>
The remaining stanzas,
<security -role/> are the roles that are used in this webapp, and they are elements that are often forgotten in a web.xml.
Now that we have defined that only users who have roles role1 or tomcat can access the application, we then need to configure Tomcat. This is done through server.xml, and can be a bit of a rat hole to figure everything out. So, extracted from server.xml we have the following XML stanzas. Ive removed most of the commenting so you can see the overall structure of the document:
<server port="8005" shutdown="SHUTDOWN"> <!-- Global JNDI resources --> <globalnamingresources> <!-- Editable user database that can also be used by<br /> UserDatabaseRealm to authenticate users --> <resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml" /> </globalnamingresources> <!-- Define the Tomcat Stand-Alone Service --> <service name="Catalina"> <!-- Define a non-SSL HTTP/1.1 Connector on port 8080 --> <connector port="8080" maxHttpHeaderSize="8192" maxThreads="150" minSpareThreads="25" maxSpareThreads="75" enableLookups="false" redirectPort="8443" acceptCount="100" connectionTimeout="20000" disableUploadTimeout="true" /> <!-- Define the top level container in our container hierarchy --> <engine name="Catalina" defaultHost="localhost"> <!-- Because this Realm is here, an instance will be shared globally --> <!-- This Realm uses the UserDatabase configured in the global JNDI<br /> resources under the key "UserDatabase". Any edits<br /> that are performed against this UserDatabase are immediately<br /> available for use by the Realm. --> <realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> <host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> </host> </engine> </service> </server>
Now, instead of going from top to bottom, well start from the definition for the specific host, and work our way out. The challenge that my colleague and I had was that we thought we needed to somehow map a
<security -constraint/> in the web.xml to some sort of definition in server.xml. We were mucking around with all sorts of
<realm /> stanzas embedded in
<host /> stanzas and trying to match
<realm -name/> in the web.xml to the
<realm name=XX/> element in server.xml. What we didnt realize until I found the jsp-examples sample security code is that because the realm is defined inside of the
<engine /> stanza, it applies to all hosts! And that there is NO explicit mapping between webapps and realms at a level more detailed then at an Engine level. All webapps defined in all hosts that share the same engine share the same realm! So the complete stanza looks like:
<engine name="Catalina" defaultHost="localhost"> <realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> <host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> </host> </engine>
Now, I dont know what you would do if you wanted to use different Realms for each webapp, maybe define multiple Engines I guess. Our UserDatabaseRealm is not fully defined here, instead it refers to a global resource called UserDatabase. That is defined outside in our
<globalnamingresources /> stanza:
<globalnamingresources> <!-- Editable user database that can also be used by<br /> UserDatabaseRealm to authenticate users --> <resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml" /> </globalnamingresources>
Weve defined a resource call UserDatabase that reads in data in the conf/tomcat-users.xml file and exposes it as a user database.
Hopefully this will walk you through all the setup steps required for adding a Realm to your webapp. For me, the key insight was that the realm definition inside of the engine definition applies to all the web-apps in all the hosts. I am working on cutting our 1.0.1 version of Gatekeeper which will demo using a JDBCRealm. I will post a follow up on that.