users@jersey.java.net

client certificate based authentication and accounting with Jersey on GlassFish HOWTO

From: Gabor Szokoli <szocske_at_gmail.com>
Date: Mon, 5 May 2008 15:10:51 +0200

Hi,

My jersey web service prototype has come full circle, so here's a
rough rundown on how I did it. This is of course a prototype only,
useful for getting some hands on experience with the technology. To
get any meaningful security out of it, you will have to invest some
time into figuring out what's what, how to keep private keys secret,
establish a human-in-the-loop protocol for signing certs, etc. Anyway,
here it goes:

Objective: a simple REST-style read-only web service using SSL
certificates to authenticate clients and keep an audit log of who
accessed what when.

Design choices: Jersey as a Servlet on Glassfish, using java ee
servlet authentication, but no audit module.

Steps:

1, Create the web service .war file

Implement your resources and converters as normal, or use the
HelloWorld example.

2, Extend the web.xml and sun-web.xml, configure Glassfish

2.1,
Here's my web.xml in full. Only login-config, security-role and
security constraint are relevant.

<web-app>
        <servlet>
                <servlet-name>Jersey Web Application</servlet-name>
                <servlet-class>
                        com.sun.ws.rest.spi.container.servlet.ServletContainer
                </servlet-class>
                <init-param>
                        <param-name>webresourceclass</param-name>
                        <param-value>my.package.name.RootResources</param-value>
                </init-param>

                <load-on-startup>1</load-on-startup>
        </servlet>

        <servlet-mapping>
                <servlet-name>Jersey Web Application</servlet-name>
                <url-pattern>/*</url-pattern>
        </servlet-mapping>

        <persistence-context-ref>
                <persistence-context-ref-name>
                        persistence/CCFEntityManager
                </persistence-context-ref-name>
                <persistence-unit-name>ccf-pu</persistence-unit-name>
        </persistence-context-ref>

        <login-config>
                <auth-method>CLIENT-CERT</auth-method>
        </login-config>

        <security-role>
                <description />
                <role-name>authorized</role-name>
        </security-role>


        <security-constraint>
                <display-name>CCF-REST</display-name>
                <web-resource-collection>
                        <web-resource-name>CCF_REST</web-resource-name>
                        <description></description>
                        <url-pattern>/</url-pattern>
                        <http-method>GET</http-method>
                        <http-method>POST</http-method>
                        <http-method>HEAD</http-method>
                        <http-method>PUT</http-method>
                        <http-method>OPTIONS</http-method>
                        <http-method>TRACE</http-method>
                        <http-method>DELETE</http-method>
                </web-resource-collection>
                <auth-constraint>
                        <description/>
                        <role-name>authorized</role-name>
                </auth-constraint>
                <user-data-constraint>
                        <description/>
                        <transport-guarantee>CONFIDENTIAL</transport-guarantee>
                </user-data-constraint>
        </security-constraint>

</web-app>

2.2,

And here is the corresponding sun-web.xml which defines the
"authorized" role (it is not a keyword. "CLIENT-CERT" and
"CONFIDENTAL" are keywords. "CCF" is the name of my application.)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-web-app PUBLIC "-//Sun Microsystems, Inc.//DTD
Application Server 9.0 Servlet 2.5//EN"
"http://www.sun.com/software/appserver/dtds/sun-web-app_2_5-0.dtd">
<sun-web-app error-url="">
        <context-root>CCF-REST</context-root>
        <class-loader delegate="true" />

        <security-role-mapping>
                <role-name>authorized</role-name>
                <group-name>authorized</group-name>
        </security-role-mapping>

</sun-web-app>

2.3,
To complete this step, configure the glassfish certificate realm to
admint valid certificate holding clients into the "authorized" group.
(still not a keyword.)
Either in your domain.xml directly, or via the web admin console or asadmin:
 <auth-realm classname="com.sun.enterprise.security.auth.realm.certificate.CertificateRealm"
name="certificate">
       <property name="assign-groups" value="authorized"/>
</auth-realm>

You should be unable to access the web service at this point.

3, Create the client certificate, register the CA cert as trusted.

As registering a new trusted client certifice into the truststore
requires a Glassfish restart, we register a trusted Certificate
Authority (CA) certificate instead, once and for all. Then each new
client can get its certificate from the trusted CA, and become
"transitively" trusted.
To be exact, it is normaly the client who creates a public-private
keypair, then submits the public part in the form of a Certificate
Signing Request to the Certificate Authority, who then creates the
certificate by signing it with its own private key. This certifies
that tha CA trusts the certificate holder to be who he claims to be.
Confusing at first, but rather straight forward once you get the hang
of Public Key Infrastructure.

3.1, Establish a CA (assuming you don't have one already):
source:
http://www.madboa.com/geek/openssl/

openssl req \
 -x509 -nodes -days 365 \
 -newkey rsa:1024 -keyout cakey.pem -out cacert.pem

Edit openssl.cnf, create directories and files referenced there:
mkdir demoCA
echo '100001' > ./demoCA/serial
mkdir ./demoCA/newcerts
mkdir demoCA/private
cp cacert.pem demoCA/
cp cakey.pem demoCA/private/

Enable signing foreign keys in openssl.cnf:

policy = policy_anything


3.2, Sign the certificate of the client key:

Create client key pair and certification request. This is normaly done
by the client. The information provided here becomes part of the
certificate, and will be included in the audit logs as "user
principal":

openssl genrsa -out somekey.pem 2048
openssl req -new -key somekey.pem -out somekey.csr

sign (done by the CA):
openssl ca -config openssl.cnf -in somekey.csr -out somekey.crt

3.4, package key and cert for browser consumption:
openssl pkcs12 -export -out somekey.pfx -in somekey.crt -inkey
somekey.pem -name "Some Cert"

3.5, Add own CA cert to truststore:

keytool -import -v -keystore
/opt/glassfish/domains/domain1/config/cacerts.jks -storepass changeit
-alias localCA -file cacert.pem

Restart Glassfish.
Importing somekey.pfx into a browser should enable it to access your
web service again at this point.

4, Extend @GET methods with audit logging

I have decided against using a custom Glassfish Audit module because I
was unsure how I could automate shipping and deploying it. Please note
however that since the application is RESTful, the user principal
together with the URL accessed are sufficient for an audit trail.

You need URIInfo and SecurityContext injected into your resource class:

@Context private UriInfo context;
        
@Context SecurityContext security;

Then each @GET method can call this audit method:

        protected void doAudit() {
                StringBuffer auditMessage = new StringBuffer("AUDIT LOG for GET method. ");
                auditMessage.append("USER: ");
                if (security != null && security.getUserPrincipal() != null) {
                        auditMessage.append(security.getUserPrincipal().toString());
                } else {
                        logger.info("SecurityContext or UserPrincipal was null.");
                        auditMessage.append("UNKNOWN.");
                }
                auditMessage.append(" URL: ");
                if (context !=null && context.getRequestUri() != null) {
                        auditMessage.append( context.getRequestUri().toString());
                } else {
                        logger.info("URIContext or RequestUri was null.");
                        auditMessage.append("UNKNOWN");
                }
                logger.info(auditMessage.toString());
        }

This is totally permissive of course, depending on your domain you
probably don't want to allow the client to access anything unless the
audit trail is fully descriptive. Keeping the audit entries in a
database instead of intermixed with regular logs is also recommended.


Share and enjoy!
(Public domain, use or reproduce in any way you want)

Gabor Szokoli
Sirius Cybernetics Corporation Complaints division