Friday, August 12, 2011

Creating a server instance and deploying a Java app on Amazon's EC2 Cloud

My previous blog post detailed the process by which I chose to host my web application on the Amazons EC2 Cloud using the Bitnami Tomcat Stack AMI.  This post will describe some of the steps I followed when creating the server instance, connecting to it and deploying a Java .war.

Connecting to your Amazon EC2 server instance

There's no need for me to write step-by-step directions for creating the instance since Bitnami has already done a great job in providing instructions.  It's quite simple and I didn't run into any issues creating the instance.

Don't forget to open the SSH and HTTP ports for your server instance using the Security Groups function of the AWS Management Console.  Once you open port 80 you will be able to access the default Tomcat web page in your browser via your instance's public DNS name (ie. http://ec2-107-20-221-246.compute-1.amazonaws.com).

Connecting to the new server instance is a little trickier, particularly for someone who isn't exactly a Unix/Linux guru.  However, it's still relatively simple and again Bitnami has provided detailed instructions.

They key to connecting to your instance is the key.  :) Well, key pairs to be exact.  It's not just the key pair, you'll also need an SSH client like PuTTy.  You will be prompted to create and download the key when you create your first EC2 server instance.  Keep this file secure and do not lose it.

Here's the part that tripped me up: the key you download from AWS is not compatible with the PuTTy SSH client.  AWS provides a key in .pem format and you'll need to convert it to the .ppk format that PuTTy can use.  This excellent blog post provides details and instructions for converting the key and connecting with PuTTy.

Configuring MySQL

My next step was to access MySQL and create my database schema. I installed Navicat Lite on my desktop and configured it as described by Bitnami here.  This worked like a charm and it wasn't long before I had remotely logged into MySQL running on my server instance on EC2.  I  ran SQL commands to create a database schema, user and grant privileges.  I then imported a .sql file with the DDL I had exported from my local database using MySQL's "mysqldump" tool.

Deploying a .war file to Tomcat

I installed the WinSCP FTP client on my development machine and configured it to connect to my server instance.  Then I FTP'd my .war file to the "/opt/bitami/apache-tomcat/webapps" directory and voila!  My web application was now running on Amazon's EC2.

Getting started with Amazon's Elastic Compute Cloud (EC2) and the Bitnami Tomcat Stack

My next couple of blog posts will detail my experiences thus far with Amazon's Elastic Computing Cloud (Amazon EC2) and the Bitnami TomcatStack Amazon Machine Image (AMI).

Choosing Amazon's EC2

In the past, I have typically deployed my Java applications within a company intranet or extranet.  However,  my current project needs to be publicly accessible and it's up to me to choose a suitable and ultra-affordable web hosting provider.

My hosting requirements are fairly simple.  I want to deploy my application on Apache Tomcat using a MySQL database and several popular Java frameworks including Hibernate, Struts 2, and eventually Spring.  Other than that, I don't expect my current web application to require much in terms of bandwidth and resources.

I was considering trying Google's App Engine until I learned the App Engine doesn't support the Hibernate ORM framework and requires some tweaking to work with Struts 2.

I began to look more closely at Amazon's EC2 service after reading several StackOverflow posts (here and here) suggesting EC2 as an alternative to traditional web hosting services for Java applications.  I soon learned that Amazon offers a Free Usage Tier for new customers.  As described on their website, the "free usage tier can be used for anything you want to run in the cloud: launch new applications, test existing applications in the cloud, or simply gain hands-on experience with AWS."

At first, getting started with EC2 initially seemed a like daunting task.  I explored using Amazon's Elastic Beanstalk which is promoted as "an even easier way for you to quickly deploy and manage applications in the AWS cloud."  However, I couldn't find any documentation or examples of using Beanstalk with MySQL.  The examples I found used the Amazon SimpleDB and it wasn't clear whether I could easily use SimpleDB with Hibernate.  Lacking a clear understanding of how to use Beanstalk with MySQL I decided not to try Beanstalk.  (Note: Please let me know if you have used Beanstalk with MySQL or if you've found some resources on this topic.  Thanks!)

Choosing an Amazon Machine Image (AMI)

After creating an Amazon AWS account, it was time to select and launch a server instance on the EC2.  But which AMI should I select for my EC2 instance?  There are thousands of Community AMI's, hundreds of which are listed as free tier eligible.

Here's a useful blog post I found describing the steps necessary to select and install Java, Tomcat, and MySQL.  The blog author chose the Amazon Linux AMI.  This AMI is a good starting point and is actively supported by Amazon.  "Amazon Web Services also provides ongoing security and maintenance updates to all instances running the Amazon Linux AMI. The Amazon Linux AMI is provided at no additional charge to Amazon EC2 users."

But alas, is there an even easier way to get started?  With all those Community AMI's out there can I find one that bundles Linux, Java, Apache, Tomcat and MySQL.  The answer is yes!  The Bitnami Tomcat Stack "includes ready-to-run versions of Apache, Tomcat, MySQL and Java and required dependencies" and "is distributed for free under the Apache 2.0."  Nice!

Bitnami also offers a Cloud Hosting service which in conjunction with the Amazon EC2 provides a number of ease-of-use benefits.  However, it is not required to use the Bitnami Cloud Hosting service when selecting one of their free Community AMI's.

In my next blog post I'll explain the process of creating and connecting to the Amazon EC2 instance.

Sunday, August 7, 2011

Using Hibernate's event architecture to manage created and updated timestamp fields

This blog post describes a method for managing created and last modified timestamp table columns using Hibernate 3.6.  Before I detail the working solution, I'll explain a failed approach I attempted.  I'm including this approach because it turned out to be useful learning experience.  You can skip to the bottom section of this post if you want to go straight to the working solution. :)

A little Googling found a Stack Overflow post suggesting using @Prepersist and @PreUpdate JPA Annotations.  Up to this point I'd been using Hibernate mapping files (hbm.xml) to define my entities.  I'd been using Ant and the hibernatetool task to generate both the POJO classes and the Database tables.  For the most part I was satisfied using the hbm xml mappings although I had planned on eventually converting to JPA Annotations mappings anyway.

The @Prepersist and @PreUpdate annotations looked simple enough and I decided now was time to bite the bullet and I convert all my .hbm xml mappings to JPA Annotations.  This took some time but was a worthwhile experience.  Maybe I'll write a blog post discussing the process I used for migrating from .hbm xml mapping files to JPA Annotations.

When I was finally satisfied my newly converted JPA Annotated classes were functioning as expected I went ahead and added @Prepersist and @PreUpdate to my entity POJOs as follows:

@Temporal(TemporalType.TIMESTAMP)
@Column(name="created", nullable = false)
public Date getCreated() {return created;}
public void setCreated(Date created) {this.created = created;}
private Date created;

@Temporal(TemporalType.TIMESTAMP)
@Column(name="updated", nullable = false)
public Date getUpdated() {return updated;}
public void setUpdated(Date updated) {this.updated = updated;}
private Date updated;

@PrePersist
protected void onCreate() {
setCreated(new Date());
setUpdated(new Date());}

@PreUpdate
protected void onUpdate() {
setUpdated(new Date());}

I soon realized an important drawback to this approach.  It simply doesn't work when using Hibernate's Session API.  This was a big problem for me since as I've discussed in detail in previous blog posts I am in fact using org.hibernate.Session.  :( As described in this Stack Overflow post, it is necessary to use the EntityManager API when using the @Prepersist and @PreUpdate annotations.

I considered converting from the Session API to the EntityManager API but decided against it for the time being.  That's when I looked more closely at Hibernate's interceptor and events architecture.  I also found a very helpful blog post by a blogger name neildo that explained how to use an event listener to manage last modified date.

Using neildo's blog post and the documentation as a starting point, I developed the following code that I am using today.  My code differs from the neildo's example in that I'm setting both the created and the modified dates in the custom event listener.

First there's the interface that each entity POJO must implement.  As you can see the interface includes "id" which I'll explain later.
public interface AuditableDate {

    public void setId(int id);
    public int getId();
    
    public void setCreated(Date date);
    public Date getCreated();
    
    public void setUpdated(Date date);
    public Date getUpdated();
}

Here's an example of an entity POJO that implements AuditableDate.  For this example, I've stripped out the rest of the entity properties and left only those needed to implement AuditableDate.  Obviously the real entity would have other properties as well.
public class Tag implements AuditableDate, Serializable {

    @Id
    @GeneratedValue
    @Column(name="TagId")
    @Override
    public int getId() {return this.id;}
    public void setId(int id) {this.id = id;}
    private int id;

    @Override
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name="CreatedDate",
            nullable=false, 
        updatable = false)
    public Date getCreated() {return created;}
    public void setCreated(Date date) {this.created = date;}
    private Date created;
    
    @Override
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name="UpdatedDate", nullable = false)
    public Date getUpdated() {return updated;}
    public void setUpdated(Date date) {this.updated = date;}
    private Date updated;

    public Tag() {
    }
}

The event listener is as follows.  You can see where I'm checking the Id property to determine if the we are working with a new record and if so I'm setting the both the Created Date and the Updated Date.
public class SaveOrUpdateDateListener extends DefaultSaveOrUpdateEventListener {
    
    static Logger logger = Logger.getLogger(SaveOrUpdateDateListener.class);

    @Override
    public void onSaveOrUpdate(SaveOrUpdateEvent event) {
        logger.debug("Entering onSaveOrUpdate()");
        
        if (event.getObject() instanceof AuditableDate) {
            AuditableDate record = (AuditableDate) event.getObject();
            
            // set the Updated date/time
            record.setUpdated(new Date());

            // set the Created date/time
            if (record.getId() == 0) {
                record.setCreated(new Date());
            }
        }
}

The last step is to add a reference to the Hibernate configuration so that it knows to fire the custom listener.
<event type="save-update">
    <listener class="org.robbins.persistence.SaveOrUpdateDateListener"/>
    <listener class="org.hibernate.event.def.DefaultSaveOrUpdateEventListener"/>
</event>

You'll notice that I've included BOTH the custom listener and the default listener.  I'm not sure why that was necessary but I was unable to get the default listener to fire in the custom listener by using:
super.onSaveOrUpdate(event);

I hope you found this post useful.  I'd appreciate any questions or suggestions you may have.  Thanks!

Thursday, August 4, 2011

Integrating OpenId into my Struts 2 app - Part 4 - The OpenIdAuthenticator bean

In the final post in this series, we'll concentrate on the OpenIdAuthenticator which does the heavy lifting, with help from the OpenId4Java library, in our interaction with the user's selected OpenID Provider.

According to the OpenId4Java website, the OpenId4Java library "allows you to OpenID-enable your Java webapp."  Many thanks to the folks who put together this solid library.  I suggest checking out the sample code.  There's a number of example that will help you get started.

One of the tricky things I encountered was managing to get the OpenId provider to provide information about the logged in user.  Things like the user's first and last name, their email address, country, and language.  I was disappointed to discover the OpenID Providers (Google, Yahoo, AOL, etc) are not consistent in the level of detail they provide about the user.  Not only that but the format of the attributes are different as well.  This difference leads to the if/else section of the getValidateOpenIdUrl() method.

public class OpenIdAuthenticator {
    
    static Logger logger = Logger.getLogger(OpenIdAuthenticator.class);

    private static ConsumerManager getConsumerManager(Map<String, Object> application) {
        logger.debug("Entering getConsumerManager()");
        
        ConsumerManager manager;

        // try to get the ConsumerManager from the Application scope
        manager = (ConsumerManager)application.get("consumermanager");
        
        if (manager == null) {
            // create a new ConsumerManager
            try {
                manager = new ConsumerManager();
                manager.setAssociations(new InMemoryConsumerAssociationStore());
                manager.setNonceVerifier(new InMemoryNonceVerifier(5000));
            } catch (Exception e) {
                e.printStackTrace();
            }
        
            // add the Consumer Manager to the application scope
            application.put("consumermanager", manager);
        }

        return manager;
    }
    
    @SuppressWarnings("unchecked")
    public static String getValidateOpenIdUrl(String returnUrl, String openIdIdentifier, Map<String, Object> httpSession, Map<String, Object> application) throws DiscoveryException,
            MessageException, ConsumerException {
        
        logger.debug("Entering getOpenIdDestinationUrl()");

        // get a reference to the Consumer Manager
        ConsumerManager manager = getConsumerManager(application);
        
        // perform discovery on the user-supplied identifier
        List<DiscoveryInformation> discoveries = manager.discover(openIdIdentifier);

        // attempt to associate with the OpenID provider
        // and retrieve one service endpoint for authentication
        DiscoveryInformation discovered = manager.associate(discoveries);

        // store the discovery information in the user's session for later use
        // leave out for stateless operation / if there is no session
        httpSession.put("discovered", discovered);

        // obtain a AuthRequest message to be sent to the OpenID provider
        AuthRequest authReq = manager.authenticate(discovered, returnUrl);

        // Attribute Exchange
        FetchRequest fetch = FetchRequest.createFetchRequest();

        // different Open Id providers accept different attributes
        if (openIdIdentifier.contains("google.com")) {
            logger.debug("Open Id Identifier is: google.com");
            
            fetch.addAttribute("first", "http://axschema.org/namePerson/first", true);
            fetch.addAttribute("last", "http://axschema.org/namePerson/last", true);
            fetch.addAttribute("email", "http://axschema.org/contact/email", true);
            fetch.addAttribute("language", "http://axschema.org/pref/language", true);
        }
        else if (openIdIdentifier.contains("yahoo.com")) {
            logger.debug("Open Id Identifier is: yahoo.com");
            
            fetch.addAttribute("fullname", "http://axschema.org/namePerson", true);
            fetch.addAttribute("nickname", "http://axschema.org/namePerson/friendly", true);
            fetch.addAttribute("email", "http://axschema.org/contact/email", true);
            fetch.addAttribute("language", "http://axschema.org/pref/language", true);
        }
        else if (openIdIdentifier.contains("aol.com")) {
            logger.debug("Open Id Identifier is: aol.com");

            fetch.addAttribute("first", "http://axschema.org/namePerson/first", true);
            fetch.addAttribute("last", "http://axschema.org/namePerson/last", true);
            fetch.addAttribute("email", "http://axschema.org/contact/email", true);
            fetch.addAttribute("language", "http://axschema.org/pref/language", true);
        }
        else {
            logger.debug("Open Id Identifier is: something else");
            
            fetch.addAttribute("fullname", "http://schema.openid.net/namePerson", true); 
            fetch.addAttribute("email", "http://schema.openid.net/contact/email", true); 
            fetch.addAttribute("country", "http://axschema.org/contact/country/home", true);    
        }

        // attach the extension to the authentication request
        authReq.addExtension(fetch);

        logger.info("The request string is: " + authReq.getDestinationUrl(true).replaceAll("&", "\n"));

        return authReq.getDestinationUrl(true);
    }
    
    public static User getAuthenticatedUser(Map<String,String[]> parmList,
            final StringBuffer receivingURL, Map<String, Object> httpSession, Map<String, Object> application)
            throws MessageException, DiscoveryException, AssociationException {

        logger.debug("Entering getAuthenticatedUser()");

        // extract the parameters from the authentication response
        // (which comes in as a HTTP request from the OpenID provider)
        ParameterList openidResp = new ParameterList(parmList);
        
        // retrieve the previously stored discovery information
        final DiscoveryInformation discovered = (DiscoveryInformation) httpSession.get("discovered");

        // get a reference to the Consumer Manager
        ConsumerManager manager = getConsumerManager(application);
        
        // verify the response
        final VerificationResult verification = manager.verify(receivingURL.toString(), openidResp, discovered);
        
        // examine the verification result and extract the verified identifier
        Identifier verified = verification.getVerifiedId();
        if (verified == null) {
            return null;
        }

        AuthSuccess authSuccess = (AuthSuccess) verification.getAuthResponse();

        User user = new User();
        user.setOpenid(authSuccess.getIdentity());
        
        if (authSuccess.hasExtension(AxMessage.OPENID_NS_AX)) {
            logger.info("Processed as OPENID_NS_AX");
            
            FetchResponse fetchResp = (FetchResponse) authSuccess.getExtension(AxMessage.OPENID_NS_AX);

            // populate the User object with attributes from the FetchResponse
            user.setNickname(fetchResp.getAttributeValue("nickname"));
            user.setEmail(fetchResp.getAttributeValue("email"));
            user.setFullName(fetchResp.getAttributeValue("fullname"));
            user.setFirstName(fetchResp.getAttributeValue("first"));
            user.setLastName(fetchResp.getAttributeValue("last"));
            user.setLanguage(fetchResp.getAttributeValue("language"));
            user.setCountry(fetchResp.getAttributeValue("country"));

            logger.info("User: " + user.toString());
        }

        if (authSuccess.hasExtension(SRegMessage.OPENID_NS_SREG)) {
            logger.info("Processed as OPENID_NS_SREG");
            
            SRegResponse sregResp = (SRegResponse) authSuccess.getExtension(SRegMessage.OPENID_NS_SREG);

            // if we didn't get the user's email addy from the FetchResponse, try to get it from the SRegResponse 
            if (StringUtils.isBlank(user.getEmail())) {
                user.setEmail(sregResp.getAttributeValue("email"));
            }
        }
        return user;
    }
}

Integrating OpenId into my Struts 2 app - Part 3 - The LoginAction Action class

The LoginAction class controls the flow between the user's authentication request and the business bean that does our heavy lifting.  Aside from a Logout method that removes the user's http session, the two main methods in this class are as follows:
  • validateOpenId() - This method is called after the user selects an OpenID provider from the OpenID Selector component on the Login form.  The Action then calls the business bean (OpenIdAuthenticator) which generates the destination URL for OpenId Provider.  The request is then redirected to the destination URL.  It's important to note that we are now redirecting the user away from our web application.  Our application never sees or has access to the user's password.  The OpenID Provider will let us know if the use has successfully authenticated.
  •  validateOpenId() - Once the user has successfully authenticated with the OpenID Provider.  The Provider redirects the user back to our web application, using the return URL we provided.  This method processes that request.  Again we pass it off to the business bean and let it confirm the login was successful and glean any relevant information sent by the OpenID Provider.  The business bean will return a User object which we'll persist in the database and add to the user's HTTP Session.  At this point we'll redirect the user to the resource he was originally requesting before we forced him to authenticate.
Here's the Action class:
public class LoginAction extends FlashCardsAppBaseAction implements Preparable, ServletRequestAware, ServletResponseAware, SessionAware, ApplicationAware {

    static Logger logger = Logger.getLogger(LoginAction.class);

    // this is the only form field we will be looking for from OpenID Selector on the front end
    private String openid_identifier;

    // Hibernate Session
    private Session hibernateSession;
    
    // we'll need access to the Servlet spec objects, rather than just their attribute or parm maps
    private HttpServletRequest request;
    private HttpServletResponse response;
    
    // we'll be storing the User object in the Session
    private Map<String, Object> httpSession;
    
    // the OpenIdAuthenticator class needs access to the application to store a OpenId4Java related object
    private Map<String, Object> application;
    
    // we'll need to send this to the OpenId provider so it knows where to send its response 
    private final String returnAction = "/home/authenticateOpenId.action";

    // the OpenID Selector form will submit to this Action method
    public String validateOpenId() throws Exception {

        logger.debug("Entering validateOpenId()");
        
        // get rid of trailing slash
        if (getOpenid_identifier().endsWith("/")) {
            setOpenid_identifier(getOpenid_identifier().substring(0, getOpenid_identifier().length() - 1));
        }

        logger.debug("The requested OpenId identifier is: " + getOpenid_identifier());

        // determine a return_to URL where the application will receive
        // the authentication responses from the OpenID provider
        String returnUrl = getServerContext(request) + returnAction;
        
        // construct the destination Url to send to the Open Id provider
        String destinationUrl = OpenIdAuthenticator.getValidateOpenIdUrl(returnUrl, this.getOpenid_identifier(), httpSession, application); 
        
        // redirect to the Auth Request
        response.sendRedirect(destinationUrl);
        
        // no need to return a view
        return NONE;
    }
    
    public String authenticateOpenId() throws Exception {
        logger.debug("Entering authenticateOpenId()");

        Map<String,String[]> parmList = request.getParameterMap();

        // extract the receiving URL from the HTTP request
        final StringBuffer receivingURL = request.getRequestURL();
        final String queryString = request.getQueryString();

        if (queryString != null && queryString.length() > 0) {
            receivingURL.append("?").append(request.getQueryString());
        }
        
        logger.debug(receivingURL.toString().replaceAll("&", "\n"));

        // verify the user has authenticated with the Open Id provider and 
        // get a reference to the authenticated User
        User user = OpenIdAuthenticator.getAuthenticatedUser(parmList, receivingURL, httpSession, application);

        // save the user to the DB
        UserPersister uPersister = new UserPersister();
        uPersister.saveOrUpdateUser(user, hibernateSession);
        
        // add the user to the HTTP Session
        httpSession.put("user", user);
        
        // retrieve the original URL from the Session
        String desitinationURL = (String)httpSession.get("originalURL");

        // was a destination URL provided?
        if (desitinationURL == null) {
            logger.debug("No destination URL provided, will send to Home");
            return "home";
        }
        else {
            logger.debug("Redirecting to : " + desitinationURL);
            response.sendRedirect(desitinationURL);
            return NONE;
        }
    }

    @SuppressWarnings("rawtypes")
    public String logout() {
        logger.debug("Entering logout()");
        
        try {
            // invalidate the user's session
            httpSession.remove("user");

            if (httpSession instanceof org.apache.struts2.dispatcher.SessionMap) {
                try {
                    ((org.apache.struts2.dispatcher.SessionMap) httpSession).invalidate();
                } catch (IllegalStateException e) {
                    logger.error("Exception in logout()", e);
                }
            }
            
            return "success";
        } catch (Exception e) {
            logger.error("Exception in logout():", e);
            
            return "error";
        }
    }

    private String getServerContext(final HttpServletRequest request) {
        // Get the base url.
        final StringBuilder serverPath = new StringBuilder();
        
        serverPath.append(request.getScheme() + "://");
        serverPath.append(request.getServerName());

        if (request.getServerPort() != 80) {
            serverPath.append(":" + request.getServerPort());
        }
        serverPath.append(request.getContextPath());
        
        return serverPath.toString();
    }
    
    @Override
    public void prepare() throws Exception {
        logger.debug("Entering prepare()");

        hibernateSession = getHibernateSession();
    }

    public String getOpenid_identifier() {
        return openid_identifier;
    }

    public void setOpenid_identifier(String openid_identifier) {
        this.openid_identifier = openid_identifier;
    }

    @Override
    public void setSession(Map<String, Object> httpSession) {
        this.httpSession = httpSession;
    }
    
    @Override
    public void setServletResponse(final HttpServletResponse response) {
        this.response = response;
    }

    @Override
    public void setServletRequest(final HttpServletRequest request) {
        this.request = request;
    }

    @Override
    public void setApplication(Map<String, Object> application) {
        this.application = application;
    }
}

In the final post in this series, I'll include the code for the OpenIdAuthenticator business bean which does the heavy lifting and relies heavily on the OpenId4Java library.

Integrating OpenId into my Struts 2 app - Part 2 - Javascript OpenID Selector

Now that we're integrating OpenID authentication into our website, we'll need a way for users to select an OpenID provider.  According to Wikipedia ,there are over two dozen OpenID Providers.  It would seem a daunting task to create an attractive web form where a user can select from a list of approved providers.

Thankfully, the openid-selector project already exists and provides a "a user-friendly way to select an OpenID.  It has been designed so that users do not even need to know what OpenID is to use it, they simply select their account by a recognisable logo."

This component is quite easy to use and I was able to integrate it into my application very easily.

Here's the code from my JSP:
    <%@ taglib prefix="s" uri="/struts-tags"%>
    
    <s:actionerror theme="jquery" />
    <s:fielderror theme="jquery" />

    <h2>Login to the Flash Cards Application</h2>
    <br/>
    <!-- Simple OpenID Selector -->
    <s:form action="validateOpenId" namespace="/home" method="get" id="openid_form">
        <input type="hidden" name="action" value="verify" />
        <fieldset>
            <legend>Sign-in or Create New Account</legend>
            <div id="openid_choice">
                <p>Please click your account provider:</p>
                <div id="openid_btns"></div>
            </div>
            <div id="openid_input_area">
                <input id="openid_identifier" name="openid_identifier" type="text" value="http://" />
                <input id="openid_submit" type="submit" value="Sign-In"/>
            </div>
            <noscript>
                <p>OpenID is service that allows you to log-on to many different websites using a single indentity.
                Find out <a href="http://openid.net/what/">more about OpenID</a> and <a href="http://openid.net/get/">how to get an OpenID enabled account</a>.</p>
            </noscript>
        </fieldset>
    </s:form>
    <!-- /Simple OpenID Selector -->

Take note of the"openid_identifier" field.  This field hold the value of the selected OpenID Provider and we'll reference it in both the Struts Interceptor and the Authentication class.

Here's a screenshot of the form in my application:
In my next post, I'll discuss the Action class I'm using to "control" login traffic.

Integrating OpenId into my Struts 2 app - Part 1 - Interceptors

My next few blog posts will detail the integration of OpenID authentication into my application. The OpenId website describes the benefits of using OpenId.  As a web site user myself, I'm always relieved when I'm able to reuse my existing Google account when creating new a profile on a website.  That's one less User Id and Password I'll need to write down or remember.  I also appreciate that I'm safely authenticating with the OpenId provider and the underlying website never has access to my password.

Before I begin, I want to list and give props to several websites that provided invaluable information and code samples that greatly assisted me in integrating OpenId into my own application.

Struts 2 Interceptors

OpenId4Java Code samples

I'm not going into detail on how Interceptors work in Struts 2.  I suggest reading the relevant Struts 2 docs and the links I posted above.

Here's the code for the LoginInterceptor:
public class LoginInterceptor extends AbstractInterceptor implements StrutsStatics{
    
    static Logger logger = Logger.getLogger(LoginInterceptor.class);

    public String intercept(final ActionInvocation invocation) throws Exception {
        logger.debug("Entering intercept()");

        String invocatedAction = invocation.getAction().getClass().getName();
        logger.debug("Invocated Action: " + invocatedAction);

        // get references to the App Context, Session, and Request objects
        final ActionContext context = invocation.getInvocationContext ();
        HttpServletRequest request = (HttpServletRequest) context.get(HTTP_REQUEST);
        HttpSession session =  request.getSession (true);

        // Is there a "user" object stored in the user's HttpSession?
        User user = (User)session.getAttribute("user");
        
        if (user == null) {
            // The user has not logged in yet.
            logger.debug("User NOT found in the Session");

            // Is the user attempting to log in right now?
            String loginIdentifier = request.getParameter("openid_identifier");
            String openIdEndpoint =  request.getParameter("openid.op_endpoint");

            // we can know the user is trying to login right now if the "openid_identifier" is a Request parm
            if (! StringUtils.isBlank (loginIdentifier) ) {
                // The user is attempting to log in.
                logger.debug("The user is attempting to log in");

                // Process the user's login attempt.
                return invocation.invoke ();
            }
            // we know the user has just auth'd with the OpenID provider if the "openid.op_endpoint" is a Request parm
            else if(! StringUtils.isBlank (openIdEndpoint) ) {
                // The user has logged in with an OpenId provider
                logger.debug("The user has logged in with an OpenId provider");

                // Process the user's login attempt.
                return invocation.invoke ();
            }
            else {
                // save the original URL, we'll need it later
                saveReceivingURL(request, session);
                
                logger.debug("Forwarding to the Login form");
            }

            // it we get this far then the user hasn't tried to login yet, 
            // and we need to send to the login form.
            return "login";
        } 
           else {
               logger.debug("User " + user.toString() + " found in the Session");
               
               // user is already logged in
               return invocation.invoke ();
        }
    }

    private void saveReceivingURL(HttpServletRequest request, HttpSession session) {
        logger.debug("Entering saveReceivingURL()");
        
        // extract the receiving URL from the HTTP request
        final StringBuffer receivingURL = request.getRequestURL();
        final String queryString = request.getQueryString();

        // if there is a query string then we'll need that too
        if (queryString != null && queryString.length() > 0) {
            receivingURL.append("?").append(request.getQueryString());
        }
        
        logger.debug("Original URL: " + receivingURL.toString());
        
        // save the original URL in the Session
        // we're going to need to redirect the user back to this URL after login is completed
        session.setAttribute("originalURL", receivingURL.toString());
    }
}

The LoginInterceptor is invoked for every request and checks the http session to see if the user is already logged in.  If the user is found in the session, the Interceptor simply returns invocation.invoke () and allows the request to process normally.

If the user is not found in the session then we need to interrupt the request and force the user to authenticate before returning to the originally requested resource.  However, it's not as simple as just forwarding the user to the login page.  Remember, the Interceptor is invoked on every single request and we need to account for the following:
  • Is the user presently already trying to login?  If so, there's no need to interrupt the request because we're already going to the login form.
  • Is the request coming from the OpenId Provider (Google, Yahoo, etc)?  If so, again there's not need to interrupt the request.  Allow the request to proceed to our app's authentication code where we'll work with the response from the OpenId Provider.
  • Otherwise, we'll go ahead and interrupt the request and forward the user to the login form where they can choose which OpenId Provider they want to use for authentication.  Note: We'll need to keep a reference to the original requested resource so that we can send the user to it when the authentication is completed.
Now that we have the code for our Interceptor, we'll need add it to our Struts configuration and modify the Interceptor Stack.

        <interceptors>
            <interceptor name="hibernateSession" class="org.robbins.flashcards.presentation.HibernateSessionInterceptor"/>
            <interceptor name="loginInterceptor" class="org.robbins.flashcards.presentation.LoginInterceptor"/>

               <interceptor-stack name="defaultStackWithStore">
                   <interceptor-ref name="hibernateSession"/>
                   <interceptor-ref name="loginInterceptor"/>
                <interceptor-ref name="store">
                    <param name="operationMode">STORE</param>
                </interceptor-ref>
                <interceptor-ref name="defaultStack" />
            </interceptor-stack>
            
               <interceptor-stack name="defaultStackWithRetrieve">
                   <interceptor-ref name="hibernateSession"/>
                   <interceptor-ref name="loginInterceptor"/>
                <interceptor-ref name="store">
                    <param name="operationMode">RETRIEVE</param>
                </interceptor-ref>
                <interceptor-ref name="defaultStack" />
            </interceptor-stack>
        </interceptors>

Since I'm also using the "store" interceptor I've had to configure two interceptor stacks to deal with the Storing and Retrieving operations.  Below is an example of a an action declaration:

        <action name="*"
                class="org.robbins.flashcards.presentation.FlashCardAction"
                method="{1}">
               <interceptor-ref name="defaultStackWithRetrieve"/>
            <result name="success" type="tiles">{1}flashcard.tiles</result>
            <result name="error" type="tiles">error.tiles</result>
        </action>

In my next post, I'll discuss the JQuery OpenID Selector component into my Login form.