I'm writing a java web application which need to perform login through a webservice. Of course, none of the realms supplied with the application server I'm using (glassfish v2) can do the trick. I therefore had to write my own. It seems however, that the realm implementation that I wrote is completely tied to glassfish and cannot be used as is in any other application servers.
Is there any standard or widely supported way to implement a custom Realm? Is it in any way possible to deploy that realm from a .war, or does it always need to be loaded from the server's own classpath?
NOTE: The answer below is only valid for Java EE 5. As was brought to my attention in one of the other answers, Java EE 6 does support this. So if you're using Java EE 6, don't read this answer, but read the other relevant answer.
From my own research and from the responses to this question the answer I found is the following: Although JAAS is a standard interface, there is no uniform way to write, deploy and integrate a JAAS Realm + LoginModule in various application servers.
Glassfish v2 requires you to extend some of its own internal classes that implement LoginModule or Realm themselves. You can however not customize the whole login process because many methods of the LoginModule interface are marked final in Glassfish's superclass. The custom LoginModule and Realm classes must be placed in the AS classpath (not the application's), and the realm must be manually registered (no deployment from the .war possible).
The situation seems to be a bit better for Tomcat, which will let you code your own Realm and LoginModule completely, and then configure them into the application server using its own JAASRealm (which will delegate the actual work to your implementations of Realm and LoginModule). However, even tomcat doesn't allow deploying your custom realm from your .war.
Note that none of the Application Servers that showed up my results seem to be able to take full advantage of all the JAAS Callbacks. All of them seem to only support the basic username+password scheme. If you need anything more complicated than that, then you will need to find a solution that is not managed by your Java EE container.
For reference, and because it was asked for in the comments to my question, here is the code I wrote for GlassfishV2.
First of all, here's the Realm implementation:
public class WebserviceRealm extends AppservRealm {
private static final Logger log = Logger.getLogger(WebserviceRealm.class.getName());
private String jaasCtxName;
private String hostName;
private int port;
private String uri;
@Override
protected void init(Properties props) throws BadRealmException, NoSuchRealmException {
_logger.info("My Webservice Realm : init()");
// read the configuration properties from the user-supplied properties,
// use reasonable default values if not present
this.jaasCtxName = props.getProperty("jaas-context", "myWebserviceRealm");
this.hostName = props.getProperty("hostName", "localhost");
this.uri = props.getProperty("uri", "/myws/EPS");
this.port = 8181;
String configPort = props.getProperty("port");
if(configPort != null){
try{
this.port = Integer.parseInt(configPort);
}catch(NumberFormatException nfe){
log.warning("Illegal port number: " + configPort + ", using default port (8181) instead");
}
}
}
@Override
public String getJAASContext() {
return jaasCtxName;
}
public Enumeration getGroupNames(String string) throws InvalidOperationException, NoSuchUserException {
List groupNames = new LinkedList();
return (Enumeration) groupNames;
}
public String getAuthType() {
return "My Webservice Realm";
}
public String getHostName() {
return hostName;
}
public int getPort() {
return port;
}
public String getUri() {
return uri;
}
}
And then the LoginModule implementation:
public class WebserviceLoginModule extends AppservPasswordLoginModule {
// all variables starting with _ are supplied by the superclass, and must be filled
// in appropriately
@Override
protected void authenticateUser() throws LoginException {
if (_username == null || _password == null) {
throw new LoginException("username and password cannot be null");
}
String[] groups = this.getWebserviceClient().login(_username, _password);
// must be called as last operation of the login method
this.commitUserAuthentication(groups);
}
@Override
public boolean commit() throws LoginException {
if (!_succeeded) {
return false;
}
// fetch some more information through the webservice...
return super.commit();
}
private WebserviceClient getWebserviceClient(){
return theWebserviceClient;
}
}
finally, in the Realm has to be tied to the LoginModule. This is done at the JAAS configuration file level, which in glassfish v2 lies at yourDomain/config/login.conf. Add the following lines at the end of that file:
myWebserviceRealm { // use whatever String is returned from you realm's getJAASContext() method
my.auth.login.WebserviceLoginModule required;
};
This is what got things working for me on glassfish. Again, this solution is not portable across application servers, but as far as I can tell, there is no existing portable solution.
Is there any standard or widely supported way to implement a custom
Realm? Is it in any way possible to deploy that realm from a .war, or
does it always need to be loaded from the server's own classpath?
There absolutely is a standard way to implement a custom Realm, or in more general terms a custom authentication module. This can be done via the JASPIC/JASPI/JSR 196 SPI/API. JASPIC is a standard part of any full Java EE 6 implementation, but unfortunately not part of the Java EE 6 Web Profile.
However, despite JASPIC being a part of Java EE 6, it's not being optimally supported by vendors. GlassFish and WebLogic seem to have very good implementations, JBoss AS and Geronimo are a bit more problematic. The lead engineer from JBoss on this topic (Anil Saldhana) has even stated that he refuses to activate JASPIC by default for the moment. A few of the most severe bugs in Jboss AS 7.1 have been recently fixed, but as there are no public releases of JBoss 7.1.x scheduled anymore and JBoss AS 7.2 is still some time away it means as of now at least on JBoss JASPIC is troublesome.
Another unfortunate issue is that the actual authentication module may be standardized, but there's no declarative way (read XML file) to configure it that's standardized.
Is it in any way possible to deploy that realm from a .war, or does it
always need to be loaded from the server's own classpath?
With JASPIC, the authentication module ('realm') can indeed be loaded from a .war. I'm not 100% sure whether this is guaranteed by the spec, but of the 4 servers I tested (GlassFish, WebLogic, Geronimo and JBoss AS), they all supported this. Geronimo unfortunately has some kind of race condition in its programmatic registration, so you need an ugly workaround by doing a hot deploy twice, but it in the end if does load the module from the .war.
As of the proprietary mechanisms, at least JBoss AS has always supported loading the module (e.g. a subclass of org.jboss.security.auth.spi.AbstractServerLoginModule) from the .war or .ear.
I wrote a blog post about this topic recently that has some more details.
You can never deploy a realm from a WAR because the realm is NOT an Application Artifact, it is a Container Artifact (thus the phrase "container based security"). You can configure your app to use a specific realm as provided by the container, but the application can not provide this itself.
That said, while all of the containers are different, and these realms are NOT portable, simple common sense will reduce the differences to the little bit of glue code necessary to integrate with the container, if you're looking for portability.
Having has a quick look at Suns documentation it looks like you will have to write a custom LoginModule which extends their app server specific class. This seems a little backwards to me and a limitation of Glassfish.
If you want to make this more portable, I'd suggest putting the bulk of the implementation in a custom LoginModule developed against the standard JavaEE interface(s) and then having a thin implementation layer specific to Glassfish that delegates to the standard, portable implementation.
Check out Sun's article on this subject.
I never actually did this myself, but I'm pretty confident each AS gives you an option to register new realms (security domains) on it.
It probably won't be 100% portable, and for each AS you might need a different config XML, but basically, there is no reason for the code to be any different.