OpenID Authenticator for Tomcat

By default Tomcat supports following auth-method's with corresponding Authenticators.

1. BASIC - org.apache.catalina.authenticator.BasicAuthenticator
2. FORM - org.apache.catalina.authenticator.FormAuthenticator
3. DIGEST - org.apache.catalina.authenticator.DigestAuthenticator
4. CLIENT-CERT - org.apache.catalina.authenticator.SSLAuthenticator

My previous post explains how BASIC authentication works with Tomcat.

In this post, we'll be adding a new type of Authenticator to Tomcat.

5. OPENID - org.wso2.OpenIDAuthenticator

With this you can protect your web resources with OpenID authentication.

I am using WSO2 OpenID Relying Party components which do ship with WSO2 Identity Solution.

Let's get started.

First we need to configure Tomcat to use our custom Authenticator.

Extract [CATALINA_HOME]\server\lib\catalina.jar and edit the file \org\apache\catalina\startup\Authenticators.properties to look like following - simply adding our Authenticator to it.
# These must match the allowed values for auth-method as defined by the spec 
BASIC=org.apache.catalina.authenticator.BasicAuthenticator
CLIENT-CERT=org.apache.catalina.authenticator.SSLAuthenticator
DIGEST=org.apache.catalina.authenticator.DigestAuthenticator
FORM=org.apache.catalina.authenticator.FormAuthenticator
NONE=org.apache.catalina.authenticator.NonLoginAuthenticator
OPENID=org.wso2.OpenIDAuthenticator
Now we need to re-pack the extracted jar with our change to catalina.jar and keep it in it's original location.

You can download other dependency jars from here.

Copy the jars inside [ZIP_FILE]\jars to [CATALINA_HOME]\server\lib and the jars from [ZIP_FILE]\endorsed to [CATALINA_HOME]\common\endorsed.

That's it with Tomcat configuration.

Now, let's see how we can configure OPENID authentication for our webapp.

It's basically the same way you configure BASIC or any other auth-method for your web app.

You can copy [ZIP_FILE]\demo-app folder to [CATALINA_HOME]\webapps.

Let's have a look at [CATALINA_HOME]\webapps\demo-app\WEB-INF\web.xml.
<web-app> 

<security-constraint> 
<web-resource-collection>
<web-resource-name>secured resources</web-resource-name> 
<url-pattern>/web/*</url-pattern> 
</web-resource-collection>
<auth-constraint> 
<role-name>*</role-name> 
</auth-constraint> 
</security-constraint> 

<login-config> 
<auth-method>OPENID</auth-method> 
<form-login-config>
<form-login-page>/openid-login.jsp</form-login-page>
<form-error-page>/denied.jsp</form-error-page>
</form-login-config> 
</login-config> 

</web-app>
Here, the openid-login.jsp and denied.jsp pages are not application specific - so can be reused across.

All - set, let's see how the demo works - you can also access the online demo from here.

http://localhost:8080/demo-app/ - this is not a protected resource.

Click on the link to, Protected Resource - since this is OpenID protected and you are not authenticated yet, you'll be redirected to the OpenID login page - Type your OpenID there and complete the OpenID authentication routine.

You are on the protected resource now...

I'll just dump the code here for the OpenIDAuthenticator and the OpenIDRealm - it's self-explanatory through comments.
package org.wso2;

import java.io.IOException;
import java.security.Principal;

import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.Realm;
import org.apache.catalina.Session;
import org.apache.catalina.authenticator.Constants;
import org.apache.catalina.authenticator.FormAuthenticator;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.deploy.LoginConfig;
import org.wso2.solutions.identity.relyingparty.RelyingPartyException;
import org.wso2.solutions.identity.relyingparty.TokenVerifierConstants;
import org.wso2.solutions.identity.relyingparty.openid.OpenIDAuthenticationRequest;
import org.wso2.solutions.identity.relyingparty.openid.OpenIDConsumer;
import org.wso2.solutions.identity.relyingparty.openid.OpenIDRequestType;
import org.wso2.solutions.identity.relyingparty.openid.OpenIDUtil;

/**
* This extends the functionality of FormAuthenticator to facilitate OpenID logins.
* 
* @author Prabath Siriwardena @ WSO2 
* http://www.wso2.org 
* http://blog.facilelogin.com
* 
*/
public class OpenIDAuthenticator extends FormAuthenticator {

/**
* {@inheritDoc}
*/
public boolean authenticate(Request request, Response response, LoginConfig config) {
Principal principal = null;
boolean loginAction = false;
boolean isAuthenticated = false;
String requestURI = null;
Realm realm = null;
String openID = null;

// References to objects we will need later
Session session = null;

principal = request.getUserPrincipal();

if (principal != null) {
// We are here because we have being authenticated successfully, before.
return true;
}

// Check whether this is a re-submit of the original request URI after successful
// authentication? If so, forward the *original* request instead.
if (matchRequest(request)) {
return matchRequest(request, response, config);
}

// This should be the OpenID return to url.
requestURI = request.getDecodedRequestURI();

// This request came from the login page - let me login - here are my credentials.
loginAction = (request.getParameter("login") != null);

if (!loginAction) {
// This can be the initial request for the protected resource or being redirected back
// by the OpenID Provider.

if (OpenIDUtil.isOpenIDAuthetication(request)) {
// This is an OpenID response - follow the OpenID protocol
String auth = null;
try {
OpenIDConsumer.getInstance().setSessionAttributes(request);
auth = (String) request.getAttribute(TokenVerifierConstants.SERVLET_ATTR_STATE);
if (auth != null && TokenVerifierConstants.STATE_SUCCESS.equals(auth)) {
isAuthenticated = true;
} else {
forwardToErrorPage(request, response, config);
return (false);
}
} catch (RelyingPartyException e) {
forwardToErrorPage(request, response, config);
return (false);
}
} else {
try {
session = request.getSessionInternal(true);
saveRequest(request, session);
} catch (IOException ioe) {
return (false);
}
request.getSession().setAttribute("requestURI", requestURI);
forwardToLoginPage(request, response, config);
return (false);
}
}

// You are here, because you came here directly from the openid-login page or you are
// authenticated at OP and redircted back.

if (!isAuthenticated) {
// Let's build the OpenID authentication request.
try {
doOpenIDAuthentication(request, response);
return false;
} catch (RelyingPartyException e) {
forwardToErrorPage(request, response, config);
return (false);
}
}

realm = context.getRealm();
session = request.getSessionInternal(false);

if (!(realm instanceof OpenIDRealm)) {
realm = new OpenIDRealm();
context.setRealm(realm);
}

openID = (String) request.getAttribute("openid_identifier");
principal = realm.authenticate(openID, "");

if (principal == null) {
forwardToErrorPage(request, response, config);
return (false);
}

// Save the authenticated Principal in our session
session.setNote(Constants.FORM_PRINCIPAL_NOTE, principal);

// Save the OpenID
session.setNote(Constants.SESS_USERNAME_NOTE, openID);
// We have no password for OpenID
session.setNote(Constants.SESS_PASSWORD_NOTE, "");

// Redirect the user to the original request URI (which will cause
// the original request to be restored)
requestURI = savedRequestURL(session);
try {
response.sendRedirect(response.encodeRedirectURL(requestURI));
} catch (IOException e) {
return (false);
}

return (false);
}

/**
* Performs OpenID authentication
* 
* @param request Request we are processing
* @param response Response we are creating
* @throws RelyingPartyException
*/
protected void doOpenIDAuthentication(Request request, Response response)
throws RelyingPartyException {
OpenIDAuthenticationRequest openIDAuthRequest = null;
openIDAuthRequest = new OpenIDAuthenticationRequest(request, response);

openIDAuthRequest.setOpenIDUrl((String) request.getParameter("openIdUrl"));
openIDAuthRequest.addRequestType(OpenIDRequestType.SIMPLE_REGISTRATION);

if (request.getProtocol().equals("HTTP/1.1")) {
openIDAuthRequest.setReturnUrl("http://" + request.getLocalName() + ":"
+ request.getLocalPort() + request.getSession().getAttribute("requestURI"));
} else {
openIDAuthRequest.setReturnUrl("https://" + request.getLocalName() + ":"
+ request.getLocalPort() + request.getSession().getAttribute("requestURI"));
}

OpenIDConsumer.getInstance().doOpenIDAuthentication(openIDAuthRequest);
}

/**
* Check whether this is a re-submit of the original request URI after successful
* authentication? If so, forward the *original* request instead.
* 
* @param request Request we are processing
* @param response Response we are creating
* @param config Login configuration describing how authentication should be performed
*/
private boolean matchRequest(Request request, Response response, LoginConfig config) {
Session session = null;
Principal principal = null;

session = request.getSessionInternal(true);
principal = (Principal) session.getNote(Constants.FORM_PRINCIPAL_NOTE);
register(request, response, principal, Constants.FORM_METHOD, (String) session
.getNote(Constants.SESS_USERNAME_NOTE), (String) session
.getNote(Constants.SESS_PASSWORD_NOTE));
// If we're caching principals we no longer need the username
// and password in the session, so remove them
if (cache) {
session.removeNote(Constants.SESS_USERNAME_NOTE);
session.removeNote(Constants.SESS_PASSWORD_NOTE);
}
try {
if (restoreRequest(request, session)) {
return (true);
} else {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return (false);
}
} catch (IOException e) {
forwardToErrorPage(request, response, config);
return (false);
}
}
}
package org.wso2;

import java.security.Principal;

import org.apache.catalina.Context;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.realm.RealmBase;

/**
* This extends the functionality of RealmBase to facilitate OpenID logins.
* 
* @author Prabath Siriwardena @ WSO2 
* http://www.wso2.org 
* http://blog.facilelogin.com
* 
*/
public class OpenIDRealm extends RealmBase {

/**
* No passwords for OpenID
*/
protected String getPassword(String openID) {
return "";
}

/**
* {@inheritDoc}
*/
protected Principal getPrincipal(String OpenID) {
return new GenericPrincipal(this, OpenID, "", null, null);
}

/**
* We have no roles associated.
*/
public boolean hasRole(Principal principal, String role) {
return false;
}

/**
* Give this realm required permissions.
*/
public boolean hasResourcePermission(Request request, Response response,
SecurityConstraint[] constraints, Context context) {
return true;
}


/**
* Realm name
*/
protected String getName() {
return "OpenIDRealm";
}

}