Wednesday, May 6, 2009

JAX-WS webservice with client certificate auth

This post is not finished yet. But it will be eventually - have to log off now ( 06.may.2009 ) .

The goal


Running a JAX-WS SOAP webservice on Glassfish using client certificates for authentication. The client certificates will be issued by self appointed Certificate Authority (CA). We will develop sample clients for the webservice with Java and PHP (CURL). The CURL client should be easily portable to other platforms (Python, Perl, ...) as well.

The parts
1. Server key/certificate
2. CA key/certificate
3. Client keys/certificates
4. A JAX-WS webservice running on Glassfish V2.1 server
5. PHP CURL client
6. Java SOAP client

The process
1. User generates a private key and a Certificate request (CSR)
2. Provider signs the CSR and provides the user with a certificate
3. User connects to the webservice using his "client certificate" and private key

This article consists of 3 parts (each as a separate posting):
I. Part 1: Setting up the webservice
II. Part 2: The PHP-CURL client
III. Part 3: The Java client

Resources
1. OpenSSL Keytool Cheatsheet
2.

JAX-WS Part 2: PHP-CURL sample client

I'll keep the PHP code minimalistic and will not be creating a SOAP client with PHP. Instead I'll just post a ready-made SOAP xml using CURL and my client certificat/key pair. So the PHP code looks like this:


<?php

// connect to server with client cert and private key
if ($ch = curl_init()){

curl_setopt($ch, CURLOPT_URL, "https://localhost:8181/ExampleWS/Example");
curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 1);
curl_setopt($ch, 'CURLOPT_SSLCERTTYPE', 'PEM');
curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLCERT, 'democompany1.crt');
curl_setopt($ch, CURLOPT_SSLCERTPASSWD, 'changeit');
curl_setopt($ch, CURLOPT_SSLKEY, 'democompany1.key');
curl_setopt($ch, CURLOPT_SSLKEYPASSWD, 'changeit');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_CAINFO, NULL);
curl_setopt($ch, CURLOPT_CAPATH, NULL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml', 'SOAPAction: ""') );
curl_setopt($ch, CURLOPT_POST, 1);
$soap_message = <<<SOAP
<?xml version="1.0" encoding="utf-8" ?>
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<env:Body>
<ns2:sampleEcho xmlns:ns2=""><sampleParamStr>Hello World!</sampleParamStr></ns2: sampleEcho >
</env:Body>
</env:Envelope>
SOAP;
curl_setopt($ch, CURLOPT_POSTFIELDS, $soap_message);
echo "Connecting\n";
$data = curl_exec($ch);
if ($data) {
echo "DATA: ".$data;
} else {
echo curl_error($ch);
}
echo "\nDone\n";

}


?>


You can run this with PHP-CLI

php curlsample.php

Tuesday, May 5, 2009

JAX-WS Part 1: setting up the webservice

Setting up CA and generating the CA cert
The CA is set up according to these instructions .

Generating the user key/CSR with OpenSSL
You can generate the CSR with this OpenSSL commands:

openssl req -new -nodes -keyout democompany1.key -out democompany1.csr -days 365

Generating the user key/CSR with keytool
When using a Java client for the webservice you probably want to use all the JSSE stack. So you are practically binded to the JKS keystore implementation. JKS keystore management is accomplished with "keytool" from command line. One keytool quirk is that you can not export/import private keys from/to a JKS keystore with the "keytool" command. So you must use Java code directly to manage your keys in a keystore or accept that restriction. Thus when using a Java client we need to generate our private key inside a JKS keystore from the beginning. You can generate the private key and the CSR with these keytool commands:

keytool -genkey -alias democompany2 -keyalg RSA -keystore democompany2_keystore.jks
keytool -certreq -alias democompany2 -keyalg RSA -file democompany2.csr -keystore democompany2_keystore.jks

Signing the user CSR
With the CA configured you can sign a CSR request with this command:

openssl ca -config openssl.my.cnf -policy policy_anything -out certs/democompany2.crt -infiles democompany2.csr

Configuring server: Glassfish domain.xml
The default Glassfish V2.1 server configuration does not have the truststore password properly configured. It is probably intentional since you should change the truststore pass for all the systems you run Glassfish on anyway. So to make things work you have to tell Glassfish it's truststore password by adding this to the JVM commands in domain.xml:

<jvm-options>-Djavax.net.ssl.trustStorePassword=changeit</jvm-options>

You also have to configure Glassfish auth realm so that it knows wich groups you want to authenticate with user certificates. So do this in your domain.xml :

(existing) <auth-realm classname="com.sun.enterprise.security.auth.realm.certificate.CertificateRealm" name="certificate">
(add this) <property name="assign-groups" value="TESTGROUP"/>
(existing) </auth-realm>

Configuring server: Glassfish truststore
You have to import the CA cert to your glassfish truststore so that the server will trust your signed certificates. You can do this with the keytool like this:

keytool -import -v -alias myCA -file certs/myca.crt -storepass changeit -keystore /Users/erkulas/apps/glassfish-v2.1-b60e/domains/domain1/config/cacerts.jks

Running server: an JAX-WS example webservice
This is a sample webservice configuration.
application.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE application PUBLIC "-//Sun Microsystems, Inc.//DTD J2EE Application 1.3//EN" "http://java.sun.com/dtd/application_1_3.dtd">
<application>
<display-name>security-Example-sslApp</display-name>
<module>
<web>
<web-uri>Esample-web.war</web-uri>
<context-root>ExampleWS</context-root>
</web>
</module>
</application>

sun-application.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sun-application PUBLIC "-//Sun Microsystems, Inc.//DTD Sun ONE Application Server 8.0 J2EE Application 1.4//EN" "http://www.sun.com/software/sunone/appserver/dtds/sun-application_1_4-0.dtd">
<sun-application>

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

</sun-application>

web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:j2ee="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.5"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

<servlet>

<servlet-name>Example</servlet-name>
<servlet-class>ws.Example</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Example</servlet-name>
<url-pattern>/Example</url-pattern>
</servlet-mapping>

<login-config>
<auth-method>TESTGROUP</auth-method>
</login-config>

<security-role>
<role-name>TESTGROUP</role-name>
</security-role>

<security-constraint>
<web-resource-collection>
<web-resource-name>Secure Area</web-resource-name>
<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>
<role-name>TESTGROUP</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>

</web-app>


ws.Example.java:

package ws;

import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.jws.WebParam;
import javax.xml.ws.WebServiceContext;

@javax.jws.WebService(targetNamespace = "")
public class Example {

@Resource
WebServiceContext wsContext;

@WebMethod
public String sampleEcho(@WebParam(name = "sampleParamStr", targetNamespace = "") String sampleParamStr ) throws java.lang.Exception
{
// sample of getting the principal (cert data) from messagecontext
MessageContext mx = wsContext.getMessageContext();
Subject subb = (Subject)( mx.get("CLIENT_SUBJECT") );
Object[] principals = subb.getPrincipals().toArray();
Principal principal = (Principal)principals[1];
String name = principal.getName();
// return input string as a result
ret = sampleParamStr;
return ret;
}
}

Tuesday, April 7, 2009

java.sql.PreparedStatement with named params using static class

I have been programming a system wich uses exclusively JDBC queries. No ORM tools are used. All of the SQL queries are with java.sql.PreparedStatment using "?" (question marks) for data insertion. The resons for this are not the topic of this post. Since I am in this environment and rely wanted named parameters in my SQL queries I just went out and looked for these on the web. I found this.

Now this requires to make a wrapper to java.sql.PreparedStatement. But the system is quite large and it is better for me to make static calls only. So I coded my own static methods for achiving the same goal. The code is found at the bottom of this post and you can use it like this:


Connection con = ds.getConnection("xxx");
String sql_p = "SELECT * FROM table WHERE table.ID=:sid ORDER BY ID DESC ";
// if you don't have null values in params list then make hashmap with param values
HashMap param_map = new HashMap(); param_map.put("sid", 1);
// if you might have null values in params list then make hashmap like this
HashMap param_map = new HashMap(); param_map.put("sid", new Object[] {null, java.sql.Types.INTEGER});
// make indexed query (with ? markers)
String sql_i = SQLHelper.namedParamParseSQL(sql_p, param_map);
PreparedStatement stmt = con.prepareStatement(sql_i);
// assign statement params
stmt = SQLHelper.namedParamAssign(stmt, param_map, sql_p);


The SQLHelper class can be found here:

package util;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;

import ee.eschool.exceptions.DWRException;

/**
* SQL Helper functions
*
* 1. Static class for named params in JDBC SQL.
* Original code here: http://www.javaworld.com/javaworld/jw-04-2007/jw-04-jdbc.html?page=1
* Usage instructions: http://imlostinit.blogspot.com/2009/04/javasqlpreparedstatement-with-named.html
* Usage sample:
* Connection con = ds.getConnection("xxx");
* String sql_p = "SELECT * FROM table WHERE table.ID=:sid ORDER BY ID DESC ";
* // if you don't have null values in params list then make hashmap with param values
* HashMap param_map = new HashMap(); param_map.put("sid", 1);
* // if you might have null values in params list then make hashmap like this
* HashMap param_map = new HashMap(); param_map.put("sid", new Object[] {null, java.sql.Types.INTEGER});
* // make indexed query (with ? markers)
* String sql_i = SQLHelper.namedParamParseSQL(sql_p, param_map);
* PreparedStatement stmt = con.prepareStatement(sql_i);
* // assign statement params
* stmt = SQLHelper.namedParamAssign(stmt, param_map, sql_p);
*
* @author erkulas
*
*/
public class SQLHelper {

/**
* Parse SQL query with named params and return SQL query with indexes .
*
* @param sql_query SQL query string with named parameters (like :param )
* @return parsed SQL query with indexes ( ? ) in the place of params ( :param )
*/
public static String namedParamParseSQL(String sql_query) {
int length=sql_query.length();
StringBuffer parsedQuery=new StringBuffer(length);
boolean inSingleQuote=false;
boolean inDoubleQuote=false;
for(int i=0;i<length;i++) {
char c=sql_query.charAt(i);
if(inSingleQuote) {
if(c=='\'') {
inSingleQuote=false;
}
} else if(inDoubleQuote) {
if(c=='"') {
inDoubleQuote=false;
}
} else {
if(c=='\'') {
inSingleQuote=true;
} else if(c=='"') {
inDoubleQuote=true;
} else if(c==':' && i+1<length && Character.isJavaIdentifierStart(sql_query.charAt(i+1))) {
int j=i+2;
while(j<length && Character.isJavaIdentifierPart(sql_query.charAt(j))) { j++; }
String name=sql_query.substring(i+1,j);
c='?'; // replace the parameter with a question mark
i+=name.length(); // skip past the end of the parameter
}
}
parsedQuery.append(c);
}
return parsedQuery.toString();
}

/**
* Parses sql_query and returns a list of params with "param_idx" value indicating the param order in SQL.
*
* @param sql_query SQL query string with named parameters (like :param )
* @param param_map HashMap of parameter values
* @return List of HashMaps with indexes according to their order in the sql_query
*/
public static List<HashMap&rt; namedParamGetIdx(String sql_query, HashMap param_map) throws SQLException {
List<HashMap&rt; indexList = new ArrayList();
int length=sql_query.length();
StringBuffer parsedQuery=new StringBuffer(length);
boolean inSingleQuote=false;
boolean inDoubleQuote=false;
int index=1;
Object sqltype, val;
for(int i=0;i<length;i++) {
char c=sql_query.charAt(i);
if(inSingleQuote) {
if(c=='\'') {
inSingleQuote=false;
}
} else if(inDoubleQuote) {
if(c=='"') {
inDoubleQuote=false;
}
} else {
if(c=='\'') {
inSingleQuote=true;
} else if(c=='"') {
inDoubleQuote=true;
} else if(c==':' && i+1<length && Character.isJavaIdentifierStart(sql_query.charAt(i+1))) {
int j=i+2;
while(j<length && Character.isJavaIdentifierPart(sql_query.charAt(j))) { j++; }
String name=sql_query.substring(i+1,j);
c='?'; // replace the parameter with a question mark
i+=name.length(); // skip past the end of the parameter
if(param_map.get(name)!=null) {
if(param_map.get(name) instanceof Object[]) { val = ((Object[])param_map.get(name))[0]; } else { val = param_map.get(name); }
HashMap hm = new HashMap(); hm.put("param_name", name); hm.put("param_val", val); hm.put("param_idx", index);
sqltype=java.sql.Types.VARCHAR;
if((param_map.get(name) instanceof Object[]) && (((Object[])param_map.get(name))[1]!=null)) { sqltype=((Object[])param_map.get(name))[1]; }
else if(val==null) { throw new SQLException("Param null and no SQLType specified."); }
hm.put("param_sqltype", sqltype);
indexList.add(hm);
index++;
}
else { throw new SQLException("Param "+name+" not found in mapping."); }
}
}
}
return indexList;
}

/**
* Assign PreparedStatement with the values from param_map according to their order in sql_p .
*
* @param stmt The PreparedStatement instance to be used.
* @param param_map An HashMap with parameter values.
* @param sql_p SQL query with named parameters ( :param )
* @return Returns the PreparedStatement with the params assigned.
*/
public static PreparedStatement namedParamAssign(PreparedStatement stmt, HashMap param_map, String sql_p) throws SQLException {
List<HashMap&rt; param_idx = namedParamGetIdx(sql_p, param_map);
return namedParamAssign(stmt, param_idx);
}

/**
* Assign PreparedStatement with the values from param_map according to their order in sql_p .
*
* @param stmt The PreparedStatement instance to be used.
* @param statement_idx_list List of params .
* @return Returns the PreparedStatement with the params assigned.
*/
public static PreparedStatement namedParamAssign(PreparedStatement stmt, List<HashMap&rt; statement_idx_list) {
for(Integer c=0; c<statement_idx_list.size(); c=c+1) {
Object val=statement_idx_list.get(c).get("param_val");
Integer idx=(Integer)statement_idx_list.get(c).get("param_idx");
Integer sqltype=(Integer)statement_idx_list.get(c).get("param_sqltype");
try {
if(val==null) { stmt.setNull(idx, sqltype); }
else if(val instanceof String) { stmt.setString(idx, (String)val); }
else if(val instanceof Integer) { stmt.setInt(idx, (Integer)val); }
else if(val instanceof Long) { stmt.setLong(idx, (Long)val); }
else if(val instanceof Boolean) { stmt.setBoolean(idx, (Boolean)val); }
else if(val instanceof java.util.Date) { stmt.setDate(idx, new java.sql.Date( ((java.util.Date)val).getTime() ) ); }
else if(val instanceof java.sql.Date) { stmt.setDate(idx, (java.sql.Date)val); }
} catch (SQLException e) { e.printStackTrace(); }
}
return stmt;
}


}

Saturday, February 28, 2009

JavaRebel Licensing change

I've been trying out JavaRebel for some time now. It works like a charm if you take into account its features/limitations while developing.

Now Zeroturnaround is changing it's pricing/licecing model . Until March 9 2009 you can still buy perpetual licences for $100. Those licenses will still be in effect after that date but you can not buy new perpetual licenses after that date. They will only offer annual licenses.

Since I still had'nt bought JavaRebel I just went out and did it. Now I am a owner of JavaRebel perpetual license and just hoping that Zeroturnaround will not do some kind of draconian licensing stunt. The perpetual license does include updates but they could just release a new product instead.


PS: Like me, Zeroturnaround is of Estonian origin. On pure technical merits I would call the innovation of JavaRebel on par with Skype . This is just an amazing product that the big ones (Sun/IBM/...) have omitted.

Saturday, February 21, 2009

Struts 2 Ajax with JSON plugin

Heurekaaa!

I've managed to get the Struts 2 JSON plugin working. Surprisingly the thing works out of the box. Just initialize all of the stuff that you have there in the howto and you have Ajax with JSON data-transport. I even managed to integrate the example right into my application.

Maybe I'll be more verbose on the subject next post but i'ts 2 AM and I've earned my sleep for today.


PS: While researching the subject I also found this very recent presentation about JSON/Bayeoux/Comet generally and the Weblogic "Comet" API.

Thursday, February 12, 2009

Copying Hibernate objects

Today I had to copy my data in a MySQL database. My application uses Spring/Hibernate. Basically I had some rows in a database in multiple tables with relations and needed to make a copy of those with some of the data modified in the process.

Well I am an "ex" PHP guy, so ofcourse the most straightforward way should be the correct way - shouldn't it! Lets see. Our application uses Hibernate with POJO-s and manages all the data on object level. And I don't want to designate all the fields of a row (=properties of the object) one by one when copying. So I should just:
1. query the object form DB
2. give the object a new identity (reset the ID-s of that object with new ones and/or clone the object ) .
3. modify the new object according to our requirements (assign new relations, set some relations null, ...)
4. insert the object back to DB
Should be easy ... well not quite.

Hibernate keeps track of my POJO-s in a draconian CIA way, not a straightforward PHP way :). When I nullify the ID-s Hibernate just changes the existing object and since it is the same object the DB will be UPDATE when executing em.merge() or em.persist() . So I relly needed to eather clone the object using the "cloneable" interface or detach the object and modify the detached object. I could detach the object by serializing it or closing the EntityManager session but both methods are a bit too cumbersome to me for this simple task. I just need a simple way to copy database rows, something like this:

Object new_obj=copy(old_obj); em.persist(new_obj); .

So here comes the Hibernate3BeanReplicator .It does all the dirty work and still leaves me in control of the copying process. Like this:


Hibernate3BeanReplicator r = new Hibernate3BeanReplicator(null, null, null);
Object o_obj_new = r.copy(o_obj);
o_obj_new.setSomeRelation(null);
o_obj_new.setSomeIntField(0);
o_obj_new.setID(null); // we need to nullify the ID so that Hibernate will treat the result as a new object not a detached instance of an existin object
em.persist(o_obj_new);