Get hands-on training for JIRA Software, Confluence, and more at Atlassian Summit Europe. Register now ›

Everyone seems to be going nuts for OpenID.

The concept is still new, yet new sites pop up every day supporting the technology. There is plenty of chatter on various blogs and mailing list about integrating with OpenID, yet there are not really a lot of examples on how to do things as a best practice.

When we decided to turn Crowd into an OpenID server application (end of May release), one of the first things we realized was that we needed a relying-party application to test our server with.

When we finished reviewing the specification and deciding on what we though would be best path to implementation we took a serious look at the various APIs to see which one would match up to our core feature needs.

The list is short and simple, there are two options:

Both projects are hosted on code.google.com which is a plus if you want to check them out because you already have an account so it is quick and easy to start integrating with the project.

Within minutes you quickly see there is a larger community around the OpenID4Java.org project.

Recently we purchased the openid4java.org domain and setup a redirect for the project as part of a larger effort to maven 2 friendly the project. A build server has also been added, and you can review the code though the Fisheye source code browser. The original code was developed by Sxip, who are one of the major drivers of the OpenID initiative.

After laying out our common framework stuff we were ready to build an OpenID login prompt:

One of the things that we added was a dynamic attribute exchange request builder. This way you can send request for anything and see how your server responds to the messages. With our server we are building a profile editor that highlights which attributes are required by the relying-party along with those that are optional. I will share some screenshots of this in a later post.

After the relying party client directs the user to the server for an authentication check we are sent back to the server with the following:

Looking at the image above you can see the request attributes are passed back to the relying party to do with as they wish. If you look in the upper right corner of the screen you can see we display the user’s full name and identifier as part of their authentication.

So how did we do it all?

Like I said we are going to be shipping this relying party application including code with the next major release of Crowd, but until then here are some code examples which are all based on OpenID4Java…

The Login Action sets up a basic authentication request that is passed off to a servlet that sends out the request to the Open ID provider. This is done so that multiple things like javascript can initialize an authentication request:

public String execute() throws Exception
{
// if an OpenID identifier (URL/XRI) has been supplied, make the OpenID authentication request
if (StringUtils.isNotBlank(openid_identifier) && !hasActionErrors())
{
OpenIDAuthRequest openidReq = new OpenIDAuthRequest(openid_identifier, "loginresult.action");
openidReq.setRequiredAttributes(requiredAttribs);
openidReq.setOptionalAttributes(optionalAttribs);
ServletActionContext.getRequest().setAttribute("openidAuthRequest", openidReq);
logger.debug("Login.action about to make request to AuthenticationRequestServlet");
return SUCCESS;
}
else if (isAuthenticated())
{
return "secure";
}
// otherwise, show the login screen
else
{
logger.debug("Login.action about to display login.jsp");
return INPUT;
}
}

Here is our authentication request object that is used by the servlet, it is a simple pojo:

public class OpenIDAuthRequest implements Serializable
{
private String identifier;
private String returnURL; // URL to forward response to
private List requiredAttributes;
private List optionalAttributes;
...
}

The authentication request servet redirects the user to the OpenID provider:

public class AuthenticationRequestServlet extends HttpServlet
{
// spring injected
private CrowdConsumer crowdConsumer = null;
private static final Logger LOG = Logger.getLogger(AuthenticationRequestServlet.class);
public void init(ServletConfig config) throws ServletException
{
super.init(config);
final WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(config.getServletContext());
crowdConsumer = (CrowdConsumer) context.getBean("crowdConsumer");
}
// forward to doPost
protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
doPost(httpServletRequest, httpServletResponse);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
// get the openid auth request
OpenIDAuthRequest openidReq = (OpenIDAuthRequest)request.getAttribute(CrowdConsumer.OPENID_AUTH_REQUEST);
if (openidReq == null)
{
// TODO - need 500 page that is pretty regarding your openid authentication failed
throw new ServletException("FATAL: openidAuthRequest has not been set in the HttpServletRequest.");
}
try
{
// save returnURL in session (so that AuthenticationResponseServlet can read it back)
request.getSession().setAttribute(CrowdConsumer.RETURN_URL, openidReq.getReturnURL());
// this will redirect control to the OP
crowdConsumer.authenticateRequest(openidReq, request, response);
}
catch (OpenIDAuthRequestException e)
{
// if an error occurs we will redirect control back
LOG.error(e);
forwardErrorResponse(e, openidReq, request, response);
}
}
private void forwardErrorResponse(Exception e, OpenIDAuthRequest openidReq, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
// make the error response
OpenIDAuthResponse openidResp = new OpenIDAuthResponse(openidReq.getIdentifier());
openidResp.setError(e);
// put it in the request
request.setAttribute(CrowdConsumer.OPENID_AUTH_RESPONSE, e);
// forward to supplied returnURL
request.getRequestDispatcher(openidReq.getReturnURL()).forward(request, response);
}
}

On the way out we set that the OpenID provider should come back to our response servlet:

public class AuthenticationResponseServlet extends HttpServlet
{
// spring injected
private CrowdConsumer crowdConsumer = null;
public void init(ServletConfig config) throws ServletException
{
super.init(config);
final WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(config.getServletContext());
crowdConsumer = (CrowdConsumer) context.getBean("crowdConsumer");
}
// forward to doPost
protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException
{
doPost(httpServletRequest, httpServletResponse);
}
// it is expected that the OP will redirect the user to this returnTo servlet
// this serlvet is responsible for verifying the redirected response from the server
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
OpenIDAuthResponse openidResp = crowdConsumer.verifyResponse(request);
request.setAttribute(CrowdConsumer.OPENID_AUTH_RESPONSE, openidResp);
String returnURL = (String) request.getSession().getAttribute(CrowdConsumer.RETURN_URL);
if (returnURL == null)
{
throw new ServletException("Failed to find " + CrowdConsumer.RETURN_URL + " in session");
}
request.getRequestDispatcher(returnURL).forward(request, response);
}
}

Here the response comes in and we stick the results into another pojo to represent the returned message:

public class OpenIDAuthResponse implements Serializable
{
private String identifier;
private Map attributes; // Map
private OpenIDAuthException error;
...
}

The result is then forwarded to our UI control layer which can decide how it wants to display things:

public class LoginResult extends BaseAction
{
private static final Logger logger = Logger.getLogger(Login.class);
public String execute() throws Exception
{
OpenIDAuthResponse authResponse = (OpenIDAuthResponse) ServletActionContext.getRequest().getAttribute(CrowdConsumer.OPENID_AUTH_RESPONSE);
if (authResponse == null)
{
addActionError(CrowdConsumer.OPENID_AUTH_RESPONSE + " not defined in HttpServletRequest");
return ERROR;
}
if (authResponse.hasError())
{
logger.error("OpenIDAuthResponse reports authentication error", authResponse.getError());
StringBuffer errorMsg = new StringBuffer("Authentication Failed: ");
errorMsg.append(authResponse.getError().getNiceMessage());
addActionError(errorMsg.toString());
return ERROR;
}
setOpenIDPrincipal(new OpenIDPrincipal(authResponse));
return SUCCESS;
}
}

We put the authResponse response into session for use later as an object that represents the principal and their attributes:

public class OpenIDPrincipal implements Serializable
{
private String identifier;
private Map attributesMap;
public OpenIDPrincipal(OpenIDAuthResponse resp) throws OpenIDAuthResponseException
{
if (!resp.hasError())
{
this.identifier = resp.getIdentifier();
this.attributesMap = resp.getAttributes();
}
else
{
throw new OpenIDAuthResponseException("Cannot create OpenIDPrincipal as OpenIDAuthResponse is an error message");
}
}
public String getIdentifier()
{
return identifier;
}
public void setIdentifier(String identifier)
{
this.identifier = identifier;
}
public Map getAttributesMap()
{
return attributesMap;
}
public void setAttributesMap(Map attributesMap)
{
this.attributesMap = attributesMap;
}
/**
* Returns a list of values for an attribute.
* @param key attribute name.
* @return guaranteed to return a non-null list.
*/
public List getAttributes(String key)
{
if (attributesMap != null && attributesMap.get(key) != null)
{
return (List) attributesMap.get(key);
}
return Collections.EMPTY_LIST;
}
/**
* Returns the first value for an attribute.
* @param key attribute name.
* @return returns null if attribute does not exist.
*/
public String getAttribute(String key)
{
if (attributesMap != null && !getAttributes(key).isEmpty())
{
return (String) getAttributes(key).get(0);
}
return null;
}
}

To determine if the user is authenticated we coded a servlet filter to check if the OpenIDPrincipal object is in session for the user. If the object is not in session, the authentication is considered invalid and the user is redirect to the login prompt:

/**
* Checks if web-user is authenticated. If they are not redirect them to the login page.
*/
public class VerifyOpenIDAuthenticationFilter extends OncePerRequestFilter
{
/**
* The session key stored as a String, is the requested secure url before redirect to the authentication
* page.
*/
public static final String ORIGINAL_URL = VerifyOpenIDAuthenticationFilter.class.getName() + ".ORIGINAL_URL";
/**
* create a static reference to the logger.
*/
private static final Logger logger = Logger.getLogger(VerifyOpenIDAuthenticationFilter.class);
public static final String AUTHENTICATED_PRINCIPAL_SESSION_KEY = BaseAction.class.getName() + ".AUTHENTICATED_PRINCIPAL_SESSION_KEY";
/**
* Checks if a principal is authenticated.
*
* @param request  The HTTP request.
* @param response The HTTP response.
* @return true if and only if the principal is authenticated, otherwise false.
* @throws java.io.IOException I/O related problems.
* @throws ServletException    {@link javax.servlet.Servlet} related problems.
*/
protected boolean isAuthenticated(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
// if the object is in session, assume they have a valid authentication
return request.getSession().getAttribute(AUTHENTICATED_PRINCIPAL_SESSION_KEY) != null;
}
/**
* Stores the URL the user was originally requesting.
* @param request The HTTP request.
* @return The requested URL.
*/
protected String getOringinalURL(HttpServletRequest request) {
// get the request URL
StringBuffer originalURL = request.getRequestURL();
// store the original path
boolean foundParameter = false;
if (request.getParameterMap().size() > 0)
{
originalURL.append("?");
Enumeration params = request.getParameterNames();
for (; params.hasMoreElements();)
{
if (foundParameter == false)
{
foundParameter = true;
}
else
{
originalURL.append("&");
}
String name = (String) params.nextElement();
String values[] = request.getParameterValues(name);
for (int i = 0; i < values.length; i++)
{
originalURL.append(name).append("=").append(values[i]);
}
}
}
return originalURL.toString();
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
{
logger.debug("Running VerifyOpenIDAuthenticationFilter");
// check if they are authenticated
if (!isAuthenticated(request, response))
{
// get the request URL
String originalURL = getOringinalURL(request);
logger.debug("Requesting URL is: " + originalURL);
request.getSession().setAttribute(ORIGINAL_URL, originalURL.toString());
logger.info("Authentication is not valid, redirecting to: " + request.getContextPath());
// send them to the login page, they are not authenticated
response.sendRedirect(request.getContextPath());
}
else
{
// send the mon their merry way to the page they are trying to access
request.removeAttribute(ORIGINAL_URL);
filterChain.doFilter(request, response);
}
}
}

The filter also does some other smart things like storing the requesting URL and parameters so you can redirect the user to their originally requested URL.

That’s it. To get this all integrated into your application I would set aside about 4-5 hours. Most of the work is around understanding the concepts.

Fresh ideas, announcements, and inspiration for your team, delivered weekly.

Subscribe now

Fresh ideas, announcements, and inspiration for your team, delivered weekly.

Subscribe now