Saturday, November 22, 2008

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";
}

}

16 comments:

Anonymous said...

what about http://blog.spaceprogram.com/2007/10/how-to-add-openid-support-to-your-java.html ?

Prabath said...

That is through a servlet filter - and application specific - this is basically enabling OpenID authentication at the continer level.

Thanks.
- Prabath

Anonymous said...

Very Helpful.
Just a quick question - Do you know if the same configuration should work with Tomcat 4.0.3? For some reason the FormAuthenticator Class in my jar is final and cannot be inherited.

Thanks

Prabath said...

Hi;

Not quite sure - for the time being haven't tested with Tomcat 4.0.3 - please let me know if you face any issues there.

Thanks.
- Prabath

irs said...

I have tried to follow these suggestions to provide OpenId authorization to Hudson (a continious integration tool, https://hudson.dev.java.net/) hosted in Tomcat 6 (it needs Tomcat 6), but failed miserably.

First of all, the locations for support libs: common/endorsed and server/lib, seem to be just endorsed and lib on Tomcat 6 (to be on the safe side, I copied to these, as well as the ones you mentioned, and made common/endorsed endorsed as well (in the Win Service properties).

Secondly, once I set up your demo app in my Tomcat, it would show the the mail screen, then only show a blank one, once I click on "Access Protected resource". The URL is then dhown as demo-app/web/, and the page source is empty. I don't know what I'm doing wrong, but there we are.

Besides, Hudson is a fairly complex app, and it can be configured to use Tomcat's own user authentication.

Prabath, if you have time and interest in this, may be you'll look at Tomcat 6 & Hudson specifically?

Igor

Prabath said...

Thanks Igor for the comment - I'll look into that.

- Prabath

Carl said...

Prabath.
Do you know if this approach is supported in a high volume authentication example or is this just a demo of whats possible?

crystal said...

Thank you so much!!polo shirt men'ssweate,Burberry Polo Shirts lacoste sweater, ralph lauren Columbia Jackets,ski clothing. Free Shipping, PayPal Payment. Enjoy your shopping experience on mensclothingus.com.You can find the father who desire fashionable, intellectual mens clothing simultaneously.
http://www.pumafr.com/blog
http://poloshirtsonline.blogspot.com
http://thediary.org/mensclothing
http://blog.livedoor.jp/dokoma
http://www.itimes.com/my_blog.php

crystal said...

Awesome!!!Best wishes for you !!cheap polo shirts is the father of the summer should be prepared to most commonly used item, it has both style and shape of Ralph Lauren Polo, and vest with a random function polo ralph lauren, so that in the short-sleeved apply to both on many occasions, the pink and black color men's polo shirts brought into effect, lightweight cotton, linen texture to demonstrate masculine temperament and sense of fashion exhaustively.

venus said...

God bless you!I really agree with your opinions.Also,there are some new fashion things here,gillette razor blades.gillette mach3 razor bladesfor men.As for ladies,gillette venus razor blades must the best gift for you in summer,gillette fusion blades are all the best choice for you.
http://blog.livedoor.jp/lljj332
http://shoes-puma.jugem.jp
http://poloshirts--myfashion.blogspot.com
http://blades.blogsome.com
http://gillettefusion.edublogs.org

lj said...

Perfect!!You are a outstanding person!Have you ever wore chaussures puma,Here are the most popular puma CAT,Puma shoes store gives some preview of puma speed cat,and casual but no sweat puma basket.

venus said...

Do not mean bad.Thank you so much!I just want to show some fashion things to all of you.I like puma speed, puma femmes and other puma shoes. These puma sport items are at store recently and available for anyone.

lj said...

Fantastic!God bless you!Meanwhile,you can visit my China Wholesale,we have the highest quality but the lowest price fashion products wholesale from China.Here are the most popular China Wholesale productsfor all of you.You can visit http://chinaclothes.net.Also the polo clothing is a great choice for you.

xawave said...

This is very useful but for my case, I got the following error (with Tomcat 6.0.20) -

2009-07-31 15:49:18,662 [main] INFO org.apache.catalina.core.StandardPipeline -
Can't register valve org.wso2.OpenIDAuthenticator@17e4ca
java.lang.NoClassDefFoundError: org/apache/catalina/connector/Request
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2427)
at java.lang.Class.privateGetPublicMethods(Class.java:2547)
...

Any idea why?

jtan said...

This does not work with tomcat 6.0.20 with ws02 2 jdk 1.6.

Null pointer exception, and does not allow any access to the protected resource. Error -

java.lang.NullPointerException
at org.wso2.carbon.identity.relyingparty.ui.openid.OpenIDConsumer.authRe
quest(OpenIDConsumer.java:176)
at org.wso2.carbon.identity.relyingparty.ui.openid.OpenIDConsumer.doOpen
IDAuthentication(OpenIDConsumer.java:97)
at org.apache.jsp.openidsubmit_jsp._jspService(openidsubmit_jsp.java:108
)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper
.java:374)
at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:3
42)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:267)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(Appl
icationFilterChain.java:290)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationF
ilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperV
alve.java:233)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextV
alve.java:191)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(Authentica
torBase.java:433)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.j
ava:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.j
ava:102)
at

get4gopi said...

Is this will work for integration with Google OAuth2 + Tomcat 6?