Securing a RESTful JPA WebApp with Tomcat's JDBCRealm and Jersey via HTTP Basic-Auth
posted by fas on 2011-04-02 . Tagged as programming, jersey, tomcat, java, jsr250, hibernate, jee
+----------------------+ +-------------------+ | tomcat_users | | tomcat_groups | +----------------------+ +-------------------+ | username varchar | | username varchar | | passwordhash varchar | | groupname varchar | +----------------------+ +-------------------+
Although one can choose the table and column names freely I had to stick to this database scheme so Tomcat would authenticate users via the JDBCRealm.
This put my in a tight spot, since I already had a Domain Model for Users and Groups defined via JPA Entities and the resulting database scheme looked nothing like JDBCRealm's expectations.
The following simple JPA Entities with a simple m:n association from UserDetails to Group.
@Entity
@Table(name = "cr_users")
public class UserDetails{
@Id
@GeneratedValue
private long id;
private String name;
private String passwordHash;
@ManyToMany
private Set<Group> groups;
}
@Entity
@Table(name = "cr_groups")
public class Group {
@Id
@GeneratedValue
private long id;
private String name;
}
yields a database scheme something like this:
+----------------------+ +-------------------+ +--------------------+ | cr_users | | cr_groups | | cr_users_cr_groups | +----------------------+ +-------------------+ +--------------------+ | id long | | id long | | cr_users_id long | | name varchar | | name varchar | | groups_id long | | passwordhash varchar | +-------------------+ +--------------------+ +----------------------+
So for JDBCRealm to be able to read those entries from the database a View can be created which aggregates the necessary user data in the expected format. In this simple example the following 2 SQL queries can be used to create the View in the database.
create view tc_realm_groups as select cr_users.name as username, groups.name as groupname from cr_users left join ( select cr_users_cr_groups.cr_users_id,cr_groups.name from cr_groups left join cr_users_cr_groups on cr_users_cr_groups.groups_id=cr_groups.id ) as groups on groups.cr_users_id=id; create view tc_realm_users as select name as username from cr_users;
Those two new views in the databse meet the JDBCRealm's required format:
+----------------------+ +-------------------+ | tc_realm_users | | tc_realm_groups | +----------------------+ +-------------------+ | username varchar | | username varchar | | passwordhash varchar | | groupname varchar | +----------------------+ +-------------------+
The requirements for enabling the JDBCRealm are matched by the new view and the server.xml's Realm element can be changed:
<Realm className="org.apache.catalina.realm.JDBCRealm"
driverName="org.postgresql.Driver"
connectionURL="jdbc:postgresql://localhost:5432/mydb"
connectionName="myuser" connectionPassword="mypass"
userTable="tc_realm_users" userNameCol="username" userCredCol="passwordhash"
userRoleTable="tc_realm_groups" roleNameCol="groupname"
digest="sha-256"/>
where userNameCol is the column name under which the username can be found in both tables. After adding some test users being member of a “manager-gui” Group it is possible to test the JDBCRealm by trying to log into Tomcat's management console, which does require an authenticated user that is a member of the group “manager-gui”. When using digest=”sha-256”, JDBCRealm uses a SHA-256 algorithm to create a password hash to check against. This means that the users in the database also have to have a SHA-256 checksum and not a plaintext password, which is generally a good idea.
If JDBCRealm authentication works one can add the security constraints to the web.xml, so Tomcat challenges any anonymous requests and relays the user info into Jersey's SecurityContext, from which the Controller's will aqcuire user data in the application. Also we are redirecting any user from a unsecured to a SSL-encrytped Socket, since BASIC-Auth passwords are transmitted unencrypted with every request.The <auth-constraint> element tells Tomcat which group a user must belong to in order to access the webapp.
<security-constraint> <web-resource-collection> <web-resource-name>my webapp</web-resource-name> <url-pattern>/*</url-pattern> </web-resource-collection> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> <auth-constraint> <role-name>webapp-user</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>BASIC</auth-method> <realm-name>My secured Webapp</realm-name> </login-config>
And finally we will need to tell Tomcat to initalize the Jersey Servlet with the the proper parameters to ensure the “@RolesAllowed” constraints from JSR-250 are considered by the web application:
<servlet> <servlet-name>Jersey Spring Web Application</servlet-name> <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class> <init-param> <param-name>com.sun.jersey.spi.container.ResourceFilters</param-name> <param-value>com.sun.jersey.api.container.filter.RolesAllowedResourceFilterFactory</param-value> </init-param> </servlet>
Tada! Now it's possible to use the javax.security annotations (@RolesAllowed) and have jersey provide the username via a SecurityContext:
@Path("/test")
@Component
public class MyResource {
private static final Logger log = LoggerFactory
.getLogger(MyResource.class);
@GET
@Path("/{id}")
@Produces(MediaType.TEXT_HTML)
@RolesAllowed({"webapp-user"})
public Response getEvent(@Context SecurityContext sc,@PathParam("id") long id) {
log.debug("auth: " + sc.getAuthenticationScheme());
log.debug("user: " + sc.getUserPrincipal().getName()); // the username!
log.debug("admin-privileges: " + sc.isUserInRole("webapp-admin"));
return Response.ok(“auth success”).build();
}
}
The drawbacks of this approach are the tight coupeling to Tomcat as an application server, although configuring a Database Realm should be possible in every Application Server, and the authentication mechanism for the whole Application Server has to be changed, not just a single webapp.
I used the following application stack for this example:
- tomcat 7.0.11
- spring 2.5.6
- jersey 1.6
- hibernate 3.3
- hibernate-annotations 3.4
- jsr250-api 1.0
- postgresql 9.0.3
Thanks go out to to Eric Warriner who's blogentry about JSR-250 and Tomcat has been of great value!
Links,Sources and further reading:
- Eric Warriner's blog entry
- IBM Developerworks: Build a RESTful Web service using Jersey and Apache Tomcat
- Apache Tomcat's Realm Howto
- Wrox - Professional Apache Tomcat 6 by Chopra, Li, Genender
- Eduard Neuwirt's blog entry on SaltAwareJDBCRealm
- Hash passwords securely
Tags: programming, jersey, tomcat, java, jsr250, hibernate, jee
